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