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