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