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