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 69Adds new files to 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 TODO 94 95=over 4 96 97=item * 98 99Delete files from F<MANIFEST> 100 101=item * 102 103Update F<Porting/Maintainers.pl> 104 105=item * 106 107Optional, run a full test suite 108 109=item * 110 111Handle complicated C<FILES> 112 113=back 114 115This is an initial version; no attempt has been made yet to make this 116portable. It shells out instead of trying to find a Perl solution. 117In particular, it assumes wget, git, tar, chmod, perl, make, and rm 118to be available. 119 120=cut 121 122 123package Maintainers; 124 125use 5.010; 126 127use strict; 128use warnings; 129use Getopt::Long; 130 131$| = 1; 132 133die "This does not look like a top level directory" 134 unless -d "cpan" && -d "Porting"; 135 136our @IGNORABLE; 137our %Modules; 138 139use autodie; 140 141require "Porting/Maintainers.pl"; 142 143my %IGNORABLE = map {$_ => 1} @IGNORABLE; 144 145my $package = "02packages.details.txt"; 146my $package_url = "http://www.cpan.org/modules/$package"; 147my $package_file = "/tmp/$package"; 148 149my @problematic = ( 150 'podlators', # weird CUSTOMIZED section due to .PL files 151); 152 153 154GetOptions ('tarball=s' => \my $tarball, 155 'version=s' => \my $version, 156 force => \my $force,) 157 or die "Failed to parse arguments"; 158 159die "Usage: $0 module [args] [cpan package]" unless @ARGV == 1 || @ARGV == 2; 160 161my ($module) = shift; 162my $cpan_mod = @ARGV ? shift : $module; 163 164 165my $info = $Modules {$module} or die "Cannot find module $module"; 166my $distribution = $$info {DISTRIBUTION}; 167 168my @files = glob $$info {FILES}; 169if (!-d $files [0] || grep { $_ eq $module } @problematic) { 170 say "This looks like a setup $0 cannot handle (yet)"; 171 unless ($force) { 172 say "Will not continue without a --force option"; 173 exit 1; 174 } 175 say "--force is in effect, so we'll soldier on. Wish me luck!"; 176} 177 178 179chdir "cpan"; 180 181my $pkg_dir = $files[0]; 182 $pkg_dir =~ s!.*/!!; 183 184my ($old_version) = $distribution =~ /-([0-9.]+)\.tar\.gz/; 185 186my $o_module = $module; 187if ($cpan_mod =~ /-/ && $cpan_mod !~ /::/) { 188 $cpan_mod =~ s/-/::/g; 189} 190 191# 192# Find the information from CPAN. 193# 194my $new_file; 195my $new_version; 196unless ($tarball) { 197 # 198 # Poor man's cache 199 # 200 unless (-f $package_file && -M $package_file < 1) { 201 system wget => $package_url, '-qO', $package_file; 202 } 203 204 my $new_line = `grep '^$cpan_mod ' $package_file` 205 or die "Cannot find $cpan_mod on CPAN\n"; 206 chomp $new_line; 207 (undef, $new_version, my $new_path) = split ' ', $new_line; 208 if (defined $version) { 209 $new_path =~ s/-$new_version\./-$version\./; 210 $new_version = $version; 211 } 212 $new_file = (split '/', $new_path) [-1]; 213 214 my $url = "http://search.cpan.org/CPAN/authors/id/$new_path"; 215 say "Fetching $url"; 216 # 217 # Fetch the new distro 218 # 219 system wget => $url, '-qO', $new_file; 220} 221else { 222 $new_file = $tarball; 223 $new_version = $version // ($new_file =~ /-([0-9._]+)\.tar\.gz/) [0]; 224} 225 226my $old_dir = "$pkg_dir-$old_version"; 227 228say "Cleaning out old directory"; 229system git => 'clean', '-dfxq', $pkg_dir; 230 231say "Unpacking $new_file"; 232 233system tar => 'xfz', $new_file; 234(my $new_dir = $new_file) =~ s/\.tar\.gz//; 235# ensure 'make' will update all files 236system('find', $new_dir, '-exec', 'touch', '{}', ';'); 237 238say "Renaming directories"; 239rename $pkg_dir => $old_dir; 240 241say "Creating new package directory"; 242mkdir $pkg_dir; 243 244say "Populating new package directory"; 245my $map = $$info {MAP}; 246my @EXCLUDED_QR; 247my %EXCLUDED_QQ; 248if ($$info {EXCLUDED}) { 249 foreach my $entry (@{$$info {EXCLUDED}}) { 250 if (ref $entry) {push @EXCLUDED_QR => $entry} 251 else {$EXCLUDED_QQ {$entry} = 1} 252 } 253} 254 255FILE: for my $file ( `find $new_dir -type f` ) { 256 chomp $file; 257 my $old_file = $file; 258 $file =~ s{^$new_dir/}{}; 259 260 next if $EXCLUDED_QQ{$file}; 261 for my $qr (@EXCLUDED_QR) { 262 next FILE if $file =~ $qr; 263 } 264 265 if ( $map ) { 266 for my $key ( sort { length $b <=> length $a } keys %$map ) { 267 my $val = $map->{$key}; 268 last if $file =~ s/^$key/$val/; 269 } 270 } 271 else { 272 $file = $files[0] . '/' . $file; 273 } 274 275 if ( $file =~ m{^cpan/} ) { 276 $file =~ s{^cpan/}{}; 277 } 278 else { 279 $file = '../' . $file; 280 } 281 282 my $prefix = ''; 283 my @parts = split '/', $file; 284 pop @parts; 285 for my $part (@parts) { 286 $prefix .= '/' if $prefix; 287 $prefix .= $part; 288 mkdir $prefix unless -d $prefix; 289 } 290 291 rename $old_file => $file; 292} 293system 'rm', '-rf', $new_dir; 294 295if (-f "$old_dir/.gitignore") { 296 say "Restoring .gitignore"; 297 system git => 'checkout', "$pkg_dir/.gitignore"; 298} 299 300my @new_files = `find $pkg_dir -type f`; 301chomp @new_files; 302@new_files = grep {$_ ne $pkg_dir} @new_files; 303s!^[^/]+/!! for @new_files; 304my %new_files = map {$_ => 1} @new_files; 305 306my @old_files = `find $old_dir -type f`; 307chomp @old_files; 308@old_files = grep {$_ ne $old_dir} @old_files; 309s!^[^/]+/!! for @old_files; 310my %old_files = map {$_ => 1} @old_files; 311 312my @delete; 313my @commit; 314my @gone; 315FILE: 316foreach my $file (@new_files) { 317 next if -d "$pkg_dir/$file"; # Ignore directories. 318 next if $old_files {$file}; # It's already there. 319 if ($IGNORABLE {$file}) { 320 push @delete => $file; 321 next; 322 } 323 push @commit => $file; 324} 325foreach my $file (@old_files) { 326 next if -d "$old_dir/$file"; 327 next if $new_files {$file}; 328 push @gone => $file; 329} 330 331# 332# Find all files with an exec bit 333# 334my @exec = `find $pkg_dir -type f -perm +111`; 335chomp @exec; 336my @de_exec; 337foreach my $file (@exec) { 338 # Remove leading dir 339 $file =~ s!^[^/]+/!!; 340 if ($file =~ m!^t/!) { 341 push @de_exec => $file; 342 next; 343 } 344 # Check to see if the file exists; if it doesn't and doesn't have 345 # the exec bit, remove it. 346 if ($old_files {$file}) { 347 unless (-x "$old_dir/$file") { 348 push @de_exec => $file; 349 } 350 } 351} 352 353# 354# No need to change the +x bit on files that will be deleted. 355# 356if (@de_exec && @delete) { 357 my %delete = map {+"$pkg_dir/$_" => 1} @delete; 358 @de_exec = grep {!$delete {$_}} @de_exec; 359} 360 361say "unlink $pkg_dir/$_" for @delete; 362say "git add $pkg_dir/$_" for @commit; 363say "git rm -f $pkg_dir/$_" for @gone; 364say "chmod a-x $pkg_dir/$_" for @de_exec; 365 366print "Hit return to continue; ^C to abort "; <STDIN>; 367 368unlink "$pkg_dir/$_" for @delete; 369system git => 'add', "$pkg_dir/$_" for @commit; 370system git => 'rm', '-f', "$pkg_dir/$_" for @gone; 371system chmod => 'a-x', "$pkg_dir/$_" for @de_exec; 372 373# 374# Restore anything that is customized. 375# We don't really care whether we've deleted the file - since we 376# do a git restore, it's going to be resurrected if necessary. 377# 378if ($$info {CUSTOMIZED}) { 379 say "Restoring customized files"; 380 foreach my $file (@{$$info {CUSTOMIZED}}) { 381 system git => "checkout", "$pkg_dir/$file"; 382 } 383} 384 385chdir ".."; 386if (@commit) { 387 say "Fixing MANIFEST"; 388 my $MANIFEST = "MANIFEST"; 389 my $MANIFEST_SORT = "$MANIFEST.sorted"; 390 open my $fh, ">>", $MANIFEST; 391 say $fh "cpan/$pkg_dir/$_" for @commit; 392 close $fh; 393 system perl => "Porting/manisort", '--output', $MANIFEST_SORT; 394 rename $MANIFEST_SORT => $MANIFEST; 395} 396 397 398print "Running a make ... "; 399system "make > make.log 2>&1" and die "Running make failed, see make.log"; 400print "done\n"; 401 402# 403# Must clean up, or else t/porting/FindExt.t will fail. 404# Note that we can always retrieve the orginal directory with a git checkout. 405# 406print "About to clean up; hit return or abort (^C) "; <STDIN>; 407 408chdir "cpan"; 409system rm => '-r', $old_dir; 410unlink $new_file unless $tarball; 411 412 413# 414# Run the tests. First the test belonging to the module, followed by the 415# the tests in t/porting 416# 417chdir "../t"; 418say "Running module tests"; 419my @test_files = `find ../cpan/$pkg_dir -name '*.t' -type f`; 420chomp @test_files; 421my $output = `./perl TEST @test_files`; 422unless ($output =~ /All tests successful/) { 423 say $output; 424 exit 1; 425} 426 427print "Running tests in t/porting "; 428my @tests = `ls porting/*.t`; 429chomp @tests; 430my @failed; 431foreach my $t (@tests) { 432 my @not = `./perl -I../lib -I.. $t | grep ^not | grep -v "# TODO"`; 433 print @not ? '!' : '.'; 434 push @failed => $t if @not; 435} 436print "\n"; 437say "Failed tests: @failed" if @failed; 438 439 440say "Attempting to update Maintainers.pl"; 441chdir '..'; 442 443open my $Maintainers_pl, '<', 'Porting/Maintainers.pl'; 444open my $new_Maintainers_pl, '>', 'Maintainers.pl'; 445 446my $found; 447my $in_mod_section; 448while (<$Maintainers_pl>) { 449 if (!$found) { 450 if ($in_mod_section) { 451 if (/DISTRIBUTION/) { 452 if (s/\Q$old_version/$new_version/) { 453 $found = 1; 454 } 455 } 456 457 if (/^ }/) { 458 $in_mod_section = 0; 459 } 460 } 461 462 if (/\Q$cpan_mod/) { 463 $in_mod_section = 1; 464 } 465 } 466 467 print $new_Maintainers_pl $_; 468} 469 470if ($found) { 471 unlink 'Porting/Maintainers.pl'; 472 rename 'Maintainers.pl' => 'Porting/Maintainers.pl'; 473 system chmod => 'a+x', 'Porting/Maintainers.pl'; 474} 475else { 476 say "Could not update Porting/Maintainers.pl."; 477 say "Make sure you update this by hand before committing."; 478} 479 480say "$o_module is now version $new_version"; 481say "Now you ought to run a make; make test ..."; 482 483 484__END__ 485