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