xref: /openbsd-src/gnu/usr.bin/perl/Porting/sync-with-cpan (revision 9f11ffb7133c203312a01e4b986886bc88c7d74b)
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
69Updates the contents of 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 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
103=head1 TODO
104
105=over 4
106
107=item *
108
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.
123In particular, it assumes git, perl, and make
124to be available.
125
126=cut
127
128
129package Maintainers;
130
131use 5.010;
132
133use strict;
134use warnings;
135use Getopt::Long;
136use Archive::Tar;
137use File::Basename qw( basename );
138use File::Path qw( remove_tree );
139use File::Find;
140use File::Spec::Functions qw( tmpdir rel2abs );
141use Config qw( %Config );
142
143$| = 1;
144
145use constant WIN32 => $^O eq 'MSWin32';
146
147die "This does not look like a top level directory"
148     unless -d "cpan" && -d "Porting";
149
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
156our @IGNORABLE;
157our %Modules;
158
159use autodie;
160
161require "./Porting/Maintainers.pl";
162
163my $MAKE_LOG = 'make.log';
164
165my %IGNORABLE    = map {$_ => 1} @IGNORABLE;
166
167my $tmpdir = tmpdir();
168
169my $package      = "02packages.details.txt";
170my $package_url  = "http://www.cpan.org/modules/$package";
171my $package_file = "$tmpdir/$package"; # this is a cache
172
173my @problematic = (
174    'podlators', # weird CUSTOMIZED section due to .PL files
175);
176
177
178sub usage
179{
180    my $err = shift and select STDERR;
181    print "Usage: $0 module [args] [cpan package]\n";
182    exit $err;
183}
184
185GetOptions ('tarball=s'  =>  \my $tarball,
186            'version=s'  =>  \my $version,
187            'jobs=i'     =>  \my $make_jobs,
188             force       =>  \my $force,
189             help        =>  sub { usage 0; },
190             ) or  die "Failed to parse arguments";
191
192usage 1 unless @ARGV == 1 || @ARGV == 2;
193
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
204# Equivalent of `chmod a-x`
205sub de_exec {
206    my ($filename) = @_;
207    my $mode = (stat $filename)[2] & 0777;
208    if ($mode & 0111) { # exec-bit set
209        chmod $mode & 0666, $filename;
210    }
211}
212
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
222sub make {
223    my @args= @_;
224    unshift @args, "-j$make_jobs" if defined $make_jobs;
225    if (WIN32) {
226        chdir "Win32";
227        system "$Config{make} @args> ..\\$MAKE_LOG 2>&1"
228            and die "Running make failed, see $MAKE_LOG";
229        chdir '..';
230    } else {
231        system "$Config{make} @args> $MAKE_LOG 2>&1"
232            and die "Running make failed, see $MAKE_LOG";
233    };
234};
235
236my ($module)  = shift;
237
238my $info = $Modules{$module};
239if (!$info) {
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
248    say "Guessing you meant $guess instead of $module";
249    $module = $guess;
250}
251
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
271my $cpan_mod = @ARGV ? shift : $module;
272
273my  $distribution = $$info {DISTRIBUTION};
274
275my @files         = glob $$info {FILES};
276if (!-d $files [0] || grep { $_ eq $module } @problematic) {
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
285use Cwd 'cwd';
286my $orig_pwd = cwd();
287
288chdir "cpan";
289
290my  $pkg_dir      = $files[0];
291    $pkg_dir      =~ s!.*/!!;
292
293my ($old_version) = $distribution =~ /-([0-9.]+(?:-TRIAL[0-9]*)?)\.tar\.gz/;
294
295my  $o_module     = $module;
296if ($cpan_mod =~ /-/ && $cpan_mod !~ /::/) {
297    $cpan_mod =~ s/-/::/g;
298}
299
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
316#
317# Find the information from CPAN.
318#
319my $new_file;
320my $new_version;
321if (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;
329}
330else {
331    #
332    # Poor man's cache
333    #
334    unless (-f $package_file && -M $package_file < 1) {
335        wget $package_url, $package_file;
336    }
337
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;
345    }
346    $new_file = (split '/', $new_path) [-1];
347
348    die "The latest version of $module is $new_version, but blead already has it\n"
349        if $new_version eq $old_version;
350
351    my $url = "https://cpan.metacpan.org/authors/id/$new_path";
352    say "Fetching $url";
353    #
354    # Fetch the new distro
355    #
356    wget $url, $new_file;
357}
358
359my  $old_dir      = "$pkg_dir-$old_version";
360
361say "Cleaning out old directory";
362system git => 'clean', '-dfxq', $pkg_dir;
363
364say "Unpacking $new_file";
365Archive::Tar->extract_archive( $new_file );
366
367(my $new_dir = basename($new_file)) =~ s/\.tar\.gz//;
368# ensure 'make' will update all files
369my $t= time;
370for my $file (find_type_f($new_dir)) {
371    make_writable($file); # for convenience if the user later edits it
372    utime($t,$t,$file);
373};
374
375say "Renaming directories";
376rename $pkg_dir => $old_dir;
377
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
392FILE: for my $file ( find_type_f( $new_dir )) {
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    }
407    else {
408        $file = $files[0] . '/' . $file;
409    }
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}
429remove_tree( $new_dir );
430
431if (-f "$old_dir/.gitignore") {
432    say "Restoring .gitignore";
433    system git => 'checkout', "$pkg_dir/.gitignore";
434}
435
436my @new_files = find_type_f( $pkg_dir );
437@new_files = grep {$_ ne $pkg_dir} @new_files;
438s!^[^/]+/!! for @new_files;
439my %new_files = map {$_ => 1} @new_files;
440
441my @old_files = find_type_f( $old_dir );
442@old_files = grep {$_ ne $old_dir} @old_files;
443s!^[^/]+/!! for @old_files;
444my %old_files = map {$_ => 1} @old_files;
445
446my @delete;
447my @commit;
448my @gone;
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    }
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}
464
465#
466# Find all files with an exec bit
467#
468my @exec = find_type_f( $pkg_dir );
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}
485
486#
487# No need to change the +x bit on files that will be deleted.
488#
489if (@de_exec && @delete) {
490    my %delete = map {+"$pkg_dir/$_" => 1} @delete;
491    @de_exec = grep {!$delete {$_}} @de_exec;
492}
493
494#
495# Mustn't change the +x bit on files that are whitelisted
496#
497if (@de_exec) {
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;
501}
502
503say "unlink $pkg_dir/$_" for @delete;
504say "git add $pkg_dir/$_" for @commit;
505say "git rm -f $pkg_dir/$_" for @gone;
506say "chmod a-x $pkg_dir/$_" for @de_exec;
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;
513de_exec( "$pkg_dir/$_" )                  for @de_exec;
514
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
527chdir "..";
528if (@commit || @gone) {
529    say "Fixing MANIFEST";
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";
551}
552
553
554print "Running a make and saving its output to $MAKE_LOG ... ";
555# Prepare for running (selected) tests
556make 'test-prep';
557print "done\n";
558
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";
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";
578}
579
580#
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.
583#
584print "About to clean up; hit return or abort (^C) "; <STDIN>;
585
586remove_tree( "cpan/$old_dir" );
587unlink "cpan/$new_file" unless $tarball;
588
589#
590# Run the tests. First the test belonging to the module, followed by the
591# the tests in t/porting
592#
593chdir "t";
594say "Running module tests";
595my @test_files = grep { /\.t$/ } find_type_f( "../cpan/$pkg_dir" );
596my $exe_dir = WIN32 ? "..\\" : './';
597my $output = `${exe_dir}perl$Config{_exe} TEST @test_files`;
598unless ($output =~ /All tests successful/) {
599    say $output;
600    exit 1;
601}
602
603print "Running tests in t/porting ";
604my @tests = glob 'porting/*.t';
605chomp @tests;
606my @failed;
607foreach my $t (@tests) {
608    my @not = grep {!/# TODO/ }
609              grep { /^not/ }
610              `${exe_dir}perl -I../lib -I.. $t`;
611    print @not ? '!' : '.';
612    push @failed => $t if @not;
613}
614print "\n";
615say "Failed tests: @failed" if @failed;
616
617
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/) {
629                if (s/\Q$old_version/$new_version/) {
630                    $found = 1;
631                }
632            }
633
634            if (/^    \}/) {
635                $in_mod_section = 0;
636            }
637        }
638
639        if (/\Q$module/) {
640            $in_mod_section = 1;
641        }
642    }
643
644    print $new_Maintainers_pl $_;
645}
646
647if ($found) {
648    say "Successfully updated Maintainers.pl";
649    unlink 'Porting/Maintainers.pl';
650    rename 'Maintainers.pl' => 'Porting/Maintainers.pl';
651    chmod 0755 => 'Porting/Maintainers.pl';
652}
653else {
654    say "Could not update Porting/Maintainers.pl.";
655    say "Make sure you update this by hand before committing.";
656}
657
658print <<"EOF";
659
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
673
674__END__
675