handle GNU tar named gnutar, and improve diagnostic messages
[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     for my $maybe_tar ($tar, qw(gtar gnutar)) {
146       my $version = `$maybe_tar --version 2>$dn`;
147       if ($version =~ /GNU tar/) {
148         $tar = $maybe_tar;
149         $gtar = 1;
150         last;
151       }
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 "Current $warn too large to create portable dist archives!  Max is ".(2**21-1).".\n"
174           ."Dist creation will most likely fail.  Install GNU tar and re-run Makefile.PL to fix this issue.\n";
175         my @try;
176         my $brew = `which brew 2>$dn`;
177         chomp $brew;
178         if (-x $brew) {
179           push @try, 'brew install gnu-tar';
180         }
181         my $ports = `which ports 2>$dn`;
182         chomp $ports;
183         if (-x $ports) {
184           push @try, 'sudo ports install gnutar';
185         }
186         if (@try) {
187           warn "Try" . (@try > 1 ? ' one of' : '') . ":\n"
188             . join '', map "    $_\n", @try;
189         }
190       }
191     }
192
193     $self->{TAR} = $tar;
194     $self->{TARFLAGS} = $tarflags;
195
196     $out;
197   }
198
199   sub tarfile_target {
200     my $self = shift;
201     my $out = $self->SUPER::tarfile_target(@_);
202     my $verify = <<'END_FRAG';
203         $(ABSPERLRUN) $(HELPERS)/verify-tarball $(DISTVNAME).tar $(DISTVNAME)/MANIFEST --tar="$(TAR)"
204 END_FRAG
205     $out =~ s{(\$\(TAR\).*\n)}{$1$verify};
206     $out;
207   }
208
209   sub dist_test {
210     my $self = shift;
211
212     my $include = '';
213     if (open my $fh, '<', 'maint/Makefile.include') {
214       $include = "\n# --- Makefile.include:\n\n" . do { local $/; <$fh> };
215       $include =~ s/\n?\z/\n/;
216     }
217
218     my @bump_targets =
219       grep { $include !~ /^bump$_(?: +\w+)*:/m } ('', 'minor', 'major');
220
221     my $distar_lib = File::Basename::dirname(__FILE__);
222     my $helpers = File::Spec->catdir($distar_lib, File::Spec->updir, 'helpers');
223
224     my $licenses = $self->{LICENSE} || $self->{META_ADD}{license} || $self->{META_MERGE}{license};
225     my $authors = $self->{AUTHOR};
226     $_ = ref $_ ? $_ : [$_ || ()]
227       for $licenses, $authors;
228
229     my %vars = (
230       DISTAR_LIB => $self->quote_literal($distar_lib),
231       HELPERS => $self->quote_literal($helpers),
232       REMAKE => join(' ', '$(PERLRUN)', '-I$(DISTAR_LIB)', '-MDistar', 'Makefile.PL', map { $self->quote_literal($_) } @ARGV),
233       BRANCH => $self->{BRANCH} ||= 'master',
234       CHANGELOG => $self->{CHANGELOG} ||= 'Changes',
235       DEV_NULL_STDOUT => ($self->{DEV_NULL} ? '>'.File::Spec->devnull : ''),
236       DISTTEST_MAKEFILE_PARAMS => '',
237       AUTHORS => $self->quote_literal(join(', ', @$authors)),
238       LICENSES => join(' ', map $self->quote_literal($_), @$licenses),
239       GET_CHANGELOG => '$(ABSPERLRUN) $(HELPERS)/get-changelog $(VERSION) $(CHANGELOG)',
240       UPDATE_DISTAR => (
241         -e File::Spec->catdir($distar_lib, File::Spec->updir, '.git')
242           ? 'git -C $(DISTAR_LIB) pull'
243           : '$(ECHO) "Distar code is not in a git repo, unable to update!"'
244       ),
245     );
246
247     my $dist_test = $self->SUPER::dist_test(@_);
248     $dist_test =~ s/(\bMakefile\.PL\b)/$1 \$(DISTTEST_MAKEFILE_PARAMS)/;
249
250     join('',
251       $dist_test,
252       "\n\n# --- Distar section:\n\n",
253       (map "$_ = $vars{$_}\n", sort keys %vars),
254       <<'END',
255
256 preflight: check-version check-manifest check-cpan-upload
257         $(ABSPERLRUN) $(HELPERS)/preflight $(VERSION) --changelog=$(CHANGELOG) --branch=$(BRANCH)
258 check-version:
259         $(ABSPERLRUN) $(HELPERS)/check-version $(VERSION) $(TO_INST_PM) $(EXE_FILES)
260 check-manifest:
261         $(ABSPERLRUN) $(HELPERS)/check-manifest
262 check-cpan-upload:
263         $(NOECHO) cpan-upload -h $(DEV_NULL_STDOUT)
264 releasetest:
265         $(MAKE) disttest RELEASE_TESTING=1 DISTTEST_MAKEFILE_PARAMS="PREREQ_FATAL=1" PASTHRU="$(PASTHRU) TEST_FILES=\"$(TEST_FILES)\""
266         $(NOECHO) $(TEST_F) $(DISTVNAME)/LICENSE || $(ECHO) "Failed to generate $(DISTVNAME)/LICENSE!" >&2
267         $(NOECHO) $(TEST_F) $(DISTVNAME)/LICENSE
268 release: preflight
269         $(MAKE) releasetest
270         $(GET_CHANGELOG) -p"Release commit for $(VERSION)" | git commit -a -F -
271         $(GET_CHANGELOG) -p"release v$(VERSION)" | git tag -a -F - "v$(VERSION)"
272         $(RM_RF) $(DISTVNAME)
273         $(MAKE) $(DISTVNAME).tar$(SUFFIX)
274         $(NOECHO) $(MAKE) pushrelease FAKE_RELEASE=$(FAKE_RELEASE)
275 pushrelease ::
276         $(NOECHO) $(NOOP)
277 pushrelease$(FAKE_RELEASE) ::
278         cpan-upload $(DISTVNAME).tar$(SUFFIX)
279         git push origin v$(VERSION) HEAD
280 distdir: readmefile licensefile
281 readmefile: create_distdir
282         $(NOECHO) $(TEST_F) $(DISTVNAME)/README || $(MAKE) $(DISTVNAME)/README
283 $(DISTVNAME)/README: $(VERSION_FROM)
284         $(NOECHO) $(MKPATH) $(DISTVNAME)
285         pod2text $(VERSION_FROM) >$(DISTVNAME)/README
286         $(NOECHO) $(ABSPERLRUN) $(HELPERS)/add-to-manifest -d $(DISTVNAME) README
287 distsignature: readmefile licensefile
288 licensefile: create_distdir
289         $(NOECHO) $(TEST_F) $(DISTVNAME)/LICENSE || $(MAKE) $(DISTVNAME)/LICENSE || $(TRUE)
290 $(DISTVNAME)/LICENSE: Makefile.PL
291         $(NOECHO) $(MKPATH) $(DISTVNAME)
292         $(ABSPERLRUN) $(HELPERS)/generate-license -o $(DISTVNAME)/LICENSE $(AUTHORS) $(LICENSES)
293         $(NOECHO) $(ABSPERLRUN) $(HELPERS)/add-to-manifest -d $(DISTVNAME) LICENSE
294 disttest: distmanicheck
295 distmanicheck: create_distdir
296         cd $(DISTVNAME) && $(ABSPERLRUN) "-MExtUtils::Manifest=manicheck" -e "exit manicheck"
297 nextrelease:
298         $(ABSPERLRUN) $(HELPERS)/add-changelog-heading --git $(VERSION) $(CHANGELOG)
299 refresh:
300         $(UPDATE_DISTAR)
301         $(RM_F) $(FIRST_MAKEFILE)
302         $(REMAKE)
303 END
304       map(sprintf(<<'END', "bump$_", ($_ || '$(V)')), @bump_targets),
305 %s:
306         $(ABSPERLRUN) $(HELPERS)/bump-version --git $(VERSION) %s
307         $(RM_F) $(FIRST_MAKEFILE)
308         $(REMAKE)
309 END
310       $include,
311       "\n",
312     );
313   }
314 }
315
316 1;
317 __END__
318
319 =head1 NAME
320
321 Distar - Additions to ExtUtils::MakeMaker for dist authors
322
323 =head1 SYNOPSIS
324
325 F<Makefile.PL>:
326
327   use ExtUtils::MakeMaker;
328   (do './maint/Makefile.PL.include' or die $@) unless -f 'META.yml';
329
330   WriteMakefile(...);
331
332 F<maint/Makefile.PL.include>:
333
334   BEGIN { -e 'Distar' or system("git clone git://git.shadowcat.co.uk/p5sagit/Distar.git") }
335   use lib 'Distar/lib';
336   use Distar 0.001;
337
338   author 'A. U. Thor <author@cpan.org>';
339
340   manifest_include t => 'test-helper.pl';
341   manifest_include corpus => '.txt';
342
343 make commmands:
344
345   $ perl Makefile.PL
346   $ make bump             # bump version
347   $ make bump V=2.000000  # bump to specific version
348   $ make bumpminor        # bump minor version component
349   $ make bumpmajor        # bump major version component
350   $ make nextrelease      # add version heading to Changes file
351   $ make releasetest      # build dist and test (with xt/ and RELEASE_TESTING=1)
352   $ make preflight        # check that repo and file state is release ready
353   $ make release          # check releasetest and preflight, commits and tags,
354                           # builds and uploads to CPAN, and pushes commits and
355                           # tag
356   $ make release FAKE_RELEASE=1
357                           # builds a release INCLUDING committing and tagging,
358                           # but does not upload to cpan or push anything to git
359
360 =head1 DESCRIPTION
361
362 L<ExtUtils::MakeMaker> works well enough as development tool for
363 builting and testing, but using it to release is annoying and error prone.
364 Distar adds just enough to L<ExtUtils::MakeMaker> for it to be a usable dist
365 author tool.  This includes extra commands for releasing and safety checks, and
366 automatic generation of some files.  It doesn't require any non-core modules and
367 is compatible with old versions of perl.
368
369 =head1 FUNCTIONS
370
371 =head2 author( $author )
372
373 Set the author to include in generated META files.  Can be a single entry, or
374 an arrayref.
375
376 =head2 manifest_include( $dir, $pattern )
377
378 Add a pattern to include files in the MANIFEST file, and thus in the generated
379 dist files.
380
381 The pattern can be either a regex, or a path suffix.  It will be applied to the
382 full path past the directory specified.
383
384 The default files that are always included are: F<.pm> and F<.pod> files in
385 F<lib>, F<.t> files in F<t> and F<xt>, F<.pm> files in F<t/lib> and F<xt/lib>,
386 F<Changes>, F<MANIFEST>, F<README>, F<LICENSE>, F<META.yml>, and F<.PL> files in
387 the dist root, and all files in F<maint>.
388
389 =head1 AUTOGENERATED FILES
390
391 =over 4
392
393 =item F<MANIFEST.SKIP>
394
395 The F<MANIFEST.SKIP> will be automatically generated to exclude any files not
396 explicitly allowed via C<manifest_include> or the included defaults.  It will be
397 created (or updated) at C<perl Makefile.PL> time.
398
399 =item F<README>
400
401 The F<README> file will be generated at dist generation time, inside the built
402 dist.  It will be generated using C<pod2text> on the main module.
403
404 If a F<README> file exists in the repo, it will be used directly instead of
405 generating the file.
406
407 =back
408
409 =head1 MAKE COMMMANDS
410
411 =head2 test
412
413 test will be adjusted to include F<xt/> tests by default.  This will only apply
414 for authors, not users installing from CPAN.
415
416 =head2 release
417
418 Releases the dist.  Before releasing, checks will be done on the dist using the
419 C<preflight> and C<releasetest> commands.
420
421 Releasing will generate a dist tarball and upload it to CPAN using cpan-upload.
422 It will also create a git tag for the release, and push the tag and branch.
423
424 =head3 FAKE_RELEASE
425
426 If release is run with FAKE_RELEASE=1 set, it will skip uploading to CPAN and
427 pushing to git.  A release commit will still be created and tagged locally.
428
429 =head2 preflight
430
431 Performs a number of checks on the files and repository, ensuring it is in a
432 sane state to do a release.  The checks are:
433
434 =over 4
435
436 =item * All version numbers match
437
438 =item * The F<MANIFEST> file is up to date
439
440 =item * The branch is correct
441
442 =item * There is no existing tag for the version
443
444 =item * There are no unmerged upstream changes
445
446 =item * There are no outstanding local changes
447
448 =item * There is an appropriate staged Changes heading
449
450 =item * cpan-upload is available
451
452 =back
453
454 =head2 releasetest
455
456 Test the dist preparing for a release.  This generates a dist dir and runs the
457 tests from inside it.  This ensures all appropriate files are included inside
458 the dist.  C<RELEASE_TESTING> will be set in the environment.
459
460 =head2 nextrelease
461
462 Adds an appropriate changelog heading for the release, and prompts to stage the
463 change.
464
465 =head2 bump
466
467 Bumps the version number.  This will try to preserve the length and format of
468 the version number.  The least significant digit will be incremented.  Versions
469 with underscores will preserve the underscore in the same position.
470
471 Optionally accepts a C<V> option to set the version to a specific value.
472
473 The version changes will automatically be committed.  Unstaged modifications to
474 the files will be left untouched.
475
476 =head3 V
477
478 The V option will be passed along to the version bumping script.  It can accept
479 a space separated list of options, including an explicit version number.
480
481 Options:
482
483 =over 4
484
485 =item --force
486
487 Updates version numbers even if they do not match the current expected version
488 number.
489
490 =item --stable
491
492 Attempts to convert the updated version to a stable version, removing any
493 underscore.
494
495 =item --alpha
496
497 Attempts to convert the updated version to an alpha version, adding an
498 underscore in an appropriate place.
499
500 =back
501
502 =head2 bumpminor
503
504 Like bump, but increments the minor segment of the version.  This will treat
505 numeric versions as x.yyyzzz format, incrementing the yyy segment.
506
507 =head2 bumpmajor
508
509 Like bumpminor, but bumping the major segment.
510
511 =head2 refresh
512
513 Updates Distar and re-runs C<perl Makefile.PL>
514
515 =head1 SUPPORT
516
517 IRC: #web-simple on irc.perl.org
518
519 Git repository: L<git://git.shadowcat.co.uk/p5sagit/Distar>
520
521 Git browser: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=p5sagit/Distar.git;a=summary>
522
523 =head1 AUTHOR
524
525 mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
526
527 =head1 CONTRIBUTORS
528
529 haarg - Graham Knop (cpan:HAARG) <haarg@cpan.org>
530
531 ether - Karen Etheridge (cpan:ETHER) <ether@cpan.org>
532
533 frew - Arthur Axel "fREW" Schmidt (cpan:FREW) <frioux@gmail.com>
534
535 Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
536
537 =head1 COPYRIGHT
538
539 Copyright (c) 2011-2015 the Distar L</AUTHOR> and L</CONTRIBUTORS>
540 as listed above.
541
542 =head1 LICENSE
543
544 This library is free software and may be distributed under the same terms
545 as perl itself. See L<http://dev.perl.org/licenses/>.
546
547 =cut