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