c501895d58e8f8ec9f512f7411a80275e5e6d388
[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 disttest: distmanicheck
195 distmanicheck: create_distdir
196         cd $(DISTVNAME) && $(ABSPERLRUN) "-MExtUtils::Manifest=manicheck" -e "exit manicheck"
197 nextrelease:
198         $(ABSPERLRUN) $(HELPERS)/add-changelog-heading --git $(VERSION) $(CHANGELOG)
199 refresh:
200         cd $(DISTAR) && git pull
201         $(RM_F) $(FIRST_MAKEFILE)
202         $(REMAKE)
203 END
204       map(sprintf(<<'END', "bump$_", ($_ || '$(V)')), @bump_targets),
205 %s:
206         $(ABSPERLRUN) $(HELPERS)/bump-version --git $(VERSION) %s
207         $(RM_F) $(FIRST_MAKEFILE)
208         $(REMAKE)
209 END
210       $include,
211       "\n",
212     );
213   }
214 }
215
216 1;
217 __END__
218
219 =head1 NAME
220
221 Distar - Additions to ExtUtils::MakeMaker for dist authors
222
223 =head1 SYNOPSIS
224
225 F<Makefile.PL>:
226
227   use ExtUtils::MakeMaker;
228   (do 'maint/Makefile.PL.include' or die $@) unless -f 'META.yml';
229
230   WriteMakefile(...);
231
232 F<maint/Makefile.PL.include>:
233
234   BEGIN { -e 'Distar' or system("git clone git://git.shadowcat.co.uk/p5sagit/Distar.git") }
235   use lib 'Distar/lib';
236   use Distar 0.001;
237
238   author 'A. U. Thor <author@cpan.org>';
239
240   manifest_include t => 'test-helper.pl';
241   manifest_include corpus => '.txt';
242
243 make commmands:
244
245   $ perl Makefile.PL
246   $ make bump             # bump version
247   $ make bump V=2.000000  # bump to specific version
248   $ make bumpminor        # bump minor version component
249   $ make bumpmajor        # bump major version component
250   $ make nextrelease      # add version heading to Changes file
251   $ make releasetest      # build dist and test (with xt/ and RELEASE_TESTING=1)
252   $ make preflight        # check that repo and file state is release ready
253   $ make release          # check releasetest and preflight, then build and
254                           # upload to CPAN, tag release, push tag and branch
255
256 =head1 DESCRIPTION
257
258 L<ExtUtils::MakeMaker> works well enough as development tool for
259 builting and testing, but using it to release is annoying and error prone.
260 Distar adds just enough to L<ExtUtils::MakeMaker> for it to be a usable dist
261 author tool.  This includes extra commands for releasing and safety checks, and
262 automatic generation of some files.  It doesn't require any non-core modules and
263 is compatible with old versions of perl.
264
265 =head1 FUNCTIONS
266
267 =head2 author( $author )
268
269 Set the author to include in generated META files.  Can be a single entry, or
270 an arrayref.
271
272 =head2 manifest_include( $dir, $pattern )
273
274 Add a pattern to include files in the MANIFEST file, and thus in the generated
275 dist files.
276
277 The pattern can be either a regex, or a path suffix.  It will be applied to the
278 full path past the directory specified.
279
280 The default files that are always included are: F<.pm> and F<.pod> files in
281 F<lib>, F<.t> files in F<t> and F<xt>, F<.pm> files in F<t/lib> and F<xt/lib>,
282 F<Changes>, F<MANIFEST>, F<README>, F<LICENSE>, F<META.yml>, and F<.PL> files in
283 the dist root, and all files in F<maint>.
284
285 =head1 AUTOGENERATED FILES
286
287 =over 4
288
289 =item F<MANIFEST.SKIP>
290
291 The F<MANIFEST.SKIP> will be automatically generated to exclude any files not
292 explicitly allowed via C<manifest_include> or the included defaults.  It will be
293 created (or updated) at C<perl Makefile.PL> time.
294
295 =item F<README>
296
297 The F<README> file will be generated at dist generation time, inside the built
298 dist.  It will be generated using C<pod2text> on the main module.
299
300 If a F<README> file exists in the repo, it will be used directly instead of
301 generating the file.
302
303 =back
304
305 =head1 MAKE COMMMANDS
306
307 =head2 test
308
309 test will be adjusted to include F<xt/> tests by default.  This will only apply
310 for authors, not users installing from CPAN.
311
312 =head2 release
313
314 Releases the dist.  Before releasing, checks will be done on the dist using the
315 C<preflight> and C<releasetest> commands.
316
317 Releasing will generate a dist tarball and upload it to CPAN using cpan-upload.
318 It will also create a git tag for the release, and push the tag and branch.
319
320 =head2 FAKE_RELEASE
321
322 If release is run with FAKE_RELEASE=1 set, it will skip uploading to CPAN and
323 pushing to git.  A release commit will still be created and tagged locally.
324
325 =head2 preflight
326
327 Performs a number of checks on the files and repository, ensuring it is in a
328 sane state to do a release.  The checks are:
329
330 =over 4
331
332 =item * All version numbers match
333
334 =item * The F<MANIFEST> file is up to date
335
336 =item * The branch is correct
337
338 =item * There is no existing tag for the version
339
340 =item * There are no unmerged upstream changes
341
342 =item * There are no outstanding local changes
343
344 =item * There is an appropriate staged Changes heading
345
346 =item * cpan-upload is available
347
348 =back
349
350 =head2 releasetest
351
352 Test the dist preparing for a release.  This generates a dist dir and runs the
353 tests from inside it.  This ensures all appropriate files are included inside
354 the dist.  C<RELEASE_TESTING> will be set in the environment.
355
356 =head2 nextrelease
357
358 Adds an appropriate changelog heading for the release, and prompts to stage the
359 change.
360
361 =head2 bump
362
363 Bumps the version number.  This will try to preserve the length and format of
364 the version number.  The least significant digit will be incremented.
365
366 Optionally accepts a C<V> option to set the version to a specific value.
367
368 The version changes will automatically be committed.  Unstaged modifications to
369 the files will be left untouched.
370
371 =head2 bumpminor
372
373 Like bump, but increments the minor segment of the version.  This will treat
374 numeric versions as x.yyyzzz format, incrementing the yyy segment.
375
376 =head2 bumpmajor
377
378 Like bumpminor, but bumping the major segment.
379
380 =head2 refresh
381
382 Updates Distar and re-runs C<perl Makefile.PL>
383
384 =head1 SUPPORT
385
386 IRC: #web-simple on irc.perl.org
387
388 Git repository: L<git://git.shadowcat.co.uk/p5sagit/Distar>
389
390 Git browser: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=p5sagit/Distar.git;a=summary>
391
392 =head1 AUTHOR
393
394 mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
395
396 =head1 CONTRIBUTORS
397
398 haarg - Graham Knop (cpan:HAARG) <haarg@cpan.org>
399
400 ether = Karen Etheridge (cpan:ETHER) <ether@cpan.org>
401
402 frew - Arthur Axel "fREW" Schmidt (cpan:FREW) <frioux@gmail.com>
403
404 Mithaldu - Christian Walde (cpan:MITHALDU) <walde.christian@googlemail.com>
405
406 =head1 COPYRIGHT
407
408 Copyright (c) 2011-2015 the Distar L</AUTHOR> and L</CONTRIBUTORS>
409 as listed above.
410
411 =head1 LICENSE
412
413 This library is free software and may be distributed under the same terms
414 as perl itself. See L<http://dev.perl.org/licenses/>.
415
416 =cut