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