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