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