5 Porting/sync-with-cpan - Synchronize with CPAN distributions
10 perl Porting/sync-with-cpan <module>
12 where <module> is the name it appears in the C<%Modules> hash
13 of F<Porting/Maintainers.pl>
17 Script to help out with syncing cpan distros.
25 Fetches the package list from CPAN. Finds the current version of the given
30 Downloads the relevant tarball; unpacks the tarball. [1]
34 Clean out the old directory (C<git clean -dfx>)
38 Moves the old directory out of the way, moves the new directory in place.
42 Restores any F<.gitignore> file.
46 Removes files from C<@IGNORE> and C<EXCLUDED>
50 C<git add> any new files.
54 C<git rm> any files that are gone.
58 Remove the +x bit on files in F<t/>
62 Remove the +x bit on files that don't have it enabled in the current dir
66 Restore files mentioned in C<CUSTOMIZED>
70 Updates the contents of F<MANIFEST>
74 Runs a C<make> (assumes a configure has been run)
82 Runs tests for the package
86 Runs the porting tests
90 [1] If the C<--tarball> option is given, then CPAN is not consulted.
91 C<--tarball> should be the path to the tarball; the version is extracted
92 from the filename -- but can be overwritten by the C<--version> option.
100 When running C<make>, pass a C<< -jI<N> >> option to it.
110 Update F<Porting/Maintainers.pl>
114 Optional, run a full test suite
118 Handle complicated C<FILES>
122 This is an initial version; no attempt has been made yet to make this
123 portable. It shells out instead of trying to find a Perl solution.
124 In particular, it assumes git, perl, and make
138 use File::Basename qw( basename );
139 use File::Path qw( remove_tree );
141 use File::Spec::Functions qw( tmpdir rel2abs );
142 use Config qw( %Config );
146 use constant WIN32 => $^O eq 'MSWin32';
148 die "This does not look like a top level directory"
149 unless -d "cpan" && -d "Porting";
151 # Check that there's a Makefile, if needed; otherwise, we'll do most of our
152 # work only to fail when we try to run make, and the user will have to
153 # either unpick everything we've done, or do the rest manually.
154 die "Please run Configure before using $0\n"
155 if !WIN32 && !-f "Makefile";
162 require "./Porting/Maintainers.pl";
164 my $MAKE_LOG = 'make.log';
166 my %IGNORABLE = map {$_ => 1} @IGNORABLE;
168 my $tmpdir = tmpdir();
170 my $package = "02packages.details.txt";
171 my $package_url = "http://www.cpan.org/modules/$package";
172 my $package_file = "$tmpdir/$package"; # this is a cache
175 'podlators', # weird CUSTOMIZED section due to .PL files
181 my $err = shift and select STDERR;
182 print "Usage: $0 module [args] [cpan package]\n";
186 GetOptions ('tarball=s' => \my $tarball,
187 'version=s' => \my $version,
188 'jobs=i' => \my $make_jobs,
190 help => sub { usage 0; },
191 ) or die "Failed to parse arguments";
193 usage 1 unless @ARGV == 1 || @ARGV == 2;
197 find( { no_chdir => 1, wanted => sub {
198 my $file= $File::Find::name;
199 return unless -f $file;
205 # Equivalent of `chmod a-x`
208 my $mode = (stat $filename)[2] & 0777;
209 if ($mode & 0111) { # exec-bit set
210 chmod $mode & 0666, $filename;
214 # Equivalent of `chmod +w`
217 my $mode = (stat $filename)[2] & 0777;
218 if (!($mode & 0222)) { # not writable
219 chmod $mode | (0222 & ~umask), $filename;
225 unshift @args, "-j$make_jobs" if defined $make_jobs;
228 system "$Config{make} @args> ..\\$MAKE_LOG 2>&1"
229 and die "Running make failed, see $MAKE_LOG";
232 system "$Config{make} @args> $MAKE_LOG 2>&1"
233 and die "Running make failed, see $MAKE_LOG";
237 my ($module) = shift;
239 my $info = $Modules{$module};
241 # Maybe the user said "Test-Simple" instead of "Test::Simple", or
242 # "IO::Compress" instead of "IO-Compress". See if we can fix it up.
244 s/-/::/g or s/::/-/g for $guess;
245 $info = $Modules{$guess} or die <<"EOF";
246 Cannot find module $module.
247 The available options are listed in the %Modules hash in Porting/Maintainers.pl
249 say "Guessing you meant $guess instead of $module";
253 if ($info->{CUSTOMIZED}) {
255 $module has a CUSTOMIZED entry in Porting/Maintainers.pl.
257 This program's behaviour is to copy every CUSTOMIZED file into the version
258 of the module being imported. But that might not be the right thing: in some
259 cases, the new CPAN version will supersede whatever changes had previously
260 been made in blead, so it would be better to import the new CPAN files.
262 If you've checked that the CUSTOMIZED versions are still correct, you can
263 proceed now. Otherwise, you should abort and investigate the situation. If
264 the blead customizations are no longer needed, delete the CUSTOMIZED entry
265 for $module in Porting/Maintainers.pl (and you'll also need to regenerate
266 t/porting/customized.dat in that case; see t/porting/customized.t).
269 print "Hit return to continue; ^C to abort "; <STDIN>;
272 my $cpan_mod = @ARGV ? shift : $module;
274 my $distribution = $$info {DISTRIBUTION};
276 my @files = glob $$info {FILES};
277 if (!-d $files [0] || grep { $_ eq $module } @problematic) {
278 say "This looks like a setup $0 cannot handle (yet)";
280 say "Will not continue without a --force option";
283 say "--force is in effect, so we'll soldier on. Wish me luck!";
287 my $orig_pwd = cwd();
291 my $pkg_dir = $files[0];
294 my ($old_version) = $distribution =~ /-([0-9.]+(?:-TRIAL[0-9]*)?)\.tar\.gz/;
296 my $o_module = $module;
297 if ($cpan_mod =~ /-/ && $cpan_mod !~ /::/) {
298 $cpan_mod =~ s/-/::/g;
302 my ($url, $saveas) = @_;
305 require IO::Socket::SSL;
308 my $http = HTTP::Tiny->new();
309 $ht_res = $http->mirror( $url => $saveas );
312 # Try harder to download the file
313 # Some system do not have wget. Fall back to curl if we do not
314 # have it. On Windows, `which wget` is not going to work, so
315 # just use wget, as this script has always done.
316 WIN32 || -x substr(`which wget`, 0, -1)
317 ? system wget => $url, '-qO', $saveas
318 : system curl => $url, '-sSo', $saveas;
320 # We were able to use HTTP::Tiny and it didn't have fatal errors,
321 # but we failed the request
322 if ( $ht_res && ! $ht_res->{'success'} ) {
323 die "Cannot retrieve file: $url\n" .
324 sprintf "Status: %s\nReason: %s\nContent: %s\n",
325 map $_ // '(unavailable)', @{$ht_res}{qw< status reason content >};
330 # Find the information from CPAN.
334 if (defined $tarball) {
335 $tarball = rel2abs( $tarball, $orig_pwd ) ;
336 die "Tarball $tarball does not exist\n" if !-e $tarball;
337 die "Tarball $tarball is not a plain file\n" if !-f _;
338 $new_file = $tarball;
339 $new_version = $version // ($new_file =~ /-([0-9._]+(?:-TRIAL[0-9]*)?)\.tar\.gz/) [0];
340 die "Blead and that tarball both have version $new_version of $module\n"
341 if $new_version eq $old_version;
347 unless (-f $package_file && -M $package_file < 1) {
348 wget $package_url, $package_file;
351 open my $fh, '<', $package_file;
352 (my $new_line) = grep {/^$cpan_mod/} <$fh> # Yes, this needs a lot of memory
353 or die "Cannot find $cpan_mod on CPAN\n";
354 (undef, $new_version, my $new_path) = split ' ', $new_line;
355 if (defined $version) {
356 $new_path =~ s/-$new_version\./-$version\./;
357 $new_version = $version;
359 $new_file = (split '/', $new_path) [-1];
361 die "The latest version of $module is $new_version, but blead already has it\n"
362 if $new_version eq $old_version;
364 my $url = "https://cpan.metacpan.org/authors/id/$new_path";
367 # Fetch the new distro
369 wget $url, $new_file;
372 my $old_dir = "$pkg_dir-$old_version";
374 say "Cleaning out old directory";
375 system git => 'clean', '-dfxq', $pkg_dir;
377 say "Unpacking $new_file";
378 Archive::Tar->extract_archive( $new_file );
380 (my $new_dir = basename($new_file)) =~ s/\.tar\.gz//;
381 # ensure 'make' will update all files
383 for my $file (find_type_f($new_dir)) {
384 make_writable($file); # for convenience if the user later edits it
388 say "Renaming directories";
389 rename $pkg_dir => $old_dir;
391 say "Creating new package directory";
394 say "Populating new package directory";
395 my $map = $$info {MAP};
398 if ($$info {EXCLUDED}) {
399 foreach my $entry (@{$$info {EXCLUDED}}) {
400 if (ref $entry) {push @EXCLUDED_QR => $entry}
401 else {$EXCLUDED_QQ {$entry} = 1}
405 FILE: for my $file ( find_type_f( $new_dir )) {
406 my $old_file = $file;
407 $file =~ s{^$new_dir/}{};
409 next if $EXCLUDED_QQ{$file};
410 for my $qr (@EXCLUDED_QR) {
411 next FILE if $file =~ $qr;
415 for my $key ( sort { length $b <=> length $a } keys %$map ) {
416 my $val = $map->{$key};
417 last if $file =~ s/^$key/$val/;
421 $file = $files[0] . '/' . $file;
424 if ( $file =~ m{^cpan/} ) {
425 $file =~ s{^cpan/}{};
428 $file = '../' . $file;
432 my @parts = split '/', $file;
434 for my $part (@parts) {
435 $prefix .= '/' if $prefix;
437 mkdir $prefix unless -d $prefix;
440 rename $old_file => $file;
442 remove_tree( $new_dir );
444 if (-f "$old_dir/.gitignore") {
445 say "Restoring .gitignore";
446 system git => 'checkout', "$pkg_dir/.gitignore";
449 my @new_files = find_type_f( $pkg_dir );
450 @new_files = grep {$_ ne $pkg_dir} @new_files;
451 s!^[^/]+/!! for @new_files;
452 my %new_files = map {$_ => 1} @new_files;
454 my @old_files = find_type_f( $old_dir );
455 @old_files = grep {$_ ne $old_dir} @old_files;
456 s!^[^/]+/!! for @old_files;
457 my %old_files = map {$_ => 1} @old_files;
463 foreach my $file (@new_files) {
464 next if -d "$pkg_dir/$file"; # Ignore directories.
465 next if $old_files {$file}; # It's already there.
466 if ($IGNORABLE {$file}) {
467 push @delete => $file;
470 push @commit => $file;
472 foreach my $file (@old_files) {
473 next if -d "$old_dir/$file";
474 next if $new_files {$file};
479 # Find all files with an exec bit
481 my @exec = find_type_f( $pkg_dir );
483 foreach my $file (@exec) {
485 $file =~ s!^[^/]+/!!;
486 if ($file =~ m!^t/!) {
487 push @de_exec => $file;
490 # Check to see if the file exists; if it doesn't and doesn't have
491 # the exec bit, remove it.
492 if ($old_files {$file}) {
493 unless (-x "$old_dir/$file") {
494 push @de_exec => $file;
500 # No need to change the +x bit on files that will be deleted.
502 if (@de_exec && @delete) {
503 my %delete = map {+"$pkg_dir/$_" => 1} @delete;
504 @de_exec = grep {!$delete {$_}} @de_exec;
508 # Mustn't change the +x bit on files that are whitelisted
511 my %permitted = map { (my $x = $_) =~ tr/\n//d; $x => 1 } grep !/^#/,
512 do { local @ARGV = '../Porting/exec-bit.txt'; <> };
513 @de_exec = grep !$permitted{"cpan/$pkg_dir/$_"}, @de_exec;
516 say "unlink $pkg_dir/$_" for @delete;
517 say "git add $pkg_dir/$_" for @commit;
518 say "git rm -f $pkg_dir/$_" for @gone;
519 say "chmod a-x $pkg_dir/$_" for @de_exec;
521 print "Hit return to continue; ^C to abort "; <STDIN>;
523 unlink "$pkg_dir/$_" for @delete;
524 system git => 'add', "$pkg_dir/$_" for @commit;
525 system git => 'rm', '-f', "$pkg_dir/$_" for @gone;
526 de_exec( "$pkg_dir/$_" ) for @de_exec;
529 # Restore anything that is customized.
530 # We don't really care whether we've deleted the file - since we
531 # do a git restore, it's going to be resurrected if necessary.
533 if ($$info {CUSTOMIZED}) {
534 say "Restoring customized files";
535 foreach my $file (@{$$info {CUSTOMIZED}}) {
536 system git => "checkout", "$pkg_dir/$file";
541 if (@commit || @gone) {
542 say "Fixing MANIFEST";
543 my $MANIFEST = "MANIFEST";
544 my $MANIFEST_NEW = "$MANIFEST.new";
546 open my $orig, "<", $MANIFEST
547 or die "Failed to open $MANIFEST for reading: $!\n";
548 open my $new, ">", $MANIFEST_NEW
549 or die "Failed to open $MANIFEST_NEW for writing: $!\n";
550 my %gone = map +("cpan/$pkg_dir/$_" => 1), @gone;
551 while (my $line = <$orig>) {
552 my ($file) = $line =~ /^(\S+)/
553 or die "Can't parse MANIFEST line: $line";
554 print $new $line if !$gone{$file};
557 say $new "cpan/$pkg_dir/$_" for @commit;
559 close $new or die "Can't close $MANIFEST: $!\n";
561 system $^X => "Porting/manisort", '--quiet', "--output=$MANIFEST", $MANIFEST_NEW;
563 or die "Can't delete temporary $MANIFEST_NEW: $!\n";
567 print "Running a make and saving its output to $MAKE_LOG ... ";
568 # Prepare for running (selected) tests
572 # The build system installs code from CPAN dists into the lib/ directory,
573 # creating directories as needed. This means that the cleaning-related rules
574 # in the Makefile need to know which directories to clean up. The Makefile
575 # is generated by Configure from Makefile.SH, so *that* file needs the list
576 # of directories. regen/lib_cleanup.pl is capable of automatically updating
577 # the contents of Makefile.SH (and win32/Makefile, which needs similar but
578 # not identical lists of directories), so we can just run that (using the
579 # newly-built Perl, as is done with the regen programs run by "make regen").
581 # We do this if any files at all have been added or deleted, regardless of
582 # whether those changes result in any directories being added or deleted,
583 # because the alternative would be to replicate the regen/lib_cleanup.pl
584 # logic here. That's fine, because regen/lib_cleanup.pl is idempotent if run
586 if (@commit || @gone) {
587 say "Running regen/lib_cleanup.pl to handle potential added/deleted dirs";
588 my $exe_dir = WIN32 ? ".\\" : './';
589 system "${exe_dir}perl$Config{_exe}", "-Ilib", "regen/lib_cleanup.pl"
590 and die "regen/lib_cleanup.pl failed\n";
594 # Must clean up, or else t/porting/FindExt.t will fail.
595 # Note that we can always retrieve the original directory with a git checkout.
597 print "About to clean up; hit return or abort (^C) "; <STDIN>;
599 remove_tree( "cpan/$old_dir" );
600 unlink "cpan/$new_file" unless $tarball;
603 # Run the tests. First the test belonging to the module, followed by the
607 say "Running module tests";
608 my @test_files = grep { /\.t$/ } find_type_f( "../cpan/$pkg_dir" );
609 my $exe_dir = WIN32 ? "..\\" : './';
610 my $output = `${exe_dir}perl$Config{_exe} TEST @test_files`;
611 unless ($output =~ /All tests successful/) {
616 print "Running tests in t/porting ";
617 my @tests = glob 'porting/*.t';
620 foreach my $t (@tests) {
621 my @not = grep {!/# TODO/ }
623 `${exe_dir}perl -I../lib -I.. $t`;
624 print @not ? '!' : '.';
625 push @failed => $t if @not;
628 say "Failed tests: @failed" if @failed;
633 open my $Maintainers_pl, '<', 'Porting/Maintainers.pl';
634 open my $new_Maintainers_pl, '>', 'Maintainers.pl';
638 while (<$Maintainers_pl>) {
640 if ($in_mod_section) {
641 if (/DISTRIBUTION/) {
642 if (s/\Q$old_version/$new_version/) {
657 print $new_Maintainers_pl $_;
661 say "Successfully updated Maintainers.pl";
662 unlink 'Porting/Maintainers.pl';
663 rename 'Maintainers.pl' => 'Porting/Maintainers.pl';
664 chmod 0755 => 'Porting/Maintainers.pl';
667 say "Could not update Porting/Maintainers.pl.";
668 say "Make sure you update this by hand before committing.";
673 =======================================================================
675 $o_module is now at version $new_version
676 Next, you should run a "make test".
678 Hopefully that will complete successfully, but if not, you can make any
679 changes you need to get the tests to pass. Don't forget that you'll need
680 a "CUSTOMIZED" entry in Porting/Maintainers.pl if you change any of the
681 files under cpan/$pkg_dir.
683 Once all tests pass, you can "git add -u" and "git commit" the changes.