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