5 Porting/sync-with-cpan - Synchronize with CPAN distributions
10 perl Porting/sync-with-cpan <module>
12 where C<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-or-dist> [args]\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 $distribution = $$info {DISTRIBUTION};
274 my @files = glob $$info {FILES};
275 if (!-d $files [0] || grep { $_ eq $module } @problematic) {
276 say "This looks like a setup $0 cannot handle (yet)";
278 say "Will not continue without a --force option";
281 say "--force is in effect, so we'll soldier on. Wish me luck!";
285 my $orig_pwd = cwd();
289 my $pkg_dir = $files[0];
292 my ($old_version) = $distribution =~ /-([0-9.]+(?:-TRIAL[0-9]*)?)\.tar\.gz/;
295 my ($url, $saveas) = @_;
298 require IO::Socket::SSL;
301 my $http = HTTP::Tiny->new();
302 $ht_res = $http->mirror( $url => $saveas );
305 # Try harder to download the file
306 # Some system do not have wget. Fall back to curl if we do not
307 # have it. On Windows, `which wget` is not going to work, so
308 # just use wget, as this script has always done.
309 WIN32 || -x substr(`which wget`, 0, -1)
310 ? system wget => $url, '-qO', $saveas
311 : system curl => $url, '-sSo', $saveas;
313 # We were able to use HTTP::Tiny and it didn't have fatal errors,
314 # but we failed the request
315 if ( $ht_res && ! $ht_res->{'success'} ) {
316 die "Cannot retrieve file: $url\n" .
317 sprintf "Status: %s\nReason: %s\nContent: %s\n",
318 map $_ // '(unavailable)', @{$ht_res}{qw< status reason content >};
323 # Find the information from CPAN.
327 if (defined $tarball) {
328 $tarball = rel2abs( $tarball, $orig_pwd ) ;
329 die "Tarball $tarball does not exist\n" if !-e $tarball;
330 die "Tarball $tarball is not a plain file\n" if !-f _;
331 $new_file = $tarball;
332 $new_version = $version // ($new_file =~ /-([0-9._]+(?:-TRIAL[0-9]*)?)\.tar\.gz/) [0];
333 die "Blead and that tarball both have version $new_version of $module\n"
334 if $new_version eq $old_version;
340 unless (-f $package_file && -M $package_file < 1) {
341 wget $package_url, $package_file;
344 my $cpan_mod = $info->{MAIN_MODULE} // $module;
345 open my $fh, '<', $package_file;
346 (my $new_line) = grep {/^\Q$cpan_mod\E /} <$fh> # Yes, this needs a lot of memory
347 or die "Cannot find $cpan_mod on CPAN\n";
348 (undef, $new_version, my $new_path) = split ' ', $new_line;
349 if (defined $version) {
350 $new_path =~ s/-$new_version\./-$version\./;
351 $new_version = $version;
353 $new_file = (split '/', $new_path) [-1];
355 die "The latest version of $module is $new_version, but blead already has it\n"
356 if $new_version eq $old_version;
358 my $url = "https://cpan.metacpan.org/authors/id/$new_path";
361 # Fetch the new distro
363 wget $url, $new_file;
366 my $old_dir = "$pkg_dir-$old_version";
368 say "Cleaning out old directory";
369 system git => 'clean', '-dfxq', $pkg_dir;
371 say "Unpacking $new_file";
372 Archive::Tar->extract_archive( $new_file );
374 (my $new_dir = basename($new_file)) =~ s/\.tar\.gz//;
375 # ensure 'make' will update all files
377 for my $file (find_type_f($new_dir)) {
378 make_writable($file); # for convenience if the user later edits it
382 say "Renaming directories";
383 rename $pkg_dir => $old_dir;
385 say "Creating new package directory";
388 say "Populating new package directory";
389 my $map = $$info {MAP};
392 if ($$info {EXCLUDED}) {
393 foreach my $entry (@{$$info {EXCLUDED}}) {
394 if (ref $entry) {push @EXCLUDED_QR => $entry}
395 else {$EXCLUDED_QQ {$entry} = 1}
399 FILE: for my $file ( find_type_f( $new_dir )) {
400 my $old_file = $file;
401 $file =~ s{^\Q$new_dir\E/}{};
403 next if $EXCLUDED_QQ{$file};
404 for my $qr (@EXCLUDED_QR) {
405 next FILE if $file =~ $qr;
409 for my $key ( sort { length $b <=> length $a } keys %$map ) {
410 my $val = $map->{$key};
411 last if $file =~ s/^$key/$val/;
415 $file = $files[0] . '/' . $file;
418 if ( $file =~ m{^cpan/} ) {
419 $file =~ s{^cpan/}{};
422 $file = '../' . $file;
426 my @parts = split '/', $file;
428 for my $part (@parts) {
429 $prefix .= '/' if $prefix;
431 mkdir $prefix unless -d $prefix;
434 rename $old_file => $file;
436 remove_tree( $new_dir );
438 if (-f "$old_dir/.gitignore") {
439 say "Restoring .gitignore";
440 system git => 'checkout', "$pkg_dir/.gitignore";
443 my @new_files = find_type_f( $pkg_dir );
444 @new_files = grep {$_ ne $pkg_dir} @new_files;
445 s!^[^/]+/!! for @new_files;
446 my %new_files = map {$_ => 1} @new_files;
448 my @old_files = find_type_f( $old_dir );
449 @old_files = grep {$_ ne $old_dir} @old_files;
450 s!^[^/]+/!! for @old_files;
451 my %old_files = map {$_ => 1} @old_files;
457 foreach my $file (@new_files) {
458 next if -d "$pkg_dir/$file"; # Ignore directories.
459 next if $old_files {$file}; # It's already there.
460 if ($IGNORABLE {$file}) {
461 push @delete => $file;
464 push @commit => $file;
466 foreach my $file (@old_files) {
467 next if -d "$old_dir/$file";
468 next if $new_files {$file};
473 # Find all files with an exec bit
475 my @exec = find_type_f( $pkg_dir );
477 foreach my $file (@exec) {
479 $file =~ s!^[^/]+/!!;
480 if ($file =~ m!^t/!) {
481 push @de_exec => $file;
484 # Check to see if the file exists; if it doesn't and doesn't have
485 # the exec bit, remove it.
486 if ($old_files {$file}) {
487 unless (-x "$old_dir/$file") {
488 push @de_exec => $file;
494 # No need to change the +x bit on files that will be deleted.
496 if (@de_exec && @delete) {
497 my %delete = map {+"$pkg_dir/$_" => 1} @delete;
498 @de_exec = grep {!$delete {$_}} @de_exec;
502 # Mustn't change the +x bit on files that are whitelisted
505 my %permitted = map { (my $x = $_) =~ tr/\n//d; $x => 1 } grep !/^#/,
506 do { local @ARGV = '../Porting/exec-bit.txt'; <> };
507 @de_exec = grep !$permitted{"cpan/$pkg_dir/$_"}, @de_exec;
510 say "unlink $pkg_dir/$_" for @delete;
511 say "git add $pkg_dir/$_" for @commit;
512 say "git rm -f $pkg_dir/$_" for @gone;
513 say "chmod a-x $pkg_dir/$_" for @de_exec;
515 print "Hit return to continue; ^C to abort "; <STDIN>;
517 unlink "$pkg_dir/$_" for @delete;
518 system git => 'add', "$pkg_dir/$_" for @commit;
519 system git => 'rm', '-f', "$pkg_dir/$_" for @gone;
520 de_exec( "$pkg_dir/$_" ) for @de_exec;
523 # Restore anything that is customized.
524 # We don't really care whether we've deleted the file - since we
525 # do a git restore, it's going to be resurrected if necessary.
527 if ($$info {CUSTOMIZED}) {
528 say "Restoring customized files";
529 foreach my $file (@{$$info {CUSTOMIZED}}) {
530 system git => "checkout", "$pkg_dir/$file";
535 if (@commit || @gone) {
536 say "Fixing MANIFEST";
537 my $MANIFEST = "MANIFEST";
538 my $MANIFEST_NEW = "$MANIFEST.new";
540 open my $orig, "<", $MANIFEST
541 or die "Failed to open $MANIFEST for reading: $!\n";
542 open my $new, ">", $MANIFEST_NEW
543 or die "Failed to open $MANIFEST_NEW for writing: $!\n";
544 my %gone = map +("cpan/$pkg_dir/$_" => 1), @gone;
545 while (my $line = <$orig>) {
546 my ($file) = $line =~ /^(\S+)/
547 or die "Can't parse MANIFEST line: $line";
548 print $new $line if !$gone{$file};
551 say $new "cpan/$pkg_dir/$_" for @commit;
553 close $new or die "Can't close $MANIFEST: $!\n";
555 system $^X => "Porting/manisort", '--quiet', "--output=$MANIFEST", $MANIFEST_NEW;
557 or die "Can't delete temporary $MANIFEST_NEW: $!\n";
561 print "Running a make and saving its output to $MAKE_LOG ... ";
562 # Prepare for running (selected) tests
566 # The build system installs code from CPAN dists into the lib/ directory,
567 # creating directories as needed. This means that the cleaning-related rules
568 # in the Makefile need to know which directories to clean up. The Makefile
569 # is generated by Configure from Makefile.SH, so *that* file needs the list
570 # of directories. regen/lib_cleanup.pl is capable of automatically updating
571 # the contents of Makefile.SH (and win32/Makefile, which needs similar but
572 # not identical lists of directories), so we can just run that (using the
573 # newly-built Perl, as is done with the regen programs run by "make regen").
575 # We do this if any files at all have been added or deleted, regardless of
576 # whether those changes result in any directories being added or deleted,
577 # because the alternative would be to replicate the regen/lib_cleanup.pl
578 # logic here. That's fine, because regen/lib_cleanup.pl is idempotent if run
580 if (@commit || @gone) {
581 say "Running regen/lib_cleanup.pl to handle potential added/deleted dirs";
582 my $exe_dir = WIN32 ? ".\\" : './';
583 system "${exe_dir}perl$Config{_exe}", "-Ilib", "regen/lib_cleanup.pl"
584 and die "regen/lib_cleanup.pl failed\n";
588 # Must clean up, or else t/porting/FindExt.t will fail.
589 # Note that we can always retrieve the original directory with a git checkout.
591 print "About to clean up; hit return or abort (^C) "; <STDIN>;
593 remove_tree( "cpan/$old_dir" );
594 unlink "cpan/$new_file" unless $tarball;
597 # Run the tests. First the test belonging to the module, followed by the
601 say "Running module tests";
602 my @test_files = grep { /\.t$/ } find_type_f( "../cpan/$pkg_dir" );
603 my $exe_dir = WIN32 ? "..\\" : './';
604 my $output = `${exe_dir}perl$Config{_exe} TEST @test_files`;
605 unless ($output =~ /All tests successful/) {
610 print "Running tests in t/porting ";
611 my @tests = glob 'porting/*.t';
614 foreach my $t (@tests) {
615 my @not = grep {!/# TODO/ }
617 `${exe_dir}perl -I../lib -I.. $t`;
618 print @not ? '!' : '.';
619 push @failed => $t if @not;
622 say "Failed tests: @failed" if @failed;
627 open my $Maintainers_pl, '<', 'Porting/Maintainers.pl';
628 open my $new_Maintainers_pl, '>', 'Maintainers.pl';
632 while (<$Maintainers_pl>) {
634 if ($in_mod_section) {
635 if (/DISTRIBUTION/) {
636 if (s/\Q$old_version/$new_version/) {
651 print $new_Maintainers_pl $_;
655 say "Successfully updated Maintainers.pl";
656 unlink 'Porting/Maintainers.pl';
657 rename 'Maintainers.pl' => 'Porting/Maintainers.pl';
658 chmod 0755 => 'Porting/Maintainers.pl';
661 say "Could not update Porting/Maintainers.pl.";
662 say "Make sure you update this by hand before committing.";
667 =======================================================================
669 $module is now at version $new_version
670 Next, you should run "make minitest" and then "make test".
672 Minitest uses miniperl, which does not support XS modules. The full test
673 suite uses perl, which does. Minitest can fail - e.g. if a cpan module
674 has added an XS dependancy - even if the full test suite passes just fine.
676 Hopefully all will complete successfully, but if not, you can make any
677 changes you need to get the tests to pass. Don't forget that you'll need
678 a "CUSTOMIZED" entry in Porting/Maintainers.pl if you change any of the
679 files under cpan/$pkg_dir.
681 Once all tests pass, you can "git add -u" and "git commit" the changes
682 with a message along the lines of "Update Foo::Bar to v1.234".