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