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