xref: /openbsd-src/gnu/usr.bin/perl/Porting/core-cpan-diff (revision 3d61058aa5c692477b6d18acfbbdb653a9930ff9)
143003dfeSmillert#!/usr/bin/env perl
243003dfeSmillert
343003dfeSmillert# core-cpan-diff: Compare CPAN modules with their equivalent in core
443003dfeSmillert
543003dfeSmillert# Originally based on App::DualLivedDiff by Steffen Mueller.
643003dfeSmillert
743003dfeSmillertuse strict;
843003dfeSmillertuse warnings;
943003dfeSmillert
1043003dfeSmillertuse 5.010;
1143003dfeSmillert
12*3d61058aSafresh1use Getopt::Long qw(:config bundling);
13898184e3Ssthenuse File::Basename ();
14898184e3Ssthenuse File::Copy     ();
1543003dfeSmillertuse File::Temp     ();
1643003dfeSmillertuse File::Path     ();
176fb12b70Safresh1use File::Spec;
18898184e3Ssthenuse File::Spec::Functions;
1943003dfeSmillertuse IO::Uncompress::Gunzip ();
2043003dfeSmillertuse File::Compare          ();
2143003dfeSmillertuse ExtUtils::Manifest;
22898184e3Ssthenuse ExtUtils::MakeMaker ();
23898184e3Ssthenuse HTTP::Tiny;
2443003dfeSmillert
2543003dfeSmillertBEGIN { die "Must be run from root of perl source tree\n" unless -d 'Porting' }
2643003dfeSmillertuse lib 'Porting';
2743003dfeSmillertuse Maintainers ();
2843003dfeSmillert
2943003dfeSmillertuse Archive::Tar;
306fb12b70Safresh1use Cwd qw[cwd chdir];
3143003dfeSmillertuse IPC::Open3;
3243003dfeSmillertuse IO::Select;
336fb12b70Safresh1local $Archive::Tar::WARN = 0;
3443003dfeSmillert
35898184e3Ssthen# where, under the cache dir, to download tarballs to
36898184e3Ssthenuse constant SRC_DIR => 'tarballs';
3743003dfeSmillert
3843003dfeSmillert# where, under the cache dir, to untar stuff to
3943003dfeSmillertuse constant UNTAR_DIR => 'untarred';
4043003dfeSmillert
4143003dfeSmillertuse constant DIFF_CMD => 'diff';
4243003dfeSmillert
4343003dfeSmillertsub usage {
4443003dfeSmillert    print STDERR "\n@_\n\n" if @_;
4543003dfeSmillert    print STDERR <<HERE;
4643003dfeSmillertUsage: $0 [opts] [ -d | -v | -x ] [ -a | module ... ]
4743003dfeSmillert
4843003dfeSmillert-a/--all        Scan all dual-life modules.
4943003dfeSmillert
5043003dfeSmillert-c/--cachedir   Where to save downloaded CPAN tarball files
5143003dfeSmillert                (defaults to /tmp/something/ with deletion after each run).
5243003dfeSmillert
5343003dfeSmillert-d/--diff       Display file differences using diff(1), rather than just
5443003dfeSmillert                listing which files have changed.
5543003dfeSmillert
56e0680481Safresh1--diffopts      Options to pass to the diff command. Defaults to '-u --text'
57eac174f2Safresh1                (except on *BSD, where it's just '-u').
5843003dfeSmillert
59*3d61058aSafresh1-f/--force      Force download from CPAN of new 02packages.details.txt file
6043003dfeSmillert                (with --crosscheck only).
6143003dfeSmillert
62*3d61058aSafresh1-m/--mirror     Preferred CPAN mirror URI (http:// or file:///)
63898184e3Ssthen                (Local mirror must be a complete mirror, not minicpan)
64898184e3Ssthen
6543003dfeSmillert-o/--output     File name to write output to (defaults to STDOUT).
6643003dfeSmillert
6743003dfeSmillert-r/--reverse    Reverses the diff (perl to CPAN).
6843003dfeSmillert
69*3d61058aSafresh1-u/--upstream   Only print modules with the given upstream (defaults to all)
70b39c5158Smillert
7143003dfeSmillert-v/--verbose    List the fate of *all* files in the tarball, not just those
7243003dfeSmillert                that differ or are missing.
7343003dfeSmillert
74*3d61058aSafresh1-x/--crosscheck List the distributions whose current CPAN version differs from
7543003dfeSmillert                that in blead (i.e. the DISTRIBUTION field in Maintainers.pl).
7643003dfeSmillert
7743003dfeSmillertBy default (i.e. without the --crosscheck option),  for each listed module
7843003dfeSmillert(or with -a, all CPAN modules listed in Maintainers.pl), grab the tarball
7943003dfeSmillertfrom CPAN associated with that module, and compare the files in it with
8043003dfeSmillertthose in the perl source tree.
8143003dfeSmillert
8243003dfeSmillertMust be run from the root of the perl source tree.
8343003dfeSmillertModule names must match the keys of %Modules in Maintainers.pl.
8491f110e0Safresh1
8591f110e0Safresh1The diff(1) command is assumed to be in your PATH and is used to diff files
8691f110e0Safresh1regardless of whether the --diff option has been chosen to display any file
8791f110e0Safresh1differences.
8843003dfeSmillertHERE
8943003dfeSmillert    exit(1);
9043003dfeSmillert}
9143003dfeSmillert
9243003dfeSmillertsub run {
9343003dfeSmillert    my $scan_all;
9443003dfeSmillert    my $diff_opts;
9543003dfeSmillert    my $reverse = 0;
96b39c5158Smillert    my @wanted_upstreams;
9743003dfeSmillert    my $cache_dir;
98898184e3Ssthen    my $mirror_url = "http://www.cpan.org/";
9943003dfeSmillert    my $use_diff;
10043003dfeSmillert    my $output_file;
1016fb12b70Safresh1    my $verbose = 0;
10243003dfeSmillert    my $force;
10343003dfeSmillert    my $do_crosscheck;
10443003dfeSmillert
10543003dfeSmillert    GetOptions(
10643003dfeSmillert        'a|all'         => \$scan_all,
10743003dfeSmillert        'c|cachedir=s'  => \$cache_dir,
10843003dfeSmillert        'd|diff'        => \$use_diff,
10943003dfeSmillert        'diffopts:s'    => \$diff_opts,
11043003dfeSmillert        'f|force'       => \$force,
11143003dfeSmillert        'h|help'        => \&usage,
112898184e3Ssthen        'm|mirror=s'    => \$mirror_url,
11343003dfeSmillert        'o|output=s'    => \$output_file,
11443003dfeSmillert        'r|reverse'     => \$reverse,
115b39c5158Smillert        'u|upstream=s@' => \@wanted_upstreams,
1166fb12b70Safresh1        'v|verbose:1'   => \$verbose,
11743003dfeSmillert        'x|crosscheck'  => \$do_crosscheck,
11843003dfeSmillert    ) or usage;
11943003dfeSmillert
12043003dfeSmillert    my @modules;
12143003dfeSmillert
12243003dfeSmillert    usage("Cannot mix -a with module list") if $scan_all && @ARGV;
12343003dfeSmillert
12443003dfeSmillert    if ($do_crosscheck) {
1256fb12b70Safresh1        usage("can't use -r, -d, --diffopts with --crosscheck")
1266fb12b70Safresh1          if ( $reverse || $use_diff || $diff_opts );
12743003dfeSmillert    }
12843003dfeSmillert    else {
129e0680481Safresh1        #$diff_opts = '-u --text' unless defined $diff_opts;
130eac174f2Safresh1        if (! defined $diff_opts) {
131e0680481Safresh1            $diff_opts = ($^O =~ m/bsd$/i) ? '-u' : '-u --text';
132eac174f2Safresh1        };
13343003dfeSmillert        usage("can't use -f without --crosscheck") if $force;
13443003dfeSmillert    }
13543003dfeSmillert
136898184e3Ssthen    @modules =
137898184e3Ssthen      $scan_all
13843003dfeSmillert      ? grep $Maintainers::Modules{$_}{CPAN},
13943003dfeSmillert      ( sort { lc $a cmp lc $b } keys %Maintainers::Modules )
14043003dfeSmillert      : @ARGV;
14143003dfeSmillert    usage("No modules specified") unless @modules;
14243003dfeSmillert
14343003dfeSmillert    my $outfh;
14443003dfeSmillert    if ( defined $output_file ) {
14543003dfeSmillert        open $outfh, '>', $output_file
14643003dfeSmillert          or die "ERROR: could not open file '$output_file' for writing: $!\n";
14743003dfeSmillert    }
14843003dfeSmillert    else {
14943003dfeSmillert        open $outfh, ">&STDOUT"
15043003dfeSmillert          or die "ERROR: can't dup STDOUT: $!\n";
15143003dfeSmillert    }
15243003dfeSmillert
15343003dfeSmillert    if ( defined $cache_dir ) {
15491f110e0Safresh1        die "ERROR: not a directory: '$cache_dir'\n"
15591f110e0Safresh1            if !-d $cache_dir && -e $cache_dir;
15691f110e0Safresh1        File::Path::mkpath($cache_dir);
15743003dfeSmillert    }
158898184e3Ssthen    else {
159898184e3Ssthen        $cache_dir = File::Temp::tempdir( CLEANUP => 1 );
160898184e3Ssthen    }
161898184e3Ssthen
162898184e3Ssthen    $mirror_url .= "/" unless substr( $mirror_url, -1 ) eq "/";
163898184e3Ssthen    my $test_file = "modules/03modlist.data.gz";
164898184e3Ssthen    my_getstore(
165898184e3Ssthen        cpan_url( $mirror_url, $test_file ),
166898184e3Ssthen        catfile( $cache_dir, $test_file )
167898184e3Ssthen    ) or die "ERROR: not a CPAN mirror '$mirror_url'\n";
16843003dfeSmillert
16943003dfeSmillert    if ($do_crosscheck) {
17091f110e0Safresh1        do_crosscheck(
1716fb12b70Safresh1            $outfh, $cache_dir, $mirror_url, $verbose,
17291f110e0Safresh1            $force, \@modules,  \@wanted_upstreams
17391f110e0Safresh1        );
17443003dfeSmillert    }
17543003dfeSmillert    else {
1766fb12b70Safresh1        $verbose > 2 and $use_diff++;
177898184e3Ssthen        do_compare(
178898184e3Ssthen            \@modules,  $outfh,      $output_file,
179898184e3Ssthen            $cache_dir, $mirror_url, $verbose,
180898184e3Ssthen            $use_diff,  $reverse,    $diff_opts,
181898184e3Ssthen            \@wanted_upstreams
182898184e3Ssthen        );
18343003dfeSmillert    }
18443003dfeSmillert}
18543003dfeSmillert
186898184e3Ssthen# construct a CPAN url
18743003dfeSmillert
188898184e3Ssthensub cpan_url {
189898184e3Ssthen    my ( $mirror_url, @path ) = @_;
190898184e3Ssthen    return $mirror_url unless @path;
191898184e3Ssthen    my $cpan_path = join( "/", map { split "/", $_ } @path );
192898184e3Ssthen    $cpan_path =~ s{\A/}{};    # remove leading slash since url has one trailing
193898184e3Ssthen    return $mirror_url . $cpan_path;
194898184e3Ssthen}
195898184e3Ssthen
196898184e3Ssthen# construct a CPAN URL for a author/distribution string like:
197898184e3Ssthen# BINGOS/Archive-Extract-0.52.tar.gz
198898184e3Ssthen
199898184e3Ssthensub cpan_url_distribution {
200898184e3Ssthen    my ( $mirror_url, $distribution ) = @_;
201898184e3Ssthen    $distribution =~ /^([A-Z])([A-Z])/
202898184e3Ssthen        or die "ERROR: invalid DISTRIBUTION name (not /^[A-Z]{2}/): $distribution\n";
20391f110e0Safresh1    my $path = "authors/id/$1/$1$2/$distribution";
204898184e3Ssthen    return cpan_url( $mirror_url, $path );
205898184e3Ssthen}
20643003dfeSmillert
20743003dfeSmillert# compare a list of modules against their CPAN equivalents
20843003dfeSmillert
20943003dfeSmillertsub do_compare {
210898184e3Ssthen    my (
211898184e3Ssthen        $modules,    $outfh,   $output_file, $cache_dir,
212898184e3Ssthen        $mirror_url, $verbose, $use_diff,    $reverse,
213898184e3Ssthen        $diff_opts,  $wanted_upstreams
214898184e3Ssthen    ) = @_;
21543003dfeSmillert
21643003dfeSmillert    # first, make sure we have a directory where they can all be untarred,
21743003dfeSmillert    # and if its a permanent directory, clear any previous content
218898184e3Ssthen    my $untar_dir = catdir( $cache_dir, UNTAR_DIR );
219898184e3Ssthen    my $src_dir   = catdir( $cache_dir, SRC_DIR );
220898184e3Ssthen    for my $d ( $src_dir, $untar_dir ) {
221898184e3Ssthen        next if -d $d;
222898184e3Ssthen        mkdir $d or die "mkdir $d: $!\n";
22343003dfeSmillert    }
22443003dfeSmillert
22543003dfeSmillert    my %ignorable = map { ( $_ => 1 ) } @Maintainers::IGNORABLE;
22691f110e0Safresh1    my %wanted_upstream = map { ( $_ => 1 ) } @$wanted_upstreams;
22743003dfeSmillert
22843003dfeSmillert    my %seen_dist;
22943003dfeSmillert    for my $module (@$modules) {
23043003dfeSmillert        warn "Processing $module ...\n" if defined $output_file;
23143003dfeSmillert
23243003dfeSmillert        my $m = $Maintainers::Modules{$module}
23343003dfeSmillert          or die "ERROR: No such module in Maintainers.pl: '$module'\n";
23443003dfeSmillert
23543003dfeSmillert        unless ( $m->{CPAN} ) {
23643003dfeSmillert            print $outfh "WARNING: $module is not dual-life; skipping\n";
23743003dfeSmillert            next;
23843003dfeSmillert        }
23943003dfeSmillert
24043003dfeSmillert        my $dist = $m->{DISTRIBUTION};
24143003dfeSmillert        die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $dist;
24243003dfeSmillert
243898184e3Ssthen        if ( $seen_dist{$dist}++ ) {
244898184e3Ssthen            warn "WARNING: duplicate entry for $dist in $module\n";
24543003dfeSmillert        }
246b39c5158Smillert
2476fb12b70Safresh1        my $upstream = $m->{UPSTREAM} // 'undef';
24891f110e0Safresh1        next if @$wanted_upstreams and !$wanted_upstream{$upstream};
249b39c5158Smillert
250898184e3Ssthen        print $outfh "\n$module - "
251898184e3Ssthen          . $Maintainers::Modules{$module}->{DISTRIBUTION} . "\n";
252898184e3Ssthen        print $outfh "  upstream is: "
25391f110e0Safresh1          . ( $m->{UPSTREAM} // 'UNKNOWN!' ) . "\n";
25443003dfeSmillert
25543003dfeSmillert        my $cpan_dir;
25643003dfeSmillert        eval {
257898184e3Ssthen            $cpan_dir =
258898184e3Ssthen              get_distribution( $src_dir, $mirror_url, $untar_dir, $module,
259898184e3Ssthen                $dist );
26043003dfeSmillert        };
26143003dfeSmillert        if ($@) {
26243003dfeSmillert            print $outfh "  ", $@;
26343003dfeSmillert            print $outfh "  (skipping)\n";
26443003dfeSmillert            next;
26543003dfeSmillert        }
26643003dfeSmillert
26743003dfeSmillert        my @perl_files = Maintainers::get_module_files($module);
26843003dfeSmillert
269898184e3Ssthen        my $manifest = catfile( $cpan_dir, 'MANIFEST' );
27043003dfeSmillert        die "ERROR: no such file: $manifest\n" unless -f $manifest;
27143003dfeSmillert
27243003dfeSmillert        my $cpan_files = ExtUtils::Manifest::maniread($manifest);
27343003dfeSmillert        my @cpan_files = sort keys %$cpan_files;
27443003dfeSmillert
275898184e3Ssthen        ( my $main_pm = $module ) =~ s{::}{/}g;
276898184e3Ssthen        $main_pm .= ".pm";
277898184e3Ssthen
278898184e3Ssthen        my ( $excluded, $map, $customized ) =
279898184e3Ssthen          get_map( $m, $module, \@perl_files );
28043003dfeSmillert
28143003dfeSmillert        my %perl_unseen;
28243003dfeSmillert        @perl_unseen{@perl_files} = ();
28343003dfeSmillert        my %perl_files = %perl_unseen;
28443003dfeSmillert
28543003dfeSmillert        foreach my $cpan_file (@cpan_files) {
286898184e3Ssthen            my $mapped_file =
287898184e3Ssthen              cpan_to_perl( $excluded, $map, $customized, $cpan_file );
28843003dfeSmillert            unless ( defined $mapped_file ) {
28943003dfeSmillert                print $outfh "  Excluded:  $cpan_file\n" if $verbose;
29043003dfeSmillert                next;
29143003dfeSmillert            }
29243003dfeSmillert
29343003dfeSmillert            if ( exists $perl_files{$mapped_file} ) {
29443003dfeSmillert                delete $perl_unseen{$mapped_file};
29543003dfeSmillert            }
29643003dfeSmillert            else {
297898184e3Ssthen
29843003dfeSmillert                # some CPAN files foo are stored in core as foo.packed,
29943003dfeSmillert                # which are then unpacked by 'make test_prep'
30043003dfeSmillert                my $packed_file = "$mapped_file.packed";
30143003dfeSmillert                if ( exists $perl_files{$packed_file} ) {
30243003dfeSmillert                    if ( !-f $mapped_file and -f $packed_file ) {
30343003dfeSmillert                        print $outfh <<EOF;
30443003dfeSmillertWARNING: $mapped_file not found, but .packed variant exists.
30543003dfeSmillertPerhaps you need to run 'make test_prep'?
30643003dfeSmillertEOF
30743003dfeSmillert                        next;
30843003dfeSmillert                    }
30943003dfeSmillert                    delete $perl_unseen{$packed_file};
31043003dfeSmillert                }
31143003dfeSmillert                else {
31243003dfeSmillert                    if ( $ignorable{$cpan_file} ) {
31343003dfeSmillert                        print $outfh "  Ignored:   $cpan_file\n" if $verbose;
31443003dfeSmillert                        next;
31543003dfeSmillert                    }
31643003dfeSmillert
31743003dfeSmillert                    unless ($use_diff) {
31843003dfeSmillert                        print $outfh "  CPAN only: $cpan_file",
319898184e3Ssthen                          ( $cpan_file eq $mapped_file )
320898184e3Ssthen                          ? "\n"
321898184e3Ssthen                          : " (missing $mapped_file)\n";
32243003dfeSmillert                    }
32343003dfeSmillert                    next;
32443003dfeSmillert                }
32543003dfeSmillert            }
32643003dfeSmillert
327898184e3Ssthen            my $abs_cpan_file = catfile( $cpan_dir, $cpan_file );
32843003dfeSmillert
32943003dfeSmillert            # should never happen
330898184e3Ssthen            die "ERROR: can't find file $abs_cpan_file\n"
331898184e3Ssthen              unless -f $abs_cpan_file;
33243003dfeSmillert
33343003dfeSmillert            # might happen if the FILES entry in Maintainers.pl is wrong
33443003dfeSmillert            unless ( -f $mapped_file ) {
33543003dfeSmillert                print $outfh "WARNING: perl file not found: $mapped_file\n";
33643003dfeSmillert                next;
33743003dfeSmillert            }
33843003dfeSmillert
339898184e3Ssthen            my $relative_mapped_file = relatively_mapped($mapped_file);
34043003dfeSmillert
341898184e3Ssthen            my $different =
342898184e3Ssthen              file_diff( $outfh, $abs_cpan_file, $mapped_file, $reverse,
343898184e3Ssthen                $diff_opts );
344898184e3Ssthen            if ( $different && customized( $m, $relative_mapped_file ) ) {
345898184e3Ssthen		print $outfh "  Customized for blead: $relative_mapped_file\n";
3466fb12b70Safresh1                if ( $use_diff && $verbose ) {
3476fb12b70Safresh1                    $different =~ s{^(--- |\+\+\+ )/tmp/[^/]+/}{$1}gm;
3486fb12b70Safresh1                    print $outfh $different;
349898184e3Ssthen                }
350898184e3Ssthen            }
351898184e3Ssthen            elsif ($different) {
35243003dfeSmillert                if ($use_diff) {
353898184e3Ssthen                    $different =~ s{^(--- |\+\+\+ )/tmp/[^/]+/}{$1}gm;
354898184e3Ssthen                    print $outfh $different;
35543003dfeSmillert                }
35643003dfeSmillert                else {
357b39c5158Smillert                    if ( $cpan_file eq $relative_mapped_file ) {
358b39c5158Smillert                        print $outfh "  Modified:  $relative_mapped_file\n";
35943003dfeSmillert                    }
36043003dfeSmillert                    else {
361898184e3Ssthen                        print $outfh
362898184e3Ssthen                          "  Modified:  $cpan_file $relative_mapped_file\n";
36343003dfeSmillert                    }
364898184e3Ssthen
365898184e3Ssthen                    if ( $cpan_file =~ m{\.pm\z} ) {
366898184e3Ssthen                        my $pv = MM->parse_version($mapped_file)   || 'unknown';
367898184e3Ssthen                        my $cv = MM->parse_version($abs_cpan_file) || 'unknown';
368898184e3Ssthen                        if ( $pv ne $cv ) {
369898184e3Ssthen                            print $outfh
370898184e3Ssthen"  Version mismatch in '$cpan_file':\n    $cv (cpan) vs $pv (perl)\n";
371898184e3Ssthen                        }
372898184e3Ssthen                    }
373898184e3Ssthen
374898184e3Ssthen                }
375898184e3Ssthen            }
376898184e3Ssthen            elsif ( customized( $m, $relative_mapped_file ) ) {
377898184e3Ssthen                # Maintainers.pl says we customized it, but it looks the
378898184e3Ssthen                # same as CPAN so maybe we lost the customization, which
379898184e3Ssthen                # could be bad
380898184e3Ssthen                if ( $cpan_file eq $relative_mapped_file ) {
381898184e3Ssthen                    print $outfh "  Blead customization missing: $cpan_file\n";
382898184e3Ssthen                }
383898184e3Ssthen                else {
384898184e3Ssthen                    print $outfh
385898184e3Ssthen                      "  Blead customization missing: $cpan_file $relative_mapped_file\n";
38643003dfeSmillert                }
38743003dfeSmillert            }
38843003dfeSmillert            elsif ($verbose) {
389b39c5158Smillert                if ( $cpan_file eq $relative_mapped_file ) {
39043003dfeSmillert                    print $outfh "  Unchanged: $cpan_file\n";
39143003dfeSmillert                }
39243003dfeSmillert                else {
393898184e3Ssthen                    print $outfh
394898184e3Ssthen                      "  Unchanged: $cpan_file $relative_mapped_file\n";
39543003dfeSmillert                }
39643003dfeSmillert            }
39743003dfeSmillert        }
39843003dfeSmillert        for ( sort keys %perl_unseen ) {
399898184e3Ssthen            my $relative_mapped_file = relatively_mapped($_);
400898184e3Ssthen            if ( customized( $m, $relative_mapped_file ) ) {
401898184e3Ssthen                print $outfh "  Customized for blead: $_\n";
402898184e3Ssthen            }
403898184e3Ssthen            else {
40443003dfeSmillert                print $outfh "  Perl only: $_\n" unless $use_diff;
40543003dfeSmillert            }
40643003dfeSmillert        }
4076fb12b70Safresh1        if ( $verbose ) {
4086fb12b70Safresh1            foreach my $exclude (@$excluded) {
4096fb12b70Safresh1                my $seen = 0;
4106fb12b70Safresh1                foreach my $cpan_file (@cpan_files) {
4116fb12b70Safresh1                    # may be a simple string to match exactly, or a pattern
4126fb12b70Safresh1                    if ( ref $exclude ) {
4136fb12b70Safresh1                        $seen = 1 if $cpan_file =~ $exclude;
4146fb12b70Safresh1                    }
4156fb12b70Safresh1                    else {
4166fb12b70Safresh1                        $seen = 1 if $cpan_file eq $exclude;
4176fb12b70Safresh1                    }
4186fb12b70Safresh1                    last if $seen;
4196fb12b70Safresh1                }
4206fb12b70Safresh1                if ( not $seen ) {
4216fb12b70Safresh1                    print $outfh "  Unnecessary exclusion: $exclude\n";
4226fb12b70Safresh1                }
4236fb12b70Safresh1            }
4246fb12b70Safresh1        }
42543003dfeSmillert    }
426898184e3Ssthen}
427898184e3Ssthen
428898184e3Ssthensub relatively_mapped {
429898184e3Ssthen    my $relative = shift;
430898184e3Ssthen    $relative =~ s/^(cpan|dist|ext)\/.*?\///;
431898184e3Ssthen    return $relative;
432898184e3Ssthen}
43343003dfeSmillert
43443003dfeSmillert# given FooBar-1.23_45.tar.gz, return FooBar
43543003dfeSmillert
43643003dfeSmillertsub distro_base {
43743003dfeSmillert    my $d = shift;
438*3d61058aSafresh1    my $tail_pat = qr/\.(?:tar\.(?:g?z|bz2|Z)|zip|tgz|tbz)/;
439*3d61058aSafresh1    $d =~ s{-v?([0-9._]+(?:-TRIAL[0-9]*)?)$tail_pat\z}{};
44043003dfeSmillert    return $d;
44143003dfeSmillert}
44243003dfeSmillert
44343003dfeSmillert# process --crosscheck action:
44443003dfeSmillert# ie list all distributions whose CPAN versions differ from that listed in
44543003dfeSmillert# Maintainers.pl
44643003dfeSmillert
44743003dfeSmillertsub do_crosscheck {
44891f110e0Safresh1    my (
4496fb12b70Safresh1        $outfh, $cache_dir, $mirror_url, $verbose,
45091f110e0Safresh1        $force, $modules,   $wanted_upstreams,
45191f110e0Safresh1    ) = @_;
45243003dfeSmillert
45343003dfeSmillert    my $file         = '02packages.details.txt';
45443003dfeSmillert    my $download_dir = $cache_dir || File::Temp::tempdir( CLEANUP => 1 );
455898184e3Ssthen    my $path         = catfile( $download_dir, $file );
45643003dfeSmillert    my $gzfile       = "$path.gz";
45743003dfeSmillert
45843003dfeSmillert    # grab 02packages.details.txt
45943003dfeSmillert
460898184e3Ssthen    my $url = cpan_url( $mirror_url, "modules/02packages.details.txt.gz" );
46143003dfeSmillert
46243003dfeSmillert    if ( !-f $gzfile or $force ) {
46343003dfeSmillert        unlink $gzfile;
46443003dfeSmillert        my_getstore( $url, $gzfile );
46543003dfeSmillert    }
46643003dfeSmillert    unlink $path;
46743003dfeSmillert    IO::Uncompress::Gunzip::gunzip( $gzfile, $path )
468898184e3Ssthen      or die
469898184e3Ssthen      "ERROR: failed to ungzip $gzfile: $IO::Uncompress::Gunzip::GunzipError\n";
47043003dfeSmillert
47143003dfeSmillert    # suck in the data from it
47243003dfeSmillert
47343003dfeSmillert    open my $fh, '<', $path
47443003dfeSmillert      or die "ERROR: open: $file: $!\n";
47543003dfeSmillert
47643003dfeSmillert    my %distros;
47743003dfeSmillert    my %modules;
47843003dfeSmillert
47943003dfeSmillert    while (<$fh>) {
48043003dfeSmillert        next if 1 .. /^$/;
48143003dfeSmillert        chomp;
48243003dfeSmillert        my @f = split ' ', $_;
48343003dfeSmillert        if ( @f != 3 ) {
484898184e3Ssthen            warn
485898184e3Ssthen              "WARNING: $file:$.: line doesn't have three fields (skipping)\n";
48643003dfeSmillert            next;
48743003dfeSmillert        }
48843003dfeSmillert        my $distro = $f[2];
48943003dfeSmillert        $distro =~ s{^[A-Z]/[A-Z]{2}/}{};    # strip leading A/AB/
49043003dfeSmillert        $modules{ $f[0] } = $distro;
49143003dfeSmillert
49243003dfeSmillert        ( my $short_distro = $distro ) =~ s{^.*/}{};
49343003dfeSmillert
49443003dfeSmillert        $distros{ distro_base($short_distro) }{$distro} = 1;
49543003dfeSmillert    }
49643003dfeSmillert
49791f110e0Safresh1    my %wanted_upstream = map { ( $_ => 1 ) } @$wanted_upstreams;
49843003dfeSmillert    for my $module (@$modules) {
49943003dfeSmillert        my $m = $Maintainers::Modules{$module}
50043003dfeSmillert          or die "ERROR: No such module in Maintainers.pl: '$module'\n";
50143003dfeSmillert
5026fb12b70Safresh1        $verbose and warn "Checking $module\n";
5036fb12b70Safresh1
50443003dfeSmillert        unless ( $m->{CPAN} ) {
50543003dfeSmillert            print $outfh "\nWARNING: $module is not dual-life; skipping\n";
50643003dfeSmillert            next;
50743003dfeSmillert        }
50843003dfeSmillert
50943003dfeSmillert        # given an entry like
51043003dfeSmillert        #   Foo::Bar 1.23 foo-bar-1.23.tar.gz,
51143003dfeSmillert        # first compare the module name against Foo::Bar, and failing that,
51243003dfeSmillert        # against foo-bar
51343003dfeSmillert
51443003dfeSmillert        my $pdist = $m->{DISTRIBUTION};
51543003dfeSmillert        die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $pdist;
51643003dfeSmillert
5176fb12b70Safresh1        my $upstream = $m->{UPSTREAM} // 'undef';
51891f110e0Safresh1        next if @$wanted_upstreams and !$wanted_upstream{$upstream};
51991f110e0Safresh1
52043003dfeSmillert        my $cdist = $modules{$module};
52143003dfeSmillert        ( my $short_pdist = $pdist ) =~ s{^.*/}{};
52243003dfeSmillert
52343003dfeSmillert        unless ( defined $cdist ) {
52443003dfeSmillert            my $d = $distros{ distro_base($short_pdist) };
52543003dfeSmillert            unless ( defined $d ) {
52643003dfeSmillert                print $outfh "\n$module: Can't determine current CPAN entry\n";
52743003dfeSmillert                next;
52843003dfeSmillert            }
52943003dfeSmillert            if ( keys %$d > 1 ) {
530898184e3Ssthen                print $outfh
531898184e3Ssthen                  "\n$module: (found more than one CPAN candidate):\n";
532b8851fccSafresh1                print $outfh "    Perl: $pdist\n";
53343003dfeSmillert                print $outfh "    CPAN: $_\n" for sort keys %$d;
53443003dfeSmillert                next;
53543003dfeSmillert            }
53643003dfeSmillert            $cdist = ( keys %$d )[0];
53743003dfeSmillert        }
53843003dfeSmillert
53943003dfeSmillert        if ( $cdist ne $pdist ) {
54043003dfeSmillert            print $outfh "\n$module:\n    Perl: $pdist\n    CPAN: $cdist\n";
54143003dfeSmillert        }
54243003dfeSmillert    }
54343003dfeSmillert}
54443003dfeSmillert
54543003dfeSmillert# get the EXCLUDED and MAP entries for this module, or
5466fb12b70Safresh1# make up defaults if they don't exist
54743003dfeSmillert
54843003dfeSmillertsub get_map {
54943003dfeSmillert    my ( $m, $module_name, $perl_files ) = @_;
55043003dfeSmillert
551898184e3Ssthen    my ( $excluded, $map, $customized ) = @$m{qw(EXCLUDED MAP CUSTOMIZED)};
55243003dfeSmillert
55343003dfeSmillert    $excluded   ||= [];
554898184e3Ssthen    $customized ||= [];
55543003dfeSmillert
556898184e3Ssthen    return $excluded, $map, $customized if $map;
55743003dfeSmillert
55843003dfeSmillert    # all files under ext/foo-bar (plus maybe some under t/lib)???
55943003dfeSmillert
56043003dfeSmillert    my $ext;
56143003dfeSmillert    for (@$perl_files) {
562b39c5158Smillert        if (m{^((?:ext|dist|cpan)/[^/]+/)}) {
56343003dfeSmillert            if ( defined $ext and $ext ne $1 ) {
564898184e3Ssthen
56543003dfeSmillert                # more than one ext/$ext/
56643003dfeSmillert                undef $ext;
56743003dfeSmillert                last;
56843003dfeSmillert            }
56943003dfeSmillert            $ext = $1;
57043003dfeSmillert        }
57143003dfeSmillert        elsif (m{^t/lib/}) {
57243003dfeSmillert            next;
57343003dfeSmillert        }
57443003dfeSmillert        else {
57543003dfeSmillert            undef $ext;
57643003dfeSmillert            last;
57743003dfeSmillert        }
57843003dfeSmillert    }
57943003dfeSmillert
58043003dfeSmillert    if ( defined $ext ) {
581898184e3Ssthen        $map = { '' => $ext },;
58243003dfeSmillert    }
58343003dfeSmillert    else {
58443003dfeSmillert        ( my $base = $module_name ) =~ s{::}{/}g;
58543003dfeSmillert        $base = "lib/$base";
58643003dfeSmillert        $map  = {
58743003dfeSmillert            'lib/' => 'lib/',
58843003dfeSmillert            ''     => "$base/",
58943003dfeSmillert        };
59043003dfeSmillert    }
591898184e3Ssthen    return $excluded, $map, $customized;
59243003dfeSmillert}
59343003dfeSmillert
59443003dfeSmillert# Given an exclude list and a mapping hash, convert a CPAN filename
59543003dfeSmillert# (eg 't/bar.t') to the equivalent perl filename (eg 'lib/Foo/Bar/t/bar.t').
59643003dfeSmillert# Returns an empty list for an excluded file
59743003dfeSmillert
59843003dfeSmillertsub cpan_to_perl {
599898184e3Ssthen    my ( $excluded, $map, $customized, $cpan_file ) = @_;
60043003dfeSmillert
60191f110e0Safresh1    my %customized = map { ( $_ => 1 ) } @$customized;
60243003dfeSmillert    for my $exclude (@$excluded) {
60391f110e0Safresh1        next if $customized{$exclude};
604898184e3Ssthen
60543003dfeSmillert        # may be a simple string to match exactly, or a pattern
60643003dfeSmillert        if ( ref $exclude ) {
60743003dfeSmillert            return if $cpan_file =~ $exclude;
60843003dfeSmillert        }
60943003dfeSmillert        else {
61043003dfeSmillert            return if $cpan_file eq $exclude;
61143003dfeSmillert        }
61243003dfeSmillert    }
61343003dfeSmillert
61443003dfeSmillert    my $perl_file = $cpan_file;
61543003dfeSmillert
61643003dfeSmillert    # try longest prefix first, then alphabetically on tie-break
617898184e3Ssthen    for
618898184e3Ssthen      my $prefix ( sort { length($b) <=> length($a) || $a cmp $b } keys %$map )
61943003dfeSmillert    {
62043003dfeSmillert        last if $perl_file =~ s/^\Q$prefix/$map->{$prefix}/;
62143003dfeSmillert    }
62243003dfeSmillert    return $perl_file;
62343003dfeSmillert}
62443003dfeSmillert
625898184e3Ssthen# fetch a file from a URL and store it in a file given by a filename
62643003dfeSmillert
62743003dfeSmillertsub my_getstore {
62843003dfeSmillert    my ( $url, $file ) = @_;
629898184e3Ssthen    File::Path::mkpath( File::Basename::dirname($file) );
630898184e3Ssthen    if ( $url =~ qr{\Afile://(?:localhost)?/} ) {
631898184e3Ssthen        ( my $local_path = $url ) =~ s{\Afile://(?:localhost)?}{};
632898184e3Ssthen        File::Copy::copy( $local_path, $file );
633898184e3Ssthen    } else {
634898184e3Ssthen        my $http = HTTP::Tiny->new;
635898184e3Ssthen        my $response = $http->mirror($url, $file);
636898184e3Ssthen        return $response->{success};
63743003dfeSmillert    }
63843003dfeSmillert}
63943003dfeSmillert
64043003dfeSmillert# download and unpack a distribution
64143003dfeSmillert# Returns the full pathname of the extracted directory
64243003dfeSmillert# (eg '/tmp/XYZ/Foo_bar-1.23')
64343003dfeSmillert
644898184e3Ssthen# cache_dir:  where to download the .tar.gz file to
645898184e3Ssthen# mirror_url: CPAN mirror to download from
64643003dfeSmillert# untar_dir:  where to untar or unzup the file
64743003dfeSmillert# module:     name of module
64843003dfeSmillert# dist:       name of the distribution
64943003dfeSmillert
65043003dfeSmillertsub get_distribution {
651898184e3Ssthen    my ( $src_dir, $mirror_url, $untar_dir, $module, $dist ) = @_;
65243003dfeSmillert
65343003dfeSmillert    $dist =~ m{.+/([^/]+)$}
654898184e3Ssthen      or die
655898184e3Ssthen      "ERROR: $module: invalid DISTRIBUTION name (no AUTHOR/ prefix): $dist\n";
65643003dfeSmillert    my $filename = $1;
65743003dfeSmillert
658898184e3Ssthen    my $download_file = catfile( $src_dir, $filename );
65943003dfeSmillert
66043003dfeSmillert    # download distribution
66143003dfeSmillert
66243003dfeSmillert    if ( -f $download_file and !-s $download_file ) {
663898184e3Ssthen
66491f110e0Safresh1        # failed download might leave a zero-length file
66543003dfeSmillert        unlink $download_file;
66643003dfeSmillert    }
66743003dfeSmillert
66843003dfeSmillert    unless ( -f $download_file ) {
66943003dfeSmillert
670898184e3Ssthen        # not cached
671898184e3Ssthen        my $url = cpan_url_distribution( $mirror_url, $dist );
67243003dfeSmillert        my_getstore( $url, $download_file )
67343003dfeSmillert          or die "ERROR: Could not fetch '$url'\n";
67443003dfeSmillert    }
67543003dfeSmillert
676898184e3Ssthen    # get the expected name of the extracted distribution dir
67743003dfeSmillert
678898184e3Ssthen    my $path = catfile( $untar_dir, $filename );
679898184e3Ssthen
680898184e3Ssthen    $path =~ s/\.tar\.gz$//
68191f110e0Safresh1      or $path =~ s/\.tgz$//
682898184e3Ssthen      or $path =~ s/\.zip$//
683898184e3Ssthen      or die
684898184e3Ssthen      "ERROR: downloaded file does not have a recognised suffix: $path\n";
685898184e3Ssthen
686898184e3Ssthen    # extract it unless we already have it cached or tarball is newer
687898184e3Ssthen    if ( !-d $path || ( -M $download_file < -M $path ) ) {
6886fb12b70Safresh1        $path = extract( $download_file, $untar_dir )
689898184e3Ssthen          or die
690898184e3Ssthen          "ERROR: failed to extract distribution '$download_file to temp. dir: "
6916fb12b70Safresh1          . $! . "\n";
692898184e3Ssthen    }
69343003dfeSmillert
69443003dfeSmillert    die "ERROR: Extracted tarball does not appear as $path\n" unless -d $path;
69543003dfeSmillert
69643003dfeSmillert    return $path;
69743003dfeSmillert}
69843003dfeSmillert
69943003dfeSmillert# produce the diff of a single file
70043003dfeSmillertsub file_diff {
70143003dfeSmillert    my $outfh     = shift;
70243003dfeSmillert    my $cpan_file = shift;
70343003dfeSmillert    my $perl_file = shift;
70443003dfeSmillert    my $reverse   = shift;
70543003dfeSmillert    my $diff_opts = shift;
70643003dfeSmillert
70743003dfeSmillert    my @cmd = ( DIFF_CMD, split ' ', $diff_opts );
70843003dfeSmillert    if ($reverse) {
70943003dfeSmillert        push @cmd, $perl_file, $cpan_file;
71043003dfeSmillert    }
71143003dfeSmillert    else {
71243003dfeSmillert        push @cmd, $cpan_file, $perl_file;
71343003dfeSmillert    }
714898184e3Ssthen    return `@cmd`;
71543003dfeSmillert
71643003dfeSmillert}
71743003dfeSmillert
718898184e3Ssthensub customized {
719898184e3Ssthen    my ( $module_data, $file ) = @_;
720898184e3Ssthen    return grep { $file eq $_ } @{ $module_data->{CUSTOMIZED} };
721898184e3Ssthen}
72243003dfeSmillert
7236fb12b70Safresh1sub extract {
7246fb12b70Safresh1  my ($archive,$to) = @_;
7256fb12b70Safresh1  my $cwd = cwd();
7266fb12b70Safresh1  chdir $to or die "$!\n";
7276fb12b70Safresh1  my @files;
7286fb12b70Safresh1  EXTRACT: {
7296fb12b70Safresh1    local $Archive::Tar::CHOWN = 0;
7306fb12b70Safresh1    my $next;
7316fb12b70Safresh1    unless ( $next = Archive::Tar->iter( $archive, 1 ) ) {
7326fb12b70Safresh1       $! = $Archive::Tar::error;
7336fb12b70Safresh1       last EXTRACT;
7346fb12b70Safresh1    }
7356fb12b70Safresh1    while ( my $file = $next->() ) {
7366fb12b70Safresh1      push @files, $file->full_path;
7376fb12b70Safresh1      unless ( $file->extract ) {
7386fb12b70Safresh1        $! = $Archive::Tar::error;
7396fb12b70Safresh1        last EXTRACT;
7406fb12b70Safresh1      }
7416fb12b70Safresh1    }
7426fb12b70Safresh1  }
7436fb12b70Safresh1  my $path = __get_extract_dir( \@files );
7446fb12b70Safresh1  chdir $cwd or die "$!\n";
7456fb12b70Safresh1  return $path;
7466fb12b70Safresh1}
7476fb12b70Safresh1
7486fb12b70Safresh1sub __get_extract_dir {
7496fb12b70Safresh1    my $files   = shift || [];
7506fb12b70Safresh1
7516fb12b70Safresh1    return unless scalar @$files;
7526fb12b70Safresh1
7536fb12b70Safresh1    my($dir1, $dir2);
7546fb12b70Safresh1    for my $aref ( [ \$dir1, 0 ], [ \$dir2, -1 ] ) {
7556fb12b70Safresh1        my($dir,$pos) = @$aref;
7566fb12b70Safresh1
7576fb12b70Safresh1        ### add a catdir(), so that any trailing slashes get
7586fb12b70Safresh1        ### take care of (removed)
7596fb12b70Safresh1        ### also, a catdir() normalises './dir/foo' to 'dir/foo';
7606fb12b70Safresh1        ### which was the problem in bug #23999
7616fb12b70Safresh1        my $res = -d $files->[$pos]
7626fb12b70Safresh1                    ? File::Spec->catdir( $files->[$pos], '' )
7636fb12b70Safresh1                    : File::Spec->catdir( File::Basename::dirname( $files->[$pos] ) );
7646fb12b70Safresh1
7656fb12b70Safresh1        $$dir = $res;
7666fb12b70Safresh1    }
7676fb12b70Safresh1
7686fb12b70Safresh1    ### if the first and last dir don't match, make sure the
7696fb12b70Safresh1    ### dirname is not set wrongly
7706fb12b70Safresh1    my $dir;
7716fb12b70Safresh1
7726fb12b70Safresh1    ### dirs are the same, so we know for sure what the extract dir is
7736fb12b70Safresh1    if( $dir1 eq $dir2 ) {
7746fb12b70Safresh1        $dir = $dir1;
7756fb12b70Safresh1
7766fb12b70Safresh1    ### dirs are different.. do they share the base dir?
7776fb12b70Safresh1    ### if so, use that, if not, fall back to '.'
7786fb12b70Safresh1    } else {
7796fb12b70Safresh1        my $base1 = [ File::Spec->splitdir( $dir1 ) ]->[0];
7806fb12b70Safresh1        my $base2 = [ File::Spec->splitdir( $dir2 ) ]->[0];
7816fb12b70Safresh1
7826fb12b70Safresh1        $dir = File::Spec->rel2abs( $base1 eq $base2 ? $base1 : '.' );
7836fb12b70Safresh1    }
7846fb12b70Safresh1
7856fb12b70Safresh1    return File::Spec->rel2abs( $dir );
7866fb12b70Safresh1}
7876fb12b70Safresh1
78843003dfeSmillertrun();
78943003dfeSmillert
790