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