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