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