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