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