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