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