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