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