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