This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
PATCH: [perl #121292] wrong perlunicode BOM claims
[perl5.git] / Porting / sync-with-cpan
... / ...
CommitLineData
1#!/usr/bin/env perl
2
3=head1 NAME
4
5Porting/sync-with-cpan - Synchronize with CPAN distributions
6
7=head1 SYNOPSIS
8
9 perl Porting/sync-with-cpan <module>
10
11where <module> is the name it appears in the C<%Modules> hash
12of F<Porting/Maintainers.pl>
13
14=head1 DESCRIPTION
15
16Script to help out with syncing cpan distros.
17
18Does the following:
19
20=over 4
21
22=item *
23
24Fetches the package list from CPAN. Finds the current version of the given
25package. [1]
26
27=item *
28
29Downloads the relevant tarball; unpacks the tarball. [1]
30
31=item *
32
33Clean out the old directory (C<git clean -dfx>)
34
35=item *
36
37Moves the old directory out of the way, moves the new directory in place.
38
39=item *
40
41Restores any F<.gitignore> file.
42
43=item *
44
45Removes files from C<@IGNORE> and C<EXCLUDED>
46
47=item *
48
49C<git add> any new files.
50
51=item *
52
53C<git rm> any files that are gone.
54
55=item *
56
57Remove the +x bit on files in F<t/>
58
59=item *
60
61Remove the +x bit on files that don't have it enabled in the current dir
62
63=item *
64
65Restore files mentioned in C<CUSTOMIZED>
66
67=item *
68
69Updates the contents of F<MANIFEST>
70
71=item *
72
73Runs a C<make> (assumes a configure has been run)
74
75=item *
76
77Cleans up
78
79=item *
80
81Runs tests for the package
82
83=item *
84
85Runs the porting tests
86
87=back
88
89[1] If the C<--tarball> option is given, then CPAN is not consulted.
90C<--tarball> should be the path to the tarball; the version is extracted
91from the filename -- but can be overwritten by the C<--version> option.
92
93=head1 OPTIONS
94
95=over 4
96
97=item C<--jobs> I<N>
98
99When running C<make>, pass a C<< -jI<N> >> option to it.
100
101=back
102
103=head1 TODO
104
105=over 4
106
107=item *
108
109Update F<Porting/Maintainers.pl>
110
111=item *
112
113Optional, run a full test suite
114
115=item *
116
117Handle complicated C<FILES>
118
119=back
120
121This is an initial version; no attempt has been made yet to make this
122portable. It shells out instead of trying to find a Perl solution.
123In particular, it assumes git, perl, and make
124to be available.
125
126=cut
127
128
129package Maintainers;
130
131use 5.010;
132
133use strict;
134use warnings;
135use Getopt::Long;
136use Archive::Tar;
137use File::Basename qw( basename );
138use File::Path qw( remove_tree );
139use File::Find;
140use File::Spec::Functions qw( tmpdir );
141use Config qw( %Config );
142
143$| = 1;
144
145use constant WIN32 => $^O eq 'MSWin32';
146
147die "This does not look like a top level directory"
148 unless -d "cpan" && -d "Porting";
149
150# Check that there's a Makefile, if needed; otherwise, we'll do most of our
151# work only to fail when we try to run make, and the user will have to
152# either unpick everything we've done, or do the rest manually.
153die "Please run Configure before using $0\n"
154 if !WIN32 && !-f "Makefile";
155
156our @IGNORABLE;
157our %Modules;
158
159use autodie;
160
161require "./Porting/Maintainers.pl";
162
163my $MAKE_LOG = 'make.log';
164
165my %IGNORABLE = map {$_ => 1} @IGNORABLE;
166
167my $tmpdir = tmpdir();
168
169my $package = "02packages.details.txt";
170my $package_url = "http://www.cpan.org/modules/$package";
171my $package_file = "$tmpdir/$package"; # this is a cache
172
173my @problematic = (
174 'podlators', # weird CUSTOMIZED section due to .PL files
175);
176
177
178sub usage
179{
180 my $err = shift and select STDERR;
181 print "Usage: $0 module [args] [cpan package]\n";
182 exit $err;
183}
184
185GetOptions ('tarball=s' => \my $tarball,
186 'version=s' => \my $version,
187 'jobs=i' => \my $make_jobs,
188 force => \my $force,
189 help => sub { usage 0; },
190 ) or die "Failed to parse arguments";
191
192usage 1 unless @ARGV == 1 || @ARGV == 2;
193
194sub find_type_f {
195 my @res;
196 find( { no_chdir => 1, wanted => sub {
197 my $file= $File::Find::name;
198 return unless -f $file;
199 push @res, $file
200 }}, @_ );
201 @res
202};
203
204# Equivalent of `chmod a-x`
205sub de_exec {
206 my ($filename) = @_;
207 my $mode = (stat $filename)[2] & 0777;
208 if ($mode & 0111) { # exec-bit set
209 chmod $mode & 0666, $filename;
210 }
211}
212
213# Equivalent of `chmod +w`
214sub make_writable {
215 my ($filename) = @_;
216 my $mode = (stat $filename)[2] & 0777;
217 if (!($mode & 0222)) { # not writable
218 chmod $mode | (0222 & ~umask), $filename;
219 }
220}
221
222sub make {
223 my @args= @_;
224 unshift @args, "-j$make_jobs" if defined $make_jobs;
225 if (WIN32) {
226 chdir "Win32";
227 system "$Config{make} @args> ..\\$MAKE_LOG 2>&1"
228 and die "Running make failed, see $MAKE_LOG";
229 chdir '..';
230 } else {
231 system "$Config{make} @args> $MAKE_LOG 2>&1"
232 and die "Running make failed, see $MAKE_LOG";
233 };
234};
235
236my ($module) = shift;
237
238my $info = $Modules{$module};
239if (!$info) {
240 # Maybe the user said "Test-Simple" instead of "Test::Simple", or
241 # "IO::Compress" instead of "IO-Compress". See if we can fix it up.
242 my $guess = $module;
243 s/-/::/g or s/::/-/g for $guess;
244 $info = $Modules{$guess} or die <<"EOF";
245Cannot find module $module.
246The available options are listed in the %Modules hash in Porting/Maintainers.pl
247EOF
248 say "Guessing you meant $guess instead of $module";
249 $module = $guess;
250}
251
252if ($info->{CUSTOMIZED}) {
253 print <<"EOF";
254$module has a CUSTOMIZED entry in Porting/Maintainers.pl.
255
256This program's behaviour is to copy every CUSTOMIZED file into the version
257of the module being imported. But that might not be the right thing: in some
258cases, the new CPAN version will supersede whatever changes had previously
259been made in blead, so it would be better to import the new CPAN files.
260
261If you've checked that the CUSTOMIZED versions are still correct, you can
262proceed now. Otherwise, you should abort and investigate the situation. If
263the blead customizations are no longer needed, delete the CUSTOMIZED entry
264for $module in Porting/Maintainers.pl (and you'll also need to regenerate
265t/porting/customized.dat in that case; see t/porting/customized.t).
266
267EOF
268 print "Hit return to continue; ^C to abort "; <STDIN>;
269}
270
271my $cpan_mod = @ARGV ? shift : $module;
272
273my $distribution = $$info {DISTRIBUTION};
274
275my @files = glob $$info {FILES};
276if (!-d $files [0] || grep { $_ eq $module } @problematic) {
277 say "This looks like a setup $0 cannot handle (yet)";
278 unless ($force) {
279 say "Will not continue without a --force option";
280 exit 1;
281 }
282 say "--force is in effect, so we'll soldier on. Wish me luck!";
283}
284
285
286chdir "cpan";
287
288my $pkg_dir = $files[0];
289 $pkg_dir =~ s!.*/!!;
290
291my ($old_version) = $distribution =~ /-([0-9.]+(?:-TRIAL[0-9]*)?)\.tar\.gz/;
292
293my $o_module = $module;
294if ($cpan_mod =~ /-/ && $cpan_mod !~ /::/) {
295 $cpan_mod =~ s/-/::/g;
296}
297
298#
299# Find the information from CPAN.
300#
301my $new_file;
302my $new_version;
303if (defined $tarball) {
304 die "Tarball $tarball does not exist\n" if !-e $tarball;
305 die "Tarball $tarball is not a plain file\n" if !-f _;
306 $new_file = $tarball;
307 $new_version = $version // ($new_file =~ /-([0-9._]+(?:-TRIAL[0-9]*)?)\.tar\.gz/) [0];
308 die "Blead and that tarball both have version $new_version of $module\n"
309 if $new_version eq $old_version;
310}
311else {
312 #
313 # Poor man's cache
314 #
315 unless (-f $package_file && -M $package_file < 1) {
316 eval {
317 require HTTP::Tiny;
318 my $http= HTTP::Tiny->new();
319 $http->mirror( $package_url => $package_file );
320 1
321 } or system wget => $package_url, '-qO', $package_file;
322 }
323
324 open my $fh, '<', $package_file;
325 (my $new_line) = grep {/^$cpan_mod/} <$fh> # Yes, this needs a lot of memory
326 or die "Cannot find $cpan_mod on CPAN\n";
327 (undef, $new_version, my $new_path) = split ' ', $new_line;
328 if (defined $version) {
329 $new_path =~ s/-$new_version\./-$version\./;
330 $new_version = $version;
331 }
332 $new_file = (split '/', $new_path) [-1];
333
334 die "The latest version of $module is $new_version, but blead already has it\n"
335 if $new_version eq $old_version;
336
337 my $url = "http://search.cpan.org/CPAN/authors/id/$new_path";
338 say "Fetching $url";
339 #
340 # Fetch the new distro
341 #
342 eval {
343 require HTTP::Tiny;
344 my $http= HTTP::Tiny->new();
345 $http->mirror( $url => $new_file );
346 1
347 } or system wget => $url, '-qO', $new_file;
348}
349
350my $old_dir = "$pkg_dir-$old_version";
351
352say "Cleaning out old directory";
353system git => 'clean', '-dfxq', $pkg_dir;
354
355say "Unpacking $new_file";
356Archive::Tar->extract_archive( $new_file );
357
358(my $new_dir = basename($new_file)) =~ s/\.tar\.gz//;
359# ensure 'make' will update all files
360my $t= time;
361for my $file (find_type_f($new_dir)) {
362 make_writable($file); # for convenience if the user later edits it
363 utime($t,$t,$file);
364};
365
366say "Renaming directories";
367rename $pkg_dir => $old_dir;
368
369say "Creating new package directory";
370mkdir $pkg_dir;
371
372say "Populating new package directory";
373my $map = $$info {MAP};
374my @EXCLUDED_QR;
375my %EXCLUDED_QQ;
376if ($$info {EXCLUDED}) {
377 foreach my $entry (@{$$info {EXCLUDED}}) {
378 if (ref $entry) {push @EXCLUDED_QR => $entry}
379 else {$EXCLUDED_QQ {$entry} = 1}
380 }
381}
382
383FILE: for my $file ( find_type_f( $new_dir )) {
384 my $old_file = $file;
385 $file =~ s{^$new_dir/}{};
386
387 next if $EXCLUDED_QQ{$file};
388 for my $qr (@EXCLUDED_QR) {
389 next FILE if $file =~ $qr;
390 }
391
392 if ( $map ) {
393 for my $key ( sort { length $b <=> length $a } keys %$map ) {
394 my $val = $map->{$key};
395 last if $file =~ s/^$key/$val/;
396 }
397 }
398 else {
399 $file = $files[0] . '/' . $file;
400 }
401
402 if ( $file =~ m{^cpan/} ) {
403 $file =~ s{^cpan/}{};
404 }
405 else {
406 $file = '../' . $file;
407 }
408
409 my $prefix = '';
410 my @parts = split '/', $file;
411 pop @parts;
412 for my $part (@parts) {
413 $prefix .= '/' if $prefix;
414 $prefix .= $part;
415 mkdir $prefix unless -d $prefix;
416 }
417
418 rename $old_file => $file;
419}
420remove_tree( $new_dir );
421
422if (-f "$old_dir/.gitignore") {
423 say "Restoring .gitignore";
424 system git => 'checkout', "$pkg_dir/.gitignore";
425}
426
427my @new_files = find_type_f( $pkg_dir );
428@new_files = grep {$_ ne $pkg_dir} @new_files;
429s!^[^/]+/!! for @new_files;
430my %new_files = map {$_ => 1} @new_files;
431
432my @old_files = find_type_f( $old_dir );
433@old_files = grep {$_ ne $old_dir} @old_files;
434s!^[^/]+/!! for @old_files;
435my %old_files = map {$_ => 1} @old_files;
436
437my @delete;
438my @commit;
439my @gone;
440FILE:
441foreach my $file (@new_files) {
442 next if -d "$pkg_dir/$file"; # Ignore directories.
443 next if $old_files {$file}; # It's already there.
444 if ($IGNORABLE {$file}) {
445 push @delete => $file;
446 next;
447 }
448 push @commit => $file;
449}
450foreach my $file (@old_files) {
451 next if -d "$old_dir/$file";
452 next if $new_files {$file};
453 push @gone => $file;
454}
455
456#
457# Find all files with an exec bit
458#
459my @exec = find_type_f( $pkg_dir );
460my @de_exec;
461foreach my $file (@exec) {
462 # Remove leading dir
463 $file =~ s!^[^/]+/!!;
464 if ($file =~ m!^t/!) {
465 push @de_exec => $file;
466 next;
467 }
468 # Check to see if the file exists; if it doesn't and doesn't have
469 # the exec bit, remove it.
470 if ($old_files {$file}) {
471 unless (-x "$old_dir/$file") {
472 push @de_exec => $file;
473 }
474 }
475}
476
477#
478# No need to change the +x bit on files that will be deleted.
479#
480if (@de_exec && @delete) {
481 my %delete = map {+"$pkg_dir/$_" => 1} @delete;
482 @de_exec = grep {!$delete {$_}} @de_exec;
483}
484
485#
486# Mustn't change the +x bit on files that are whitelisted
487#
488if (@de_exec) {
489 my %permitted = map +(tr/\n//dr => 1), grep !/^#/,
490 do { local @ARGV = '../Porting/exec-bit.txt'; <> };
491 @de_exec = grep !$permitted{"cpan/$pkg_dir/$_"}, @de_exec;
492}
493
494say "unlink $pkg_dir/$_" for @delete;
495say "git add $pkg_dir/$_" for @commit;
496say "git rm -f $pkg_dir/$_" for @gone;
497say "chmod a-x $pkg_dir/$_" for @de_exec;
498
499print "Hit return to continue; ^C to abort "; <STDIN>;
500
501unlink "$pkg_dir/$_" for @delete;
502system git => 'add', "$pkg_dir/$_" for @commit;
503system git => 'rm', '-f', "$pkg_dir/$_" for @gone;
504de_exec( "$pkg_dir/$_" ) for @de_exec;
505
506#
507# Restore anything that is customized.
508# We don't really care whether we've deleted the file - since we
509# do a git restore, it's going to be resurrected if necessary.
510#
511if ($$info {CUSTOMIZED}) {
512 say "Restoring customized files";
513 foreach my $file (@{$$info {CUSTOMIZED}}) {
514 system git => "checkout", "$pkg_dir/$file";
515 }
516}
517
518chdir "..";
519if (@commit || @gone) {
520 say "Fixing MANIFEST";
521 my $MANIFEST = "MANIFEST";
522 my $MANIFEST_NEW = "$MANIFEST.new";
523
524 open my $orig, "<", $MANIFEST
525 or die "Failed to open $MANIFEST for reading: $!\n";
526 open my $new, ">", $MANIFEST_NEW
527 or die "Failed to open $MANIFEST_NEW for writing: $!\n";
528 my %gone = map +("cpan/$pkg_dir/$_" => 1), @gone;
529 while (my $line = <$orig>) {
530 my ($file) = $line =~ /^(\S+)/
531 or die "Can't parse MANIFEST line: $line";
532 print $new $line if !$gone{$file};
533 }
534
535 say $new "cpan/$pkg_dir/$_" for @commit;
536
537 close $new or die "Can't close $MANIFEST: $!\n";
538
539 system $^X => "Porting/manisort", '--quiet', "--output=$MANIFEST", $MANIFEST_NEW;
540 unlink $MANIFEST_NEW
541 or die "Can't delete temporary $MANIFEST_NEW: $!\n";
542}
543
544
545print "Running a make and saving its output to $MAKE_LOG ... ";
546# Prepare for running (selected) tests
547make 'test-prep';
548print "done\n";
549
550# The build system installs code from CPAN dists into the lib/ directory,
551# creating directories as needed. This means that the cleaning-related rules
552# in the Makefile need to know which directories to clean up. The Makefile
553# is generated by Configure from Makefile.SH, so *that* file needs the list
554# of directories. regen/lib_cleanup.pl is capable of automatically updating
555# the contents of Makefile.SH (and win32/Makefile, which needs similar but
556# not identical lists of directories), so we can just run that (using the
557# newly-built Perl, as is done with the regen programs run by "make regen").
558#
559# We do this if any files at all have been added or deleted, regardless of
560# whether those changes result in any directories being added or deleted,
561# because the alternative would be to replicate the regen/lib_cleanup.pl
562# logic here. That's fine, because regen/lib_cleanup.pl is idempotent if run
563# repeatedly.
564if (@commit || @gone) {
565 say "Running regen/lib_cleanup.pl to handle potential added/deleted dirs";
566 my $exe_dir = WIN32 ? ".\\" : './';
567 system "${exe_dir}perl$Config{_exe}", "-Ilib", "regen/lib_cleanup.pl"
568 and die "regen/lib_cleanup.pl failed\n";
569}
570
571#
572# Must clean up, or else t/porting/FindExt.t will fail.
573# Note that we can always retrieve the original directory with a git checkout.
574#
575print "About to clean up; hit return or abort (^C) "; <STDIN>;
576
577remove_tree( "cpan/$old_dir" );
578unlink "cpan/$new_file" unless $tarball;
579
580#
581# Run the tests. First the test belonging to the module, followed by the
582# the tests in t/porting
583#
584chdir "t";
585say "Running module tests";
586my @test_files = grep { /\.t$/ } find_type_f( "../cpan/$pkg_dir" );
587my $exe_dir = WIN32 ? "..\\" : './';
588my $output = `${exe_dir}perl$Config{_exe} TEST @test_files`;
589unless ($output =~ /All tests successful/) {
590 say $output;
591 exit 1;
592}
593
594print "Running tests in t/porting ";
595my @tests = glob 'porting/*.t';
596chomp @tests;
597my @failed;
598foreach my $t (@tests) {
599 my @not = grep {!/# TODO/ }
600 grep { /^not/ }
601 `${exe_dir}perl -I../lib -I.. $t`;
602 print @not ? '!' : '.';
603 push @failed => $t if @not;
604}
605print "\n";
606say "Failed tests: @failed" if @failed;
607
608
609chdir '..';
610
611open my $Maintainers_pl, '<', 'Porting/Maintainers.pl';
612open my $new_Maintainers_pl, '>', 'Maintainers.pl';
613
614my $found;
615my $in_mod_section;
616while (<$Maintainers_pl>) {
617 if (!$found) {
618 if ($in_mod_section) {
619 if (/DISTRIBUTION/) {
620 if (s/\Q$old_version/$new_version/) {
621 $found = 1;
622 }
623 }
624
625 if (/^ \}/) {
626 $in_mod_section = 0;
627 }
628 }
629
630 if (/\Q$module/) {
631 $in_mod_section = 1;
632 }
633 }
634
635 print $new_Maintainers_pl $_;
636}
637
638if ($found) {
639 say "Successfully updated Maintainers.pl";
640 unlink 'Porting/Maintainers.pl';
641 rename 'Maintainers.pl' => 'Porting/Maintainers.pl';
642 chmod 0755 => 'Porting/Maintainers.pl';
643}
644else {
645 say "Could not update Porting/Maintainers.pl.";
646 say "Make sure you update this by hand before committing.";
647}
648
649print <<"EOF";
650
651=======================================================================
652
653$o_module is now at version $new_version
654Next, you should run a "make test".
655
656Hopefully that will complete successfully, but if not, you can make any
657changes you need to get the tests to pass. Don't forget that you'll need
658a "CUSTOMIZED" entry in Porting/Maintainers.pl if you change any of the
659files under cpan/$pkg_dir.
660
661Once all tests pass, you can "git add -u" and "git commit" the changes.
662
663EOF
664
665__END__