5 Porting/sync-with-cpan - Synchronize with CPAN distributions
9 perl Porting/sync-with-cpan <module>
11 where <module> is the name it appears in the C<%Modules> hash
12 of F<Porting/Maintainers.pl>
16 Script to help out with syncing cpan distros.
24 Fetches the package list from CPAN. Finds the current version of the given
29 Downloads the relevant tarball; unpacks the tarball. [1]
33 Clean out the old directory (C<git clean -dfx>)
37 Moves the old directory out of the way, moves the new directory in place.
41 Restores any F<.gitignore> file.
45 Removes files from C<@IGNORE> and C<EXCLUDED>
49 C<git add> any new files.
53 C<git rm> any files that are gone.
57 Remove the +x bit on files in F<t/>
61 Remove the +x bit on files that don't have it enabled in the current dir
65 Restore files mentioned in C<CUSTOMIZED>
69 Updates the contents of F<MANIFEST>
73 Runs a C<make> (assumes a configure has been run)
81 Runs tests for the package
85 Runs the porting tests
89 [1] If the C<--tarball> option is given, then CPAN is not consulted.
90 C<--tarball> should be the path to the tarball; the version is extracted
91 from the filename -- but can be overwritten by the C<--version> option.
99 When running C<make>, pass a C<< -jI<N> >> option to it.
109 Update F<Porting/Maintainers.pl>
113 Optional, run a full test suite
117 Handle complicated C<FILES>
121 This is an initial version; no attempt has been made yet to make this
122 portable. It shells out instead of trying to find a Perl solution.
123 In particular, it assumes git, perl, and make
137 use File::Basename qw( basename );
138 use File::Path qw( remove_tree );
140 use File::Spec::Functions qw( tmpdir rel2abs );
141 use Config qw( %Config );
145 use constant WIN32 => $^O eq 'MSWin32';
147 die "This does not look like a top level directory"
148 unless -d "cpan" && -d "Porting";
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.
153 die "Please run Configure before using $0\n"
154 if !WIN32 && !-f "Makefile";
161 require "./Porting/Maintainers.pl";
163 my $MAKE_LOG = 'make.log';
165 my %IGNORABLE = map {$_ => 1} @IGNORABLE;
167 my $tmpdir = tmpdir();
169 my $package = "02packages.details.txt";
170 my $package_url = "http://www.cpan.org/modules/$package";
171 my $package_file = "$tmpdir/$package"; # this is a cache
174 'podlators', # weird CUSTOMIZED section due to .PL files
180 my $err = shift and select STDERR;
181 print "Usage: $0 module [args] [cpan package]\n";
185 GetOptions ('tarball=s' => \my $tarball,
186 'version=s' => \my $version,
187 'jobs=i' => \my $make_jobs,
189 help => sub { usage 0; },
190 ) or die "Failed to parse arguments";
192 usage 1 unless @ARGV == 1 || @ARGV == 2;
196 find( { no_chdir => 1, wanted => sub {
197 my $file= $File::Find::name;
198 return unless -f $file;
204 # Equivalent of `chmod a-x`
207 my $mode = (stat $filename)[2] & 0777;
208 if ($mode & 0111) { # exec-bit set
209 chmod $mode & 0666, $filename;
213 # Equivalent of `chmod +w`
216 my $mode = (stat $filename)[2] & 0777;
217 if (!($mode & 0222)) { # not writable
218 chmod $mode | (0222 & ~umask), $filename;
224 unshift @args, "-j$make_jobs" if defined $make_jobs;
227 system "$Config{make} @args> ..\\$MAKE_LOG 2>&1"
228 and die "Running make failed, see $MAKE_LOG";
231 system "$Config{make} @args> $MAKE_LOG 2>&1"
232 and die "Running make failed, see $MAKE_LOG";
236 my ($module) = shift;
238 my $info = $Modules{$module};
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.
243 s/-/::/g or s/::/-/g for $guess;
244 $info = $Modules{$guess} or die <<"EOF";
245 Cannot find module $module.
246 The available options are listed in the %Modules hash in Porting/Maintainers.pl
248 say "Guessing you meant $guess instead of $module";
252 if ($info->{CUSTOMIZED}) {
254 $module has a CUSTOMIZED entry in Porting/Maintainers.pl.
256 This program's behaviour is to copy every CUSTOMIZED file into the version
257 of the module being imported. But that might not be the right thing: in some
258 cases, the new CPAN version will supersede whatever changes had previously
259 been made in blead, so it would be better to import the new CPAN files.
261 If you've checked that the CUSTOMIZED versions are still correct, you can
262 proceed now. Otherwise, you should abort and investigate the situation. If
263 the blead customizations are no longer needed, delete the CUSTOMIZED entry
264 for $module in Porting/Maintainers.pl (and you'll also need to regenerate
265 t/porting/customized.dat in that case; see t/porting/customized.t).
268 print "Hit return to continue; ^C to abort "; <STDIN>;
271 my $cpan_mod = @ARGV ? shift : $module;
273 my $distribution = $$info {DISTRIBUTION};
275 my @files = glob $$info {FILES};
276 if (!-d $files [0] || grep { $_ eq $module } @problematic) {
277 say "This looks like a setup $0 cannot handle (yet)";
279 say "Will not continue without a --force option";
282 say "--force is in effect, so we'll soldier on. Wish me luck!";
286 my $orig_pwd = cwd();
290 my $pkg_dir = $files[0];
293 my ($old_version) = $distribution =~ /-([0-9.]+(?:-TRIAL[0-9]*)?)\.tar\.gz/;
295 my $o_module = $module;
296 if ($cpan_mod =~ /-/ && $cpan_mod !~ /::/) {
297 $cpan_mod =~ s/-/::/g;
301 my ($url, $saveas) = @_;
304 my $http= HTTP::Tiny->new();
305 $http->mirror( $url => $saveas );
308 # Some system do not have wget. Fall back to curl if we do not
309 # have it. On Windows, `which wget` is not going to work, so
310 # just use wget, as this script has always done.
311 WIN32 || -x substr(`which wget`, 0, -1)
312 ? system wget => $url, '-qO', $saveas
313 : system curl => $url, '-sSo', $saveas;
317 # Find the information from CPAN.
321 if (defined $tarball) {
322 $tarball = rel2abs( $tarball, $orig_pwd ) ;
323 die "Tarball $tarball does not exist\n" if !-e $tarball;
324 die "Tarball $tarball is not a plain file\n" if !-f _;
325 $new_file = $tarball;
326 $new_version = $version // ($new_file =~ /-([0-9._]+(?:-TRIAL[0-9]*)?)\.tar\.gz/) [0];
327 die "Blead and that tarball both have version $new_version of $module\n"
328 if $new_version eq $old_version;
334 unless (-f $package_file && -M $package_file < 1) {
335 wget $package_url, $package_file;
338 open my $fh, '<', $package_file;
339 (my $new_line) = grep {/^$cpan_mod/} <$fh> # Yes, this needs a lot of memory
340 or die "Cannot find $cpan_mod on CPAN\n";
341 (undef, $new_version, my $new_path) = split ' ', $new_line;
342 if (defined $version) {
343 $new_path =~ s/-$new_version\./-$version\./;
344 $new_version = $version;
346 $new_file = (split '/', $new_path) [-1];
348 die "The latest version of $module is $new_version, but blead already has it\n"
349 if $new_version eq $old_version;
351 my $url = "https://cpan.metacpan.org/authors/id/$new_path";
354 # Fetch the new distro
356 wget $url, $new_file;
359 my $old_dir = "$pkg_dir-$old_version";
361 say "Cleaning out old directory";
362 system git => 'clean', '-dfxq', $pkg_dir;
364 say "Unpacking $new_file";
365 Archive::Tar->extract_archive( $new_file );
367 (my $new_dir = basename($new_file)) =~ s/\.tar\.gz//;
368 # ensure 'make' will update all files
370 for my $file (find_type_f($new_dir)) {
371 make_writable($file); # for convenience if the user later edits it
375 say "Renaming directories";
376 rename $pkg_dir => $old_dir;
378 say "Creating new package directory";
381 say "Populating new package directory";
382 my $map = $$info {MAP};
385 if ($$info {EXCLUDED}) {
386 foreach my $entry (@{$$info {EXCLUDED}}) {
387 if (ref $entry) {push @EXCLUDED_QR => $entry}
388 else {$EXCLUDED_QQ {$entry} = 1}
392 FILE: for my $file ( find_type_f( $new_dir )) {
393 my $old_file = $file;
394 $file =~ s{^$new_dir/}{};
396 next if $EXCLUDED_QQ{$file};
397 for my $qr (@EXCLUDED_QR) {
398 next FILE if $file =~ $qr;
402 for my $key ( sort { length $b <=> length $a } keys %$map ) {
403 my $val = $map->{$key};
404 last if $file =~ s/^$key/$val/;
408 $file = $files[0] . '/' . $file;
411 if ( $file =~ m{^cpan/} ) {
412 $file =~ s{^cpan/}{};
415 $file = '../' . $file;
419 my @parts = split '/', $file;
421 for my $part (@parts) {
422 $prefix .= '/' if $prefix;
424 mkdir $prefix unless -d $prefix;
427 rename $old_file => $file;
429 remove_tree( $new_dir );
431 if (-f "$old_dir/.gitignore") {
432 say "Restoring .gitignore";
433 system git => 'checkout', "$pkg_dir/.gitignore";
436 my @new_files = find_type_f( $pkg_dir );
437 @new_files = grep {$_ ne $pkg_dir} @new_files;
438 s!^[^/]+/!! for @new_files;
439 my %new_files = map {$_ => 1} @new_files;
441 my @old_files = find_type_f( $old_dir );
442 @old_files = grep {$_ ne $old_dir} @old_files;
443 s!^[^/]+/!! for @old_files;
444 my %old_files = map {$_ => 1} @old_files;
450 foreach my $file (@new_files) {
451 next if -d "$pkg_dir/$file"; # Ignore directories.
452 next if $old_files {$file}; # It's already there.
453 if ($IGNORABLE {$file}) {
454 push @delete => $file;
457 push @commit => $file;
459 foreach my $file (@old_files) {
460 next if -d "$old_dir/$file";
461 next if $new_files {$file};
466 # Find all files with an exec bit
468 my @exec = find_type_f( $pkg_dir );
470 foreach my $file (@exec) {
472 $file =~ s!^[^/]+/!!;
473 if ($file =~ m!^t/!) {
474 push @de_exec => $file;
477 # Check to see if the file exists; if it doesn't and doesn't have
478 # the exec bit, remove it.
479 if ($old_files {$file}) {
480 unless (-x "$old_dir/$file") {
481 push @de_exec => $file;
487 # No need to change the +x bit on files that will be deleted.
489 if (@de_exec && @delete) {
490 my %delete = map {+"$pkg_dir/$_" => 1} @delete;
491 @de_exec = grep {!$delete {$_}} @de_exec;
495 # Mustn't change the +x bit on files that are whitelisted
498 my %permitted = map { (my $x = $_) =~ tr/\n//d; $x => 1 } grep !/^#/,
499 do { local @ARGV = '../Porting/exec-bit.txt'; <> };
500 @de_exec = grep !$permitted{"cpan/$pkg_dir/$_"}, @de_exec;
503 say "unlink $pkg_dir/$_" for @delete;
504 say "git add $pkg_dir/$_" for @commit;
505 say "git rm -f $pkg_dir/$_" for @gone;
506 say "chmod a-x $pkg_dir/$_" for @de_exec;
508 print "Hit return to continue; ^C to abort "; <STDIN>;
510 unlink "$pkg_dir/$_" for @delete;
511 system git => 'add', "$pkg_dir/$_" for @commit;
512 system git => 'rm', '-f', "$pkg_dir/$_" for @gone;
513 de_exec( "$pkg_dir/$_" ) for @de_exec;
516 # Restore anything that is customized.
517 # We don't really care whether we've deleted the file - since we
518 # do a git restore, it's going to be resurrected if necessary.
520 if ($$info {CUSTOMIZED}) {
521 say "Restoring customized files";
522 foreach my $file (@{$$info {CUSTOMIZED}}) {
523 system git => "checkout", "$pkg_dir/$file";
528 if (@commit || @gone) {
529 say "Fixing MANIFEST";
530 my $MANIFEST = "MANIFEST";
531 my $MANIFEST_NEW = "$MANIFEST.new";
533 open my $orig, "<", $MANIFEST
534 or die "Failed to open $MANIFEST for reading: $!\n";
535 open my $new, ">", $MANIFEST_NEW
536 or die "Failed to open $MANIFEST_NEW for writing: $!\n";
537 my %gone = map +("cpan/$pkg_dir/$_" => 1), @gone;
538 while (my $line = <$orig>) {
539 my ($file) = $line =~ /^(\S+)/
540 or die "Can't parse MANIFEST line: $line";
541 print $new $line if !$gone{$file};
544 say $new "cpan/$pkg_dir/$_" for @commit;
546 close $new or die "Can't close $MANIFEST: $!\n";
548 system $^X => "Porting/manisort", '--quiet', "--output=$MANIFEST", $MANIFEST_NEW;
550 or die "Can't delete temporary $MANIFEST_NEW: $!\n";
554 print "Running a make and saving its output to $MAKE_LOG ... ";
555 # Prepare for running (selected) tests
559 # The build system installs code from CPAN dists into the lib/ directory,
560 # creating directories as needed. This means that the cleaning-related rules
561 # in the Makefile need to know which directories to clean up. The Makefile
562 # is generated by Configure from Makefile.SH, so *that* file needs the list
563 # of directories. regen/lib_cleanup.pl is capable of automatically updating
564 # the contents of Makefile.SH (and win32/Makefile, which needs similar but
565 # not identical lists of directories), so we can just run that (using the
566 # newly-built Perl, as is done with the regen programs run by "make regen").
568 # We do this if any files at all have been added or deleted, regardless of
569 # whether those changes result in any directories being added or deleted,
570 # because the alternative would be to replicate the regen/lib_cleanup.pl
571 # logic here. That's fine, because regen/lib_cleanup.pl is idempotent if run
573 if (@commit || @gone) {
574 say "Running regen/lib_cleanup.pl to handle potential added/deleted dirs";
575 my $exe_dir = WIN32 ? ".\\" : './';
576 system "${exe_dir}perl$Config{_exe}", "-Ilib", "regen/lib_cleanup.pl"
577 and die "regen/lib_cleanup.pl failed\n";
581 # Must clean up, or else t/porting/FindExt.t will fail.
582 # Note that we can always retrieve the original directory with a git checkout.
584 print "About to clean up; hit return or abort (^C) "; <STDIN>;
586 remove_tree( "cpan/$old_dir" );
587 unlink "cpan/$new_file" unless $tarball;
590 # Run the tests. First the test belonging to the module, followed by the
591 # the tests in t/porting
594 say "Running module tests";
595 my @test_files = grep { /\.t$/ } find_type_f( "../cpan/$pkg_dir" );
596 my $exe_dir = WIN32 ? "..\\" : './';
597 my $output = `${exe_dir}perl$Config{_exe} TEST @test_files`;
598 unless ($output =~ /All tests successful/) {
603 print "Running tests in t/porting ";
604 my @tests = glob 'porting/*.t';
607 foreach my $t (@tests) {
608 my @not = grep {!/# TODO/ }
610 `${exe_dir}perl -I../lib -I.. $t`;
611 print @not ? '!' : '.';
612 push @failed => $t if @not;
615 say "Failed tests: @failed" if @failed;
620 open my $Maintainers_pl, '<', 'Porting/Maintainers.pl';
621 open my $new_Maintainers_pl, '>', 'Maintainers.pl';
625 while (<$Maintainers_pl>) {
627 if ($in_mod_section) {
628 if (/DISTRIBUTION/) {
629 if (s/\Q$old_version/$new_version/) {
644 print $new_Maintainers_pl $_;
648 say "Successfully updated Maintainers.pl";
649 unlink 'Porting/Maintainers.pl';
650 rename 'Maintainers.pl' => 'Porting/Maintainers.pl';
651 chmod 0755 => 'Porting/Maintainers.pl';
654 say "Could not update Porting/Maintainers.pl.";
655 say "Make sure you update this by hand before committing.";
660 =======================================================================
662 $o_module is now at version $new_version
663 Next, you should run a "make test".
665 Hopefully that will complete successfully, but if not, you can make any
666 changes you need to get the tests to pass. Don't forget that you'll need
667 a "CUSTOMIZED" entry in Porting/Maintainers.pl if you change any of the
668 files under cpan/$pkg_dir.
670 Once all tests pass, you can "git add -u" and "git commit" the changes.