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