This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
Use lexically scoped ('our') variables in POD examples.
[perl5.git] / Porting / sync-with-cpan
CommitLineData
33e80a47 1#!/usr/bin/env perl
418f4069 2
c5e3e317
JL
3=head1 NAME
4
f703fc96 5Porting/sync-with-cpan - Synchronize with CPAN distributions
c5e3e317
JL
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
190c1b3b 69Updates the contents of F<MANIFEST>
c5e3e317
JL
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
b7078f1e
AC
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
c5e3e317
JL
103=head1 TODO
104
105=over 4
106
107=item *
108
c5e3e317
JL
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.
cd9a1714 123In particular, it assumes git, perl, and make
c5e3e317
JL
124to be available.
125
126=cut
127
418f4069 128
4d18e0a2
A
129package Maintainers;
130
418f4069
A
131use 5.010;
132
133use strict;
134use warnings;
4d18e0a2 135use Getopt::Long;
a1450e8b 136use Archive::Tar;
bd4de633 137use File::Basename qw( basename );
192f56b0 138use File::Path qw( remove_tree );
fc134225 139use File::Find;
84709797 140use File::Spec::Functions qw( tmpdir rel2abs );
160daab8 141use Config qw( %Config );
418f4069
A
142
143$| = 1;
144
215d9c65
AC
145use constant WIN32 => $^O eq 'MSWin32';
146
07a826df 147die "This does not look like a top level directory"
418f4069
A
148 unless -d "cpan" && -d "Porting";
149
4b760246
AC
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
418f4069
A
156our @IGNORABLE;
157our %Modules;
158
159use autodie;
160
b27c755c 161require "./Porting/Maintainers.pl";
418f4069 162
e6e4cae9
AC
163my $MAKE_LOG = 'make.log';
164
418f4069
A
165my %IGNORABLE = map {$_ => 1} @IGNORABLE;
166
af8c53c3 167my $tmpdir = tmpdir();
fc134225 168
418f4069
A
169my $package = "02packages.details.txt";
170my $package_url = "http://www.cpan.org/modules/$package";
fc134225 171my $package_file = "$tmpdir/$package"; # this is a cache
418f4069 172
b7e2b692
JL
173my @problematic = (
174 'podlators', # weird CUSTOMIZED section due to .PL files
175);
176
418f4069 177
311454c0
MB
178sub usage
179{
180 my $err = shift and select STDERR;
181 print "Usage: $0 module [args] [cpan package]\n";
182 exit $err;
183}
184
4d18e0a2 185GetOptions ('tarball=s' => \my $tarball,
b5bf278a 186 'version=s' => \my $version,
b7078f1e 187 'jobs=i' => \my $make_jobs,
311454c0
MB
188 force => \my $force,
189 help => sub { usage 0; },
190 ) or die "Failed to parse arguments";
4d18e0a2 191
311454c0 192usage 1 unless @ARGV == 1 || @ARGV == 2;
418f4069 193
fc134225
MM
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
cd9a1714
MM
204# Equivalent of `chmod a-x`
205sub de_exec {
746bc9e1
AC
206 my ($filename) = @_;
207 my $mode = (stat $filename)[2] & 0777;
208 if ($mode & 0111) { # exec-bit set
209 chmod $mode & 0666, $filename;
cd9a1714
MM
210 }
211}
212
b6574671
AC
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
cd9a1714 222sub make {
160daab8 223 my @args= @_;
b7078f1e 224 unshift @args, "-j$make_jobs" if defined $make_jobs;
215d9c65 225 if (WIN32) {
160daab8 226 chdir "Win32";
e6e4cae9
AC
227 system "$Config{make} @args> ..\\$MAKE_LOG 2>&1"
228 and die "Running make failed, see $MAKE_LOG";
160daab8
MM
229 chdir '..';
230 } else {
e6e4cae9
AC
231 system "$Config{make} @args> $MAKE_LOG 2>&1"
232 and die "Running make failed, see $MAKE_LOG";
160daab8
MM
233 };
234};
235
5b73aae5 236my ($module) = shift;
418f4069 237
24c7e242
AC
238my $info = $Modules{$module};
239if (!$info) {
469f7948
AC
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
24c7e242
AC
248 say "Guessing you meant $guess instead of $module";
249 $module = $guess;
250}
251
7035e4d3
AC
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
24c7e242 271my $cpan_mod = @ARGV ? shift : $module;
4d18e0a2 272
418f4069 273my $distribution = $$info {DISTRIBUTION};
b5bf278a
A
274
275my @files = glob $$info {FILES};
b7e2b692 276if (!-d $files [0] || grep { $_ eq $module } @problematic) {
b5bf278a
A
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
84709797
FC
285use Cwd 'cwd';
286my $orig_pwd = cwd();
b5bf278a
A
287
288chdir "cpan";
289
83d3dd1d 290my $pkg_dir = $files[0];
418f4069
A
291 $pkg_dir =~ s!.*/!!;
292
87a7cbad 293my ($old_version) = $distribution =~ /-([0-9.]+(?:-TRIAL[0-9]*)?)\.tar\.gz/;
418f4069
A
294
295my $o_module = $module;
5b73aae5
A
296if ($cpan_mod =~ /-/ && $cpan_mod !~ /::/) {
297 $cpan_mod =~ s/-/::/g;
418f4069
A
298}
299
fea97686
FC
300sub wget {
301 my ($url, $saveas) = @_;
302 eval {
303 require HTTP::Tiny;
304 my $http= HTTP::Tiny->new();
305 $http->mirror( $url => $saveas );
306 1
307 } or
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;
314}
315
418f4069
A
316#
317# Find the information from CPAN.
318#
4d18e0a2
A
319my $new_file;
320my $new_version;
d11e2991 321if (defined $tarball) {
84709797 322 $tarball = rel2abs( $tarball, $orig_pwd ) ;
861c6796
AC
323 die "Tarball $tarball does not exist\n" if !-e $tarball;
324 die "Tarball $tarball is not a plain file\n" if !-f _;
d11e2991
AC
325 $new_file = $tarball;
326 $new_version = $version // ($new_file =~ /-([0-9._]+(?:-TRIAL[0-9]*)?)\.tar\.gz/) [0];
08ee4cf2
AC
327 die "Blead and that tarball both have version $new_version of $module\n"
328 if $new_version eq $old_version;
d11e2991
AC
329}
330else {
4d18e0a2
A
331 #
332 # Poor man's cache
333 #
334 unless (-f $package_file && -M $package_file < 1) {
fea97686 335 wget $package_url, $package_file;
4d18e0a2
A
336 }
337
cefd15c2
MM
338 open my $fh, '<', $package_file;
339 (my $new_line) = grep {/^$cpan_mod/} <$fh> # Yes, this needs a lot of memory
4d18e0a2 340 or die "Cannot find $cpan_mod on CPAN\n";
4d18e0a2 341 (undef, $new_version, my $new_path) = split ' ', $new_line;
3a4316cc
JL
342 if (defined $version) {
343 $new_path =~ s/-$new_version\./-$version\./;
344 $new_version = $version;
345 }
4d18e0a2
A
346 $new_file = (split '/', $new_path) [-1];
347
08ee4cf2
AC
348 die "The latest version of $module is $new_version, but blead already has it\n"
349 if $new_version eq $old_version;
350
499e37eb 351 my $url = "https://cpan.metacpan.org/authors/id/$new_path";
4d18e0a2
A
352 say "Fetching $url";
353 #
354 # Fetch the new distro
355 #
fea97686 356 wget $url, $new_file;
4d18e0a2 357}
418f4069
A
358
359my $old_dir = "$pkg_dir-$old_version";
418f4069
A
360
361say "Cleaning out old directory";
362system git => 'clean', '-dfxq', $pkg_dir;
363
418f4069 364say "Unpacking $new_file";
5fb91d48 365Archive::Tar->extract_archive( $new_file );
418f4069 366
bd4de633 367(my $new_dir = basename($new_file)) =~ s/\.tar\.gz//;
3f7808eb 368# ensure 'make' will update all files
fc134225
MM
369my $t= time;
370for my $file (find_type_f($new_dir)) {
b6574671 371 make_writable($file); # for convenience if the user later edits it
fc134225
MM
372 utime($t,$t,$file);
373};
418f4069
A
374
375say "Renaming directories";
376rename $pkg_dir => $old_dir;
418f4069 377
83d3dd1d
JL
378say "Creating new package directory";
379mkdir $pkg_dir;
380
381say "Populating new package directory";
382my $map = $$info {MAP};
383my @EXCLUDED_QR;
384my %EXCLUDED_QQ;
385if ($$info {EXCLUDED}) {
386 foreach my $entry (@{$$info {EXCLUDED}}) {
387 if (ref $entry) {push @EXCLUDED_QR => $entry}
388 else {$EXCLUDED_QQ {$entry} = 1}
389 }
390}
391
fc134225 392FILE: for my $file ( find_type_f( $new_dir )) {
83d3dd1d
JL
393 my $old_file = $file;
394 $file =~ s{^$new_dir/}{};
395
396 next if $EXCLUDED_QQ{$file};
397 for my $qr (@EXCLUDED_QR) {
398 next FILE if $file =~ $qr;
399 }
400
401 if ( $map ) {
402 for my $key ( sort { length $b <=> length $a } keys %$map ) {
403 my $val = $map->{$key};
404 last if $file =~ s/^$key/$val/;
405 }
406 }
7bbb137d
JL
407 else {
408 $file = $files[0] . '/' . $file;
409 }
83d3dd1d
JL
410
411 if ( $file =~ m{^cpan/} ) {
412 $file =~ s{^cpan/}{};
413 }
414 else {
415 $file = '../' . $file;
416 }
417
418 my $prefix = '';
419 my @parts = split '/', $file;
420 pop @parts;
421 for my $part (@parts) {
422 $prefix .= '/' if $prefix;
423 $prefix .= $part;
424 mkdir $prefix unless -d $prefix;
425 }
426
427 rename $old_file => $file;
428}
192f56b0 429remove_tree( $new_dir );
418f4069
A
430
431if (-f "$old_dir/.gitignore") {
432 say "Restoring .gitignore";
433 system git => 'checkout', "$pkg_dir/.gitignore";
434}
435
fc134225 436my @new_files = find_type_f( $pkg_dir );
418f4069
A
437@new_files = grep {$_ ne $pkg_dir} @new_files;
438s!^[^/]+/!! for @new_files;
439my %new_files = map {$_ => 1} @new_files;
440
fc134225 441my @old_files = find_type_f( $old_dir );
418f4069
A
442@old_files = grep {$_ ne $old_dir} @old_files;
443s!^[^/]+/!! for @old_files;
444my %old_files = map {$_ => 1} @old_files;
445
418f4069
A
446my @delete;
447my @commit;
448my @gone;
418f4069
A
449FILE:
450foreach 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;
455 next;
456 }
418f4069
A
457 push @commit => $file;
458}
459foreach my $file (@old_files) {
460 next if -d "$old_dir/$file";
461 next if $new_files {$file};
462 push @gone => $file;
463}
ad9b4e6f
A
464
465#
466# Find all files with an exec bit
467#
fc134225 468my @exec = find_type_f( $pkg_dir );
ad9b4e6f
A
469my @de_exec;
470foreach my $file (@exec) {
471 # Remove leading dir
472 $file =~ s!^[^/]+/!!;
473 if ($file =~ m!^t/!) {
474 push @de_exec => $file;
475 next;
476 }
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;
482 }
483 }
484}
418f4069
A
485
486#
487# No need to change the +x bit on files that will be deleted.
488#
ad9b4e6f 489if (@de_exec && @delete) {
a9f5d1d4 490 my %delete = map {+"$pkg_dir/$_" => 1} @delete;
ad9b4e6f 491 @de_exec = grep {!$delete {$_}} @de_exec;
418f4069
A
492}
493
e42bf9ad
AC
494#
495# Mustn't change the +x bit on files that are whitelisted
496#
497if (@de_exec) {
54ed4dc4 498 my %permitted = map { (my $x = $_) =~ tr/\n//d; $x => 1 } grep !/^#/,
e42bf9ad
AC
499 do { local @ARGV = '../Porting/exec-bit.txt'; <> };
500 @de_exec = grep !$permitted{"cpan/$pkg_dir/$_"}, @de_exec;
501}
502
418f4069
A
503say "unlink $pkg_dir/$_" for @delete;
504say "git add $pkg_dir/$_" for @commit;
505say "git rm -f $pkg_dir/$_" for @gone;
ad9b4e6f 506say "chmod a-x $pkg_dir/$_" for @de_exec;
418f4069
A
507
508print "Hit return to continue; ^C to abort "; <STDIN>;
509
510unlink "$pkg_dir/$_" for @delete;
511system git => 'add', "$pkg_dir/$_" for @commit;
512system git => 'rm', '-f', "$pkg_dir/$_" for @gone;
cd9a1714 513de_exec( "$pkg_dir/$_" ) for @de_exec;
418f4069 514
9c259538
A
515#
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.
519#
520if ($$info {CUSTOMIZED}) {
521 say "Restoring customized files";
522 foreach my $file (@{$$info {CUSTOMIZED}}) {
523 system git => "checkout", "$pkg_dir/$file";
524 }
525}
526
a8121781 527chdir "..";
190c1b3b 528if (@commit || @gone) {
418f4069 529 say "Fixing MANIFEST";
190c1b3b
AC
530 my $MANIFEST = "MANIFEST";
531 my $MANIFEST_NEW = "$MANIFEST.new";
532
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};
542 }
543
544 say $new "cpan/$pkg_dir/$_" for @commit;
545
546 close $new or die "Can't close $MANIFEST: $!\n";
547
548 system $^X => "Porting/manisort", '--quiet', "--output=$MANIFEST", $MANIFEST_NEW;
549 unlink $MANIFEST_NEW
550 or die "Can't delete temporary $MANIFEST_NEW: $!\n";
418f4069
A
551}
552
553
e6e4cae9 554print "Running a make and saving its output to $MAKE_LOG ... ";
160daab8
MM
555# Prepare for running (selected) tests
556make 'test-prep';
418f4069
A
557print "done\n";
558
d8a823f4
AC
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").
567#
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
572# repeatedly.
573if (@commit || @gone) {
574 say "Running regen/lib_cleanup.pl to handle potential added/deleted dirs";
215d9c65 575 my $exe_dir = WIN32 ? ".\\" : './';
d8a823f4
AC
576 system "${exe_dir}perl$Config{_exe}", "-Ilib", "regen/lib_cleanup.pl"
577 and die "regen/lib_cleanup.pl failed\n";
578}
579
418f4069
A
580#
581# Must clean up, or else t/porting/FindExt.t will fail.
730ad6b9 582# Note that we can always retrieve the original directory with a git checkout.
418f4069
A
583#
584print "About to clean up; hit return or abort (^C) "; <STDIN>;
585
192f56b0
MM
586remove_tree( "cpan/$old_dir" );
587unlink "cpan/$new_file" unless $tarball;
418f4069 588
ad9b4e6f
A
589#
590# Run the tests. First the test belonging to the module, followed by the
591# the tests in t/porting
592#
192f56b0 593chdir "t";
ad9b4e6f 594say "Running module tests";
57b5d6e1 595my @test_files = grep { /\.t$/ } find_type_f( "../cpan/$pkg_dir" );
215d9c65 596my $exe_dir = WIN32 ? "..\\" : './';
60f5272c 597my $output = `${exe_dir}perl$Config{_exe} TEST @test_files`;
ad9b4e6f
A
598unless ($output =~ /All tests successful/) {
599 say $output;
600 exit 1;
601}
602
418f4069 603print "Running tests in t/porting ";
fc134225 604my @tests = glob 'porting/*.t';
418f4069
A
605chomp @tests;
606my @failed;
607foreach my $t (@tests) {
cefd15c2
MM
608 my @not = grep {!/# TODO/ }
609 grep { /^not/ }
610 `${exe_dir}perl -I../lib -I.. $t`;
418f4069
A
611 print @not ? '!' : '.';
612 push @failed => $t if @not;
613}
614print "\n";
615say "Failed tests: @failed" if @failed;
616
617
9807c17b
JL
618chdir '..';
619
620open my $Maintainers_pl, '<', 'Porting/Maintainers.pl';
621open my $new_Maintainers_pl, '>', 'Maintainers.pl';
622
623my $found;
624my $in_mod_section;
625while (<$Maintainers_pl>) {
626 if (!$found) {
627 if ($in_mod_section) {
628 if (/DISTRIBUTION/) {
33c6567b 629 if (s/\Q$old_version/$new_version/) {
9807c17b
JL
630 $found = 1;
631 }
632 }
633
92e8e650 634 if (/^ \}/) {
9807c17b
JL
635 $in_mod_section = 0;
636 }
637 }
638
ec1d1ba0 639 if (/\Q$module/) {
9807c17b
JL
640 $in_mod_section = 1;
641 }
642 }
643
644 print $new_Maintainers_pl $_;
645}
646
647if ($found) {
d9d83ea5 648 say "Successfully updated Maintainers.pl";
9807c17b
JL
649 unlink 'Porting/Maintainers.pl';
650 rename 'Maintainers.pl' => 'Porting/Maintainers.pl';
cd9a1714 651 chmod 0755 => 'Porting/Maintainers.pl';
9807c17b
JL
652}
653else {
654 say "Could not update Porting/Maintainers.pl.";
655 say "Make sure you update this by hand before committing.";
656}
418f4069 657
592f3827 658print <<"EOF";
418f4069 659
592f3827
AC
660=======================================================================
661
662$o_module is now at version $new_version
663Next, you should run a "make test".
664
665Hopefully that will complete successfully, but if not, you can make any
666changes you need to get the tests to pass. Don't forget that you'll need
667a "CUSTOMIZED" entry in Porting/Maintainers.pl if you change any of the
668files under cpan/$pkg_dir.
669
670Once all tests pass, you can "git add -u" and "git commit" the changes.
671
672EOF
418f4069
A
673
674__END__