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