3 # core-cpan-diff: Compare CPAN modules with their equivalent in core
5 # Originally based on App::DualLivedDiff by Steffen Mueller.
17 use IO::Uncompress::Gunzip ();
19 use ExtUtils::Manifest;
21 BEGIN { die "Must be run from root of perl source tree\n" unless -d 'Porting' }
25 # if running from blead, we may be doing -Ilib, which means when we
26 # 'chdir /tmp/foo', Archive::Extract may not find Archive::Tar etc.
27 # So preload the things we need, and tell it to check %INC first:
32 $Module::Load::Conditional::CHECK_INC_HASH = 1;
33 # stop Archive::Extract whinging about lack of Archive::Zip
34 $Archive::Extract::WARN = 0;
37 # Files, which if they exist in CPAN but not in perl, will not generate
38 # an 'Only in CPAN' listing
40 our %IGNORABLE = map { ($_ => 1) }
41 qw(.cvsignore .dualLivedDiffConfig .gitignore
42 ANNOUNCE Announce Artistic AUTHORS BENCHMARK BUGS Build.PL
43 CHANGELOG ChangeLog CHANGES Changes COPYING Copying CREDITS
44 GOALS HISTORY INSTALL INSTALL.SKIP LICENSE Makefile.PL
45 MANIFEST MANIFEST.SKIP META.yml NEW NOTES ppport.h README
46 SIGNATURE THANKS TODO Todo VERSION WHATSNEW);
48 # where, under the cache dir, to untar stuff to
50 use constant UNTAR_DIR => 'untarred';
52 use constant DIFF_CMD => 'diff';
53 use constant WGET_CMD => 'wget';
56 print STDERR "\n@_\n\n" if @_;
58 Usage: $0 [opts] [ -d | -v | -x ] [ -a | module ... ]
60 -a/--all Scan all dual-life modules.
62 -c/--cachedir Where to save downloaded CPAN tarball files
63 (defaults to /tmp/something/ with deletion after each run).
65 -d/--diff Display file differences using diff(1), rather than just
66 listing which files have changed.
67 The diff(1) command is assumed to be in your PATH.
69 --diffopts Options to pass to the diff command. Defaults to '-u'.
71 -f|force Force download from CPAN of new 02packages.details.txt file
72 (with --crosscheck only).
74 -o/--output File name to write output to (defaults to STDOUT).
76 -r/--reverse Reverses the diff (perl to CPAN).
78 -v/--verbose List the fate of *all* files in the tarball, not just those
79 that differ or are missing.
81 -x|crosscheck List the distributions whose current CPAN version differs from
82 that in blead (i.e. the DISTRIBUTION field in Maintainers.pl).
84 By default (i.e. without the --crosscheck option), for each listed module
85 (or with -a, all CPAN modules listed in Maintainers.pl), grab the tarball
86 from CPAN associated with that module, and compare the files in it with
87 those in the perl source tree.
89 Must be run from the root of the perl source tree.
90 Module names must match the keys of %Modules in Maintainers.pl.
108 'a|all' => \$scan_all,
109 'c|cachedir=s' => \$cache_dir,
110 'd|diff' => \$use_diff,
111 'diffopts:s' => \$diff_opts,
112 'f|force' => \$force,
114 'o|output=s' => \$output_file,
115 'r|reverse' => \$reverse,
116 'v|verbose' => \$verbose,
117 'x|crosscheck' => \$do_crosscheck,
123 usage("Cannot mix -a with module list") if $scan_all && @ARGV;
125 if ($do_crosscheck) {
126 usage("can't use -r, -d, --diffopts, -v with --crosscheck")
127 if ($reverse || $use_diff || $diff_opts || $verbose);
130 $diff_opts = '-u' unless defined $diff_opts;
131 usage("can't use -f without --crosscheck") if $force;
135 ? grep $Maintainers::Modules{$_}{CPAN},
136 (sort {lc $a cmp lc $b } keys %Maintainers::Modules)
138 usage("No modules specified") unless @modules;
142 if (defined $output_file) {
143 open $outfh, '>', $output_file
144 or die "ERROR: could not open file '$output_file' for writing: $!";
147 open $outfh, ">&STDOUT"
148 or die "ERROR: can't dup STDOUT: $!";
151 if (defined $cache_dir) {
152 die "ERROR: no such directory: '$cache_dir'\n" unless -d $cache_dir;
155 if ($do_crosscheck) {
156 do_crosscheck($outfh, $cache_dir, $force, \@modules);
159 do_compare(\@modules, $outfh, $cache_dir, $verbose, $use_diff,
160 $reverse, $diff_opts);
166 # compare a list of modules against their CPAN equivalents
169 my ($modules, $outfh, $cache_dir, $verbose,
170 $use_diff, $reverse, $diff_opts) = @_;
173 # first, make sure we have a directory where they can all be untarred,
174 # and if its a permanent directory, clear any previous content
177 $untar_dir = File::Spec->catdir($cache_dir, UNTAR_DIR);
179 File::Path::rmtree($untar_dir)
180 or die "failed to remove $untar_dir\n";
183 or die "mkdir $untar_dir: $!\n";
186 $untar_dir = File::Temp::tempdir( CLEANUP => 1 );
189 my %ignorable = map { ($_ => 1) } @Maintainers::IGNORABLE;
192 for my $module (@$modules) {
193 print $outfh "\n$module\n" unless $use_diff;
195 my $m = $Maintainers::Modules{$module}
196 or die "ERROR: No such module in Maintainers.pl: '$module'\n";
198 unless ($m->{CPAN}) {
199 print $outfh "WARNING: $module is not dual-life; skipping\n";
203 my $dist = $m->{DISTRIBUTION};
204 die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $dist;
206 if ($seen_dist{$dist}) {
207 warn "WARNING: duplicate entry for $dist in $module\n"
211 my $cpan_dir = get_distribution($cache_dir, $untar_dir, $module, $dist);
214 my @perl_files = Maintainers::get_module_files($module);
216 my $manifest = File::Spec->catfile($cpan_dir, 'MANIFEST');
217 die "ERROR: no such file: $manifest\n" unless -f $manifest;
219 my $cpan_files = ExtUtils::Manifest::maniread($manifest);
220 my @cpan_files = sort keys %$cpan_files;
222 my ($excluded, $map) = get_map($m, $module, \@perl_files);
225 @perl_unseen{@perl_files} = ();
226 my %perl_files = %perl_unseen;
228 foreach my $cpan_file (@cpan_files) {
229 my $mapped_file = cpan_to_perl($excluded, $map, $cpan_file);
230 unless (defined $mapped_file) {
231 print $outfh " Excluded: $cpan_file\n" if $verbose;
235 if (exists $perl_files{$mapped_file}) {
236 delete $perl_unseen{$mapped_file};
239 # some CPAN files foo are stored in core as foo.packed,
240 # which are then unpacked by 'make test_prep'
241 my $packed_file = "$mapped_file.packed";
242 if (exists $perl_files{$packed_file} ) {
243 if (! -f $mapped_file and -f $packed_file) {
245 WARNING: $mapped_file not found, but .packed variant exists.
246 Perhaps you need to run 'make test_prep'?
250 delete $perl_unseen{$packed_file};
253 if ($ignorable{$cpan_file}) {
254 print $outfh " Ignored: $cpan_file\n" if $verbose;
259 print $outfh " CPAN only: $cpan_file",
260 ($cpan_file eq $mapped_file) ? "\n"
261 : " (expected $mapped_file)\n";
268 my $abs_cpan_file = File::Spec->catfile($cpan_dir, $cpan_file);
270 # should never happen
271 die "ERROR: can't find file $abs_cpan_file\n" unless -f $abs_cpan_file;
273 # might happen if the FILES entry in Maintainers.pl is wrong
274 unless (-f $mapped_file) {
275 print $outfh "WARNING: perl file not found: $mapped_file\n";
280 if (File::Compare::compare($abs_cpan_file, $mapped_file)) {
282 file_diff($outfh, $abs_cpan_file, $mapped_file,
283 $reverse, $diff_opts);
286 if ($cpan_file eq $mapped_file) {
287 print $outfh " Modified: $cpan_file\n";
290 print $outfh " Modified: $cpan_file $mapped_file\n";
295 if ($cpan_file eq $mapped_file) {
296 print $outfh " Unchanged: $cpan_file\n";
299 print $outfh " Unchanged: $cpan_file $mapped_file\n";
303 for (sort keys %perl_unseen) {
304 print $outfh " Perl only: $_\n" unless $use_diff;
309 # given FooBar-1.23_45.tar.gz, return FooBar
313 $d =~ s/\.tar\.gz$//;
315 $d =~ s/[\d\-_\.]+$//;
319 # process --crosscheck action:
320 # ie list all distributions whose CPAN versions differ from that listed in
324 my ($outfh, $cache_dir, $force, $modules) = @_;
326 my $file = '02packages.details.txt';
327 my $download_dir = $cache_dir || File::Temp::tempdir( CLEANUP => 1 );
328 my $path = File::Spec->catfile($download_dir, $file);
329 my $gzfile = "$path.gz";
331 # grab 02packages.details.txt
333 my $url = 'http://www.cpan.org/modules/02packages.details.txt.gz';
335 if (! -f $gzfile or $force) {
337 my_getstore($url, $gzfile);
340 IO::Uncompress::Gunzip::gunzip($gzfile, $path)
341 or die "ERROR: failed to ungzip $gzfile: $IO::Uncompress::Gunzip::GunzipError\n";
343 # suck in the data from it
345 open my $fh, '<', $path
346 or die "ERROR: open: $file: $!\n";
354 my @f = split ' ', $_;
356 warn "WARNING: $file:$.: line doesn't have three fields (skipping)\n";
359 $modules{$f[0]} = $f[2];
362 $distro =~ s{^.*/}{};
364 $distros{distro_base($distro)}{$distro} = 1;
367 for my $module (@$modules) {
368 my $m = $Maintainers::Modules{$module}
369 or die "ERROR: No such module in Maintainers.pl: '$module'\n";
371 unless ($m->{CPAN}) {
372 print $outfh "\nWARNING: $module is not dual-life; skipping\n";
378 # Foo::Bar 1.23 foo-bar-1.23.tar.gz,
379 # first compare the module name against Foo::Bar, and failing that,
382 my $pdist = $m->{DISTRIBUTION};
383 die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $pdist;
386 my $cdist = $modules{$module};
388 if (defined $cdist) {
392 my $d = $distros{distro_base($pdist)};
393 unless (defined $d) {
394 print $outfh "\n$module: Can't determine current CPAN entry\n";
398 print $outfh "\n$module: (found more than one CPAN candidate):\n";
399 print $outfh " perl: $pdist\n";
400 print $outfh " CPAN: $_\n" for sort keys %$d;
403 $cdist = (keys %$d)[0];
406 if ($cdist ne $pdist) {
407 print $outfh "\n$module:\n Perl: $pdist\n CPAN: $cdist\n";
414 # get the EXCLUDED and MAP entries for this module, or
415 # make up defauts if they don't exist
418 my ($m, $module_name, $perl_files) = @_;
420 my ($excluded, $map) = @$m{qw(EXCLUDED MAP)};
424 return $excluded, $map if $map;
426 # all files under ext/foo-bar (plus maybe some under t/lib)???
430 if (m{^(ext/[^/]+/)}) {
431 if (defined $ext and $ext ne $1) {
432 # more than one ext/$ext/
448 $map = { '' => $ext },
451 (my $base = $module_name) =~ s{::}{/}g;
458 return $excluded, $map;
462 # Given an exclude list and a mapping hash, convert a CPAN filename
463 # (eg 't/bar.t') to the equivalent perl filename (eg 'lib/Foo/Bar/t/bar.t').
464 # Returns an empty list for an excluded file
467 my ($excluded, $map, $cpan_file) = @_;
469 for my $exclude (@$excluded) {
470 # may be a simple string to match exactly, or a pattern
472 return if $cpan_file =~ $exclude;
475 return if $cpan_file eq $exclude;
479 my $perl_file = $cpan_file;
481 # try longest prefix first, then alphabetically on tie-break
482 for my $prefix (sort { length($b) <=> length($a) || $a cmp $b } keys %$map)
484 last if $perl_file =~ s/^\Q$prefix/$map->{$prefix}/;
491 # do LWP::Simple::getstore, possibly without LWP::Simple being available
493 my $lwp_simple_available;
496 my ($url, $file) = @_;
497 unless (defined $lwp_simple_available) {
498 eval { require LWP::Simple };
499 $lwp_simple_available = $@ eq '';
501 if ($lwp_simple_available) {
502 return LWP::Simple::is_success(LWP::Simple::getstore($url, $file));
505 return system(WGET_CMD, "-O", $file, $url) == 0;
510 # download and unpack a distribution
511 # Returns the full pathname of the extracted directory
512 # (eg '/tmp/XYZ/Foo_bar-1.23')
514 # cache_dir: where to dowenload the .tar.gz file to
515 # untar_dir: where to untar or unzup the file
516 # module: name of module
517 # dist: name of the distribution
519 sub get_distribution {
520 my ($cache_dir, $untar_dir, $module, $dist) = @_;
522 $dist =~ m{.+/([^/]+)$}
523 or die "ERROR: $module: invalid DISTRIBUTION name (no AUTHOR/ prefix): $dist";
526 my $download_dir = $cache_dir || File::Temp::tempdir( CLEANUP => 1 );
527 my $download_file = File::Spec->catfile($download_dir, $filename);
529 # download distribution
531 if (-f $download_file and ! -s $download_file ) {
532 # wget can leave a zero-length file on failed download
533 unlink $download_file;
536 unless (-f $download_file) {
538 $dist =~ /^([A-Z])([A-Z])/
539 or die "ERROR: $module: invalid DISTRIBUTION name (not /^[A-Z]{2}/): $dist";
541 my $url = "http://www.cpan.org/modules/by-authors/id/$1/$1$2/$dist";
542 my_getstore($url, $download_file)
543 or die "ERROR: Could not fetch '$url'";
546 # extract distribution
548 my $ae = Archive::Extract->new( archive => $download_file);
549 $ae->extract( to => $untar_dir )
550 or die "ERROR: failed to extract distribution '$download_file to temp. dir: " . $ae->error();
552 # get the name of the extracted distribution dir
554 my $path = File::Spec->catfile($untar_dir, $filename);
556 $path =~ s/\.tar\.gz$// or
557 $path =~ s/\.zip$// or
558 die "ERROR: downloaded file does not have a recognised suffix: $path\n";
560 die "ERROR: Extracted tarball does not appear as $path\n" unless -d $path;
566 # produce the diff of a single file
569 my $cpan_file = shift;
570 my $perl_file = shift;
572 my $diff_opts = shift;
575 my @cmd = (DIFF_CMD, split ' ', $diff_opts);
577 push @cmd, $perl_file, $cpan_file;
580 push @cmd, $cpan_file, $perl_file;
584 $result =~ s{^(--- |\+\+\+ )/tmp/[^/]+/}{$1}gm;
586 print $outfh $result;