include provides metadata
[p5sagit/Distar.git] / lib / Distar.pm
1 package Distar;
2 use strict;
3 use warnings FATAL => 'all';
4 use base qw(Exporter);
5 use ExtUtils::MakeMaker ();
6 use ExtUtils::MM ();
7 use File::Spec ();
8
9 our $VERSION = '0.002000';
10 $VERSION = eval $VERSION;
11
12 my $MM_VER = eval $ExtUtils::MakeMaker::VERSION;
13
14 our @EXPORT = qw(
15   author manifest_include readme_generator
16 );
17
18 sub import {
19   strict->import;
20   warnings->import(FATAL => 'all');
21   shift->export_to_level(1,@_);
22 }
23
24 sub author {
25   our $Author = shift;
26   $Author = [ $Author ]
27     if !ref $Author;
28 }
29
30 our @Manifest = (
31   'lib' => '.pm',
32   'lib' => '.pod',
33   't' => '.t',
34   't/lib' => '.pm',
35   'xt' => '.t',
36   'xt/lib' => '.pm',
37   '' => qr{[^/]*\.PL},
38   '' => qr{Changes|MANIFEST|README|LICENSE|META\.yml},
39   'maint' => qr{[^.].*},
40 );
41
42 sub manifest_include {
43   push @Manifest, @_;
44 }
45
46 sub readme_generator {
47   die "readme_generator unsupported" if @_ && $_[0];
48 }
49
50 sub write_manifest_skip {
51   my ($mm) = @_;
52   my @files = @Manifest;
53   my @parts;
54   while (my ($dir, $spec) = splice(@files, 0, 2)) {
55     my $re = ($dir ? $dir.'/' : '').
56       ((ref($spec) eq 'Regexp')
57         ? $spec
58         : !ref($spec)
59           ? ".*\Q${spec}\E"
60             # print ref as well as stringification in case of overload ""
61           : die "spec must be string or regexp, was: ${spec} (${\ref $spec})");
62     push @parts, $re;
63   }
64   my $dist_name = $mm->{DISTNAME};
65   my $include = join '|', map "${_}\$", @parts;
66   my $final = "^(?:\Q$dist_name\E-v?[0-9_.]+/|(?!$include))";
67   open my $skip, '>', 'MANIFEST.SKIP'
68     or die "can't open MANIFEST.SKIP: $!";
69   print $skip "${final}\n";
70   close $skip;
71 }
72
73 sub _clone {
74   map {
75     if (ref eq 'HASH') {
76       my $struct = $_;
77       { map +($_ => _clone($struct->{$_})), keys %$struct };
78     }
79     elsif (ref eq 'ARRAY') {
80       [ _clone(@$_) ];
81     }
82     else {
83       $_;
84     }
85   } @_;
86 }
87
88 {
89   package Distar::MM;
90   our @ISA = @MM::ISA;
91   @MM::ISA = (__PACKAGE__);
92
93   sub new {
94     my ($class, $args) = @_;
95     my %test = %{$args->{test}||{}};
96     my $tests = $test{TESTS} || 't/*.t';
97     $tests !~ /\b\Q$_\E\b/ and $tests .= " $_"
98       for 'xt/*.t', 'xt/*/*.t';
99     $test{TESTS} = $tests;
100     return $class->SUPER::new({
101       LICENSE => 'perl_5',
102       MIN_PERL_VERSION => '5.006',
103       ($Distar::Author ? (
104         AUTHOR => ($MM_VER >= 6.5702 ? $Distar::Author : join(', ', @$Distar::Author)),
105       ) : ()),
106       (exists $args->{ABSTRACT} ? () : (ABSTRACT_FROM => $args->{VERSION_FROM})),
107       %$args,
108       test => \%test,
109       realclean => { FILES => (
110         ($args->{realclean}{FILES}||'')
111         . ' Distar/ MANIFEST.SKIP MANIFEST MANIFEST.bak'
112       ) },
113     });
114   }
115
116   sub metafile_data {
117     my $self = shift;
118     my $meta = { Distar::_clone($self->SUPER::metafile_data(@_) };
119
120     my $spec_ver = ($meta->{'meta-spec'} && $meta->{'meta-spec'}{version} || 1.4;
121     my $resources = $meta->{resources} ||= {};
122
123     {
124       my $no_index_dir = ${ $meta->{no_index}||={} }{directory} ||= [];
125       my %seen;
126       @$no_index_dir = grep !$seen{$_}++, @$no_index_dir, grep -d, qw(t xt);
127     }
128
129     $resources->{bugtracker} ||= do {
130       (my $queue = $meta->{name} || $self->{NAME}) =~ s/::/-/g;
131       my $rt_link = 'https://rt.cpan.org/Dist/Display.html?Name='.$queue;
132       $spec_ver < 2 ? $rt_link : {
133         web => $rt_link,
134         mailto => "bug-$queue\@rt.cpan.org",
135       };
136     };
137
138     {
139       my $license = $meta->{license} = [
140         map { $_ eq 'perl' ? 'perl_5' : $_ }
141         map { ref ? @$_ : $_ }
142         ( $meta->{license} || $self->{LICENSE} || 'perl_5' )
143       ];
144
145       $resources->{license} ||= [ 'http://dev.perl.org/licenses/' ]
146         if @$license == 1 && $license->[0] eq 'perl_5';
147     }
148
149     $meta->{x_contributors} ||= do {
150       my $author_email =
151         join '|',
152         map quotemeta,
153         map /(\<[^<>]+\@[^<>]\>)/,
154         @$Author;
155       [
156         grep !/$author_email/,
157         map { chomp; s/^\s*\d+\s*//; utf8::decode($_); $_ }
158         `git shortlog -s --email HEAD`
159       ];
160     };
161
162     if ( eval { require CPAN::Meta; require Module::Metadata; } ) {
163       $meta->{provides} ||= do {
164         my $meta_obj = CPAN::Meta->new($meta, { lazy_validation => 1 });
165         my @files = `git ls-files`;
166         chomp @files;
167         my $provides = Module::Metadata->package_versions_from_directory('.', \@files);
168         for my $module (keys %$provides) {
169           delete $provides->{$module}
170             unless $meta_obj->should_index_package($module)
171               && $meta_obj->should_index_file($provides->{$module}{file});
172         }
173         $provides;
174       };
175     }
176
177     %$meta;
178   }
179
180   sub flush {
181     my $self = shift;
182     `git ls-files --error-unmatch MANIFEST.SKIP 2>&1`;
183     my $maniskip_tracked = !$?;
184
185     Distar::write_manifest_skip($self)
186       unless $maniskip_tracked;
187
188     my @results = @{$self->{RESULT}};
189     if ($MM_VER < 7) {
190       utf8::encode($_) for @results;
191     }
192     local $self->{RESULT} = \@results;
193     $self->SUPER::flush(@_);
194   }
195
196   sub special_targets {
197     my $self = shift;
198     my $targets = $self->SUPER::special_targets(@_);
199     my $phony_targets = join ' ', qw(
200       preflight
201       check-version
202       check-manifest
203       check-cpan-upload
204       releasetest
205       release
206       readmefile
207       distmanicheck
208       nextrelease
209       refresh
210       bump
211       bumpmajor
212       bumpminor
213     );
214     $targets =~ s/^(\.PHONY *:.*)/$1 $phony_targets/m;
215     $targets;
216   }
217
218   sub init_dist {
219     my $self = shift;
220     my $pre_tar = $self->{TAR};
221     my $out = $self->SUPER::init_dist(@_);
222
223     my $tar = $self->{TAR};
224     my $gtar;
225     my $set_user;
226     my $version = `$tar --version`;
227     if ($version =~ /GNU tar/) {
228       $gtar = 1;
229     }
230     elsif (!$pre_tar && `gtar --version`) {
231       $tar = 'gtar';
232       $gtar = 1;
233     }
234     my $tarflags = $self->{TARFLAGS};
235     if (my ($flags) = $tarflags =~ /^-?([cvhlLf]+)$/) {
236       if ($flags =~ s/c// && $flags =~ s/f//) {
237         $tarflags = '--format=ustar -c'.$flags.'f';
238         if ($gtar) {
239           $tarflags = '--owner=0 --group=0 '.$tarflags;
240           $set_user = 1;
241         }
242       }
243     }
244
245     if (!$set_user) {
246       my $warn = '';
247       if ($> >= 2**21) {
248         $warn .= "uid ($>)";
249       }
250       if ($) >= 2**21) {
251         $warn .= ($warn ? ' and ' : '').'gid('.(0+$)).')';
252       }
253       if ($warn) {
254         warn "$warn too large!  Max is ".(2**21-1).".\n"
255           ."Dist creation will likely fail.  Install GNU tar to work around.\n";
256       }
257     }
258
259     $self->{TAR} = $tar;
260     $self->{TARFLAGS} = $tarflags;
261
262     $out;
263   }
264
265   sub tarfile_target {
266     my $self = shift;
267     my $out = $self->SUPER::tarfile_target(@_);
268     my $verify = <<'END_FRAG';
269         $(ABSPERLRUN) $(HELPERS)/verify-tarball $(DISTVNAME).tar $(DISTVNAME)/MANIFEST --tar="$(TAR)"
270 END_FRAG
271     $out =~ s{(\$\(TAR\).*\n)}{$1$verify};
272     $out;
273   }
274
275   sub dist_test {
276     my $self = shift;
277
278     my $include = '';
279     if (open my $fh, '<', 'maint/Makefile.include') {
280       $include = "\n# --- Makefile.include:\n\n" . do { local $/; <$fh> };
281       $include =~ s/\n?\z/\n/;
282     }
283
284     my @bump_targets =
285       grep { $include !~ /^bump$_(?: +\w+)*:/m } ('', 'minor', 'major');
286
287     my $distar = File::Spec->catdir(
288       File::Spec->catpath((File::Spec->splitpath(__FILE__))[0,1], ''),
289       File::Spec->updir,
290     );
291     my $helpers = File::Spec->catdir($distar, 'helpers');
292
293     my %vars = (
294       DISTAR => $self->quote_literal($distar),
295       HELPERS => $self->quote_literal($helpers),
296       REMAKE => join(' ', '$(PERLRUN)', '-I$(DISTAR)/lib', '-mDistar', 'Makefile.PL', map { $self->quote_literal($_) } @ARGV),
297       BRANCH => $self->{BRANCH} ||= 'master',
298       CHANGELOG => $self->{CHANGELOG} ||= 'Changes',
299       DEV_NULL_STDOUT => ($self->{DEV_NULL} ? '>'.File::Spec->devnull : ''),
300       DISTTEST_MAKEFILE_PARAMS => '',
301     );
302
303     my $dist_test = $self->SUPER::dist_test(@_);
304     $dist_test =~ s/(\bMakefile\.PL\b)/$1 \$(DISTTEST_MAKEFILE_PARAMS)/;
305
306     join('',
307       $dist_test,
308       "\n\n# --- Distar section:\n\n",
309       (map "$_ = $vars{$_}\n", sort keys %vars),
310       <<'END',
311
312 preflight: check-version check-manifest check-cpan-upload
313         $(ABSPERLRUN) $(HELPERS)/preflight $(VERSION) --changelog=$(CHANGELOG) --branch=$(BRANCH)
314 check-version:
315         $(ABSPERLRUN) $(HELPERS)/check-version $(VERSION) $(TO_INST_PM) $(EXE_FILES)
316 check-manifest:
317         $(ABSPERLRUN) $(HELPERS)/check-manifest
318 check-cpan-upload:
319         $(NOECHO) cpan-upload -h $(DEV_NULL_STDOUT)
320 releasetest:
321         $(MAKE) disttest RELEASE_TESTING=1 DISTTEST_MAKEFILE_PARAMS="PREREQ_FATAL=1" PASTHRU="$(PASTHRU) TEST_FILES=\"$(TEST_FILES)\""
322 release: preflight
323         $(MAKE) releasetest
324         git commit -a -m "Release commit for $(VERSION)"
325         git tag v$(VERSION) -m "release v$(VERSION)"
326         $(RM_RF) $(DISTVNAME)
327         $(MAKE) $(DISTVNAME).tar$(SUFFIX)
328         $(NOECHO) $(MAKE) pushrelease FAKE_RELEASE=$(FAKE_RELEASE)
329 pushrelease ::
330         $(NOECHO) $(NOOP)
331 pushrelease$(FAKE_RELEASE) ::
332         cpan-upload $(DISTVNAME).tar$(SUFFIX)
333         git push origin v$(VERSION) HEAD
334 distdir: readmefile
335 readmefile: create_distdir
336         $(NOECHO) $(TEST_F) $(DISTVNAME)/README || $(MAKE) $(DISTVNAME)/README
337 $(DISTVNAME)/README: $(VERSION_FROM)
338         $(NOECHO) $(MKPATH) $(DISTVNAME)
339         pod2text $(VERSION_FROM) >$(DISTVNAME)/README
340         $(NOECHO) $(ABSPERLRUN) $(HELPERS)/add-to-manifest -d $(DISTVNAME) README
341 distsignature: readmefile
342 disttest: distmanicheck
343 distmanicheck: create_distdir
344         cd $(DISTVNAME) && $(ABSPERLRUN) "-MExtUtils::Manifest=manicheck" -e "exit manicheck"
345 nextrelease:
346         $(ABSPERLRUN) $(HELPERS)/add-changelog-heading --git $(VERSION) $(CHANGELOG)
347 refresh:
348         cd $(DISTAR) && git pull || $(TRUE)
349         $(RM_F) $(FIRST_MAKEFILE)
350         $(REMAKE)
351 END
352       map(sprintf(<<'END', "bump$_", ($_ || '$(V)')), @bump_targets),
353 %s:
354         $(ABSPERLRUN) $(HELPERS)/bump-version --git $(VERSION) %s
355         $(RM_F) $(FIRST_MAKEFILE)
356         $(REMAKE)
357 END
358       $include,
359       "\n",
360     );
361   }
362 }
363
364 1;
365 __END__
366
367 =head1 NAME
368
369 Distar - Additions to ExtUtils::MakeMaker for dist authors
370
371 =head1 SYNOPSIS
372
373 F<Makefile.PL>:
374
375   use ExtUtils::MakeMaker;
376   (do './maint/Makefile.PL.include' or die $@) unless -f 'META.yml';
377
378   WriteMakefile(...);
379
380 F<maint/Makefile.PL.include>:
381
382   BEGIN { -e 'Distar' or system("git clone git://git.shadowcat.co.uk/p5sagit/Distar.git") }
383   use lib 'Distar/lib';
384   use Distar 0.001;
385
386   author 'A. U. Thor <author@cpan.org>';
387
388   manifest_include t => 'test-helper.pl';
389   manifest_include corpus => '.txt';
390
391 make commmands:
392
393   $ perl Makefile.PL
394   $ make bump             # bump version
395   $ make bump V=2.000000  # bump to specific version
396   $ make bumpminor        # bump minor version component
397   $ make bumpmajor        # bump major version component
398   $ make nextrelease      # add version heading to Changes file
399   $ make releasetest      # build dist and test (with xt/ and RELEASE_TESTING=1)
400   $ make preflight        # check that repo and file state is release ready
401   $ make release          # check releasetest and preflight, commits and tags,
402                           # builds and uploads to CPAN, and pushes commits and
403                           # tag
404   $ make release FAKE_RELEASE=1
405                           # builds a release INCLUDING committing and tagging,
406                           # but does not upload to cpan or push anything to git
407
408 =head1 DESCRIPTION
409
410 L<ExtUtils::MakeMaker> works well enough as development tool for
411 builting and testing, but using it to release is annoying and error prone.
412 Distar adds just enough to L<ExtUtils::MakeMaker> for it to be a usable dist
413 author tool.  This includes extra commands for releasing and safety checks, and
414 automatic generation of some files.  It doesn't require any non-core modules and
415 is compatible with old versions of perl.
416
417 =head1 FUNCTIONS
418
419 =head2 author( $author )
420
421 Set the author to include in generated META files.  Can be a single entry, or
422 an arrayref.
423
424 =head2 manifest_include( $dir, $pattern )
425
426 Add a pattern to include files in the MANIFEST file, and thus in the generated
427 dist files.
428
429 The pattern can be either a regex, or a path suffix.  It will be applied to the
430 full path past the directory specified.
431
432 The default files that are always included are: F<.pm> and F<.pod> files in
433 F<lib>, F<.t> files in F<t> and F<xt>, F<.pm> files in F<t/lib> and F<xt/lib>,
434 F<Changes>, F<MANIFEST>, F<README>, F<LICENSE>, F<META.yml>, and F<.PL> files in
435 the dist root, and all files in F<maint>.
436
437 =head1 AUTOGENERATED FILES
438
439 =over 4
440
441 =item F<MANIFEST.SKIP>
442
443 The F<MANIFEST.SKIP> will be automatically generated to exclude any files not
444 explicitly allowed via C<manifest_include> or the included defaults.  It will be
445 created (or updated) at C<perl Makefile.PL> time.
446
447 =item F<README>
448
449 The F<README> file will be generated at dist generation time, inside the built
450 dist.  It will be generated using C<pod2text> on the main module.
451
452 If a F<README> file exists in the repo, it will be used directly instead of
453 generating the file.
454
455 =back
456
457 =head1 MAKE COMMMANDS
458
459 =head2 test
460
461 test will be adjusted to include F<xt/> tests by default.  This will only apply
462 for authors, not users installing from CPAN.
463
464 =head2 release
465
466 Releases the dist.  Before releasing, checks will be done on the dist using the
467 C<preflight> and C<releasetest> commands.
468
469 Releasing will generate a dist tarball and upload it to CPAN using cpan-upload.
470 It will also create a git tag for the release, and push the tag and branch.
471
472 =head3 FAKE_RELEASE
473
474 If release is run with FAKE_RELEASE=1 set, it will skip uploading to CPAN and
475 pushing to git.  A release commit will still be created and tagged locally.
476
477 =head2 preflight
478
479 Performs a number of checks on the files and repository, ensuring it is in a
480 sane state to do a release.  The checks are:
481
482 =over 4
483
484 =item * All version numbers match
485
486 =item * The F<MANIFEST> file is up to date
487
488 =item * The branch is correct
489
490 =item * There is no existing tag for the version
491
492 =item * There are no unmerged upstream changes
493
494 =item * There are no outstanding local changes
495
496 =item * There is an appropriate staged Changes heading
497
498 =item * cpan-upload is available
499
500 =back
501
502 =head2 releasetest
503
504 Test the dist preparing for a release.  This generates a dist dir and runs the
505 tests from inside it.  This ensures all appropriate files are included inside
506 the dist.  C<RELEASE_TESTING> will be set in the environment.
507
508 =head2 nextrelease
509
510 Adds an appropriate changelog heading for the release, and prompts to stage the
511 change.
512
513 =head2 bump
514
515 Bumps the version number.  This will try to preserve the length and format of
516 the version number.  The least significant digit will be incremented.  Versions
517 with underscores will preserve the underscore in the same position.
518
519 Optionally accepts a C<V> option to set the version to a specific value.
520
521 The version changes will automatically be committed.  Unstaged modifications to
522 the files will be left untouched.
523
524 =head3 V
525
526 The V option will be passed along to the version bumping script.  It can accept
527 a space separated list of options, including an explicit version number.
528
529 Options:
530
531 =over 4
532
533 =item --force
534
535 Updates version numbers even if they do not match the current expected version
536 number.
537
538 =item --stable
539
540 Attempts to convert the updated version to a stable version, removing any
541 underscore.
542
543 =item --alpha
544
545 Attempts to convert the updated version to an alpha version, adding an
546 underscore in an appropriate place.
547
548 =back
549
550 =head2 bumpminor
551
552 Like bump, but increments the minor segment of the version.  This will treat
553 numeric versions as x.yyyzzz format, incrementing the yyy segment.
554
555 =head2 bumpmajor
556
557 Like bumpminor, but bumping the major segment.
558
559 =head2 refresh
560
561 Updates Distar and re-runs C<perl Makefile.PL>
562
563 =head1 SUPPORT
564
565 IRC: #web-simple on irc.perl.org
566
567 Git repository: L<git://git.shadowcat.co.uk/p5sagit/Distar>
568
569 Git browser: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=p5sagit/Distar.git;a=summary>
570
571 =head1 AUTHOR
572
573 mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
574
575 =head1 CONTRIBUTORS
576
577 haarg - Graham Knop (cpan:HAARG) <haarg@cpan.org>
578
579 ether - Karen Etheridge (cpan:ETHER) <ether@cpan.org>
580
581 frew - Arthur Axel "fREW" Schmidt (cpan:FREW) <frioux@gmail.com>
582
583 Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
584
585 =head1 COPYRIGHT
586
587 Copyright (c) 2011-2015 the Distar L</AUTHOR> and L</CONTRIBUTORS>
588 as listed above.
589
590 =head1 LICENSE
591
592 This library is free software and may be distributed under the same terms
593 as perl itself. See L<http://dev.perl.org/licenses/>.
594
595 =cut