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