This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
In taint.t, violates_taint() now tests more of the "insecure dependency" error.
[perl5.git] / Porting / core-cpan-diff
CommitLineData
cb097e7a
DM
1#!/usr/bin/env perl
2
3# core-cpan-diff: Compare CPAN modules with their equivalent in core
4
5# Originally based on App::DualLivedDiff by Steffen Mueller.
e66db76d 6
cb097e7a
DM
7use strict;
8use warnings;
9
10use 5.010;
11
12use Getopt::Long;
a08d2aad 13use File::Basename ();
333797b2
DG
14use File::Copy ();
15use File::Temp ();
16use File::Path ();
c4940a93 17use File::Spec::Functions;
cb097e7a
DM
18use Archive::Extract;
19use IO::Uncompress::Gunzip ();
333797b2 20use File::Compare ();
cb097e7a 21use ExtUtils::Manifest;
c4940a93 22use ExtUtils::MakeMaker ();
cb097e7a
DM
23
24BEGIN { die "Must be run from root of perl source tree\n" unless -d 'Porting' }
25use lib 'Porting';
26use Maintainers ();
27
28# if running from blead, we may be doing -Ilib, which means when we
29# 'chdir /tmp/foo', Archive::Extract may not find Archive::Tar etc.
30# So preload the things we need, and tell it to check %INC first:
31
32use Archive::Tar;
33use IPC::Open3;
34use IO::Select;
35$Module::Load::Conditional::CHECK_INC_HASH = 1;
333797b2 36
cb097e7a
DM
37# stop Archive::Extract whinging about lack of Archive::Zip
38$Archive::Extract::WARN = 0;
39
c4940a93 40# where, under the cache dir, to download tarballs to
333797b2
DG
41use constant SRC_DIR => 'tarballs';
42
cb097e7a 43# where, under the cache dir, to untar stuff to
cb097e7a
DM
44use constant UNTAR_DIR => 'untarred';
45
333797b2
DG
46use constant DIFF_CMD => 'diff';
47use constant WGET_CMD => 'wget';
cb097e7a
DM
48
49sub usage {
50 print STDERR "\n@_\n\n" if @_;
51 print STDERR <<HERE;
52Usage: $0 [opts] [ -d | -v | -x ] [ -a | module ... ]
53
54-a/--all Scan all dual-life modules.
55
56-c/--cachedir Where to save downloaded CPAN tarball files
57 (defaults to /tmp/something/ with deletion after each run).
58
59-d/--diff Display file differences using diff(1), rather than just
60 listing which files have changed.
61 The diff(1) command is assumed to be in your PATH.
62
63--diffopts Options to pass to the diff command. Defaults to '-u'.
64
65-f|force Force download from CPAN of new 02packages.details.txt file
66 (with --crosscheck only).
67
10be9a51
DG
68-m|mirror Preferred CPAN mirror URI (http:// or file:///)
69 (Local mirror must be a complete mirror, not minicpan)
70
cb097e7a
DM
71-o/--output File name to write output to (defaults to STDOUT).
72
73-r/--reverse Reverses the diff (perl to CPAN).
74
2908b263
RS
75-u/--upstream only print modules with the given upstream (defaults to all)
76
cb097e7a
DM
77-v/--verbose List the fate of *all* files in the tarball, not just those
78 that differ or are missing.
79
80-x|crosscheck List the distributions whose current CPAN version differs from
81 that in blead (i.e. the DISTRIBUTION field in Maintainers.pl).
82
83By default (i.e. without the --crosscheck option), for each listed module
84(or with -a, all CPAN modules listed in Maintainers.pl), grab the tarball
85from CPAN associated with that module, and compare the files in it with
86those in the perl source tree.
87
88Must be run from the root of the perl source tree.
89Module names must match the keys of %Modules in Maintainers.pl.
90HERE
91 exit(1);
92}
93
cb097e7a
DM
94sub run {
95 my $scan_all;
96 my $diff_opts;
333797b2 97 my $reverse = 0;
2908b263 98 my @wanted_upstreams;
cb097e7a 99 my $cache_dir;
10be9a51 100 my $mirror_url = "http://www.cpan.org/";
cb097e7a
DM
101 my $use_diff;
102 my $output_file;
103 my $verbose;
104 my $force;
105 my $do_crosscheck;
106
107 GetOptions(
333797b2
DG
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,
113 'h|help' => \&usage,
114 'm|mirror=s' => \$mirror_url,
115 'o|output=s' => \$output_file,
116 'r|reverse' => \$reverse,
117 'u|upstream=s@' => \@wanted_upstreams,
118 'v|verbose' => \$verbose,
119 'x|crosscheck' => \$do_crosscheck,
cb097e7a
DM
120 ) or usage;
121
cb097e7a
DM
122 my @modules;
123
124 usage("Cannot mix -a with module list") if $scan_all && @ARGV;
125
126 if ($do_crosscheck) {
333797b2
DG
127 usage("can't use -r, -d, --diffopts, -v with --crosscheck")
128 if ( $reverse || $use_diff || $diff_opts || $verbose );
cb097e7a
DM
129 }
130 else {
5f92f74f 131 $diff_opts = '-u -b' unless defined $diff_opts;
333797b2 132 usage("can't use -f without --crosscheck") if $force;
cb097e7a
DM
133 }
134
333797b2
DG
135 @modules =
136 $scan_all
137 ? grep $Maintainers::Modules{$_}{CPAN},
138 ( sort { lc $a cmp lc $b } keys %Maintainers::Modules )
139 : @ARGV;
cb097e7a
DM
140 usage("No modules specified") unless @modules;
141
cb097e7a 142 my $outfh;
333797b2
DG
143 if ( defined $output_file ) {
144 open $outfh, '>', $output_file
145 or die "ERROR: could not open file '$output_file' for writing: $!\n";
cb097e7a
DM
146 }
147 else {
333797b2
DG
148 open $outfh, ">&STDOUT"
149 or die "ERROR: can't dup STDOUT: $!\n";
cb097e7a
DM
150 }
151
333797b2
DG
152 if ( defined $cache_dir ) {
153 die "ERROR: no such directory: '$cache_dir'\n" unless -d $cache_dir;
cb097e7a 154 }
c4940a93 155 else {
333797b2 156 $cache_dir = File::Temp::tempdir( CLEANUP => 1 );
c4940a93 157 }
cb097e7a 158
333797b2 159 $mirror_url .= "/" unless substr( $mirror_url, -1 ) eq "/";
10be9a51 160 my $test_file = "modules/07mirror.yml";
333797b2
DG
161 my_getstore(
162 cpan_url( $mirror_url, $test_file ),
163 catfile( $cache_dir, $test_file )
164 ) or die "ERROR: not a CPAN mirror '$mirror_url'\n";
10be9a51 165
cb097e7a 166 if ($do_crosscheck) {
333797b2 167 do_crosscheck( $outfh, $cache_dir, $mirror_url, $force, \@modules );
cb097e7a
DM
168 }
169 else {
333797b2
DG
170 do_compare(
171 \@modules, $outfh, $output_file,
172 $cache_dir, $mirror_url, $verbose,
173 $use_diff, $reverse, $diff_opts,
174 \@wanted_upstreams
175 );
cb097e7a
DM
176 }
177}
178
10be9a51 179# construct a CPAN url
cb097e7a 180
10be9a51 181sub cpan_url {
333797b2 182 my ( $mirror_url, @path ) = @_;
10be9a51
DG
183 return $mirror_url unless @path;
184 my $cpan_path = join( "/", map { split "/", $_ } @path );
333797b2 185 $cpan_path =~ s{\A/}{}; # remove leading slash since url has one trailing
10be9a51
DG
186 return $mirror_url . $cpan_path;
187}
cb097e7a
DM
188
189# compare a list of modules against their CPAN equivalents
190
191sub do_compare {
333797b2
DG
192 my (
193 $modules, $outfh, $output_file, $cache_dir,
194 $mirror_url, $verbose, $use_diff, $reverse,
195 $diff_opts, $wanted_upstreams
196 ) = @_;
cb097e7a
DM
197
198 # first, make sure we have a directory where they can all be untarred,
199 # and if its a permanent directory, clear any previous content
333797b2
DG
200 my $untar_dir = catdir( $cache_dir, UNTAR_DIR );
201 my $src_dir = catdir( $cache_dir, SRC_DIR );
c4940a93 202 for my $d ( $src_dir, $untar_dir ) {
333797b2
DG
203 next if -d $d;
204 mkdir $d or die "mkdir $d: $!\n";
cb097e7a
DM
205 }
206
333797b2 207 my %ignorable = map { ( $_ => 1 ) } @Maintainers::IGNORABLE;
cb097e7a
DM
208
209 my %seen_dist;
210 for my $module (@$modules) {
333797b2 211 warn "Processing $module ...\n" if defined $output_file;
cb097e7a 212
333797b2
DG
213 my $m = $Maintainers::Modules{$module}
214 or die "ERROR: No such module in Maintainers.pl: '$module'\n";
cb097e7a 215
333797b2
DG
216 unless ( $m->{CPAN} ) {
217 print $outfh "WARNING: $module is not dual-life; skipping\n";
218 next;
219 }
cb097e7a 220
333797b2
DG
221 my $dist = $m->{DISTRIBUTION};
222 die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $dist;
cb097e7a 223
333797b2
DG
224 if ( $seen_dist{$dist}++ ) {
225 warn "WARNING: duplicate entry for $dist in $module\n";
226 }
d55832d0 227
333797b2
DG
228 my $upstream = $m->{UPSTREAM} || 'UNKNOWN';
229 next if @$wanted_upstreams and !( $upstream ~~ $wanted_upstreams );
d55832d0 230
333797b2
DG
231 print $outfh "\n$module - "
232 . $Maintainers::Modules{$module}->{DISTRIBUTION} . "\n";
233 print $outfh " upstream is: "
234 . ( $m->{UPSTREAM} || 'UNKNOWN!' ) . "\n";
cb097e7a 235
333797b2
DG
236 my $cpan_dir;
237 eval {
238 $cpan_dir =
239 get_distribution( $src_dir, $mirror_url, $untar_dir, $module,
240 $dist );
241 };
242 if ($@) {
243 print $outfh " ", $@;
244 print $outfh " (skipping)\n";
245 next;
246 }
cb097e7a 247
333797b2 248 my @perl_files = Maintainers::get_module_files($module);
cb097e7a 249
333797b2
DG
250 my $manifest = catfile( $cpan_dir, 'MANIFEST' );
251 die "ERROR: no such file: $manifest\n" unless -f $manifest;
cb097e7a 252
333797b2
DG
253 my $cpan_files = ExtUtils::Manifest::maniread($manifest);
254 my @cpan_files = sort keys %$cpan_files;
cb097e7a 255
333797b2 256 ( my $main_pm = $module ) =~ s{::}{/}g;
c4940a93
DG
257 $main_pm .= ".pm";
258
7134fed3
DG
259 my ( $excluded, $map, $customized ) =
260 get_map( $m, $module, \@perl_files );
333797b2
DG
261
262 my %perl_unseen;
263 @perl_unseen{@perl_files} = ();
264 my %perl_files = %perl_unseen;
265
266 foreach my $cpan_file (@cpan_files) {
7134fed3
DG
267 my $mapped_file =
268 cpan_to_perl( $excluded, $map, $customized, $cpan_file );
333797b2
DG
269 unless ( defined $mapped_file ) {
270 print $outfh " Excluded: $cpan_file\n" if $verbose;
271 next;
272 }
273
274 if ( exists $perl_files{$mapped_file} ) {
275 delete $perl_unseen{$mapped_file};
276 }
277 else {
278
279 # some CPAN files foo are stored in core as foo.packed,
280 # which are then unpacked by 'make test_prep'
281 my $packed_file = "$mapped_file.packed";
282 if ( exists $perl_files{$packed_file} ) {
283 if ( !-f $mapped_file and -f $packed_file ) {
284 print $outfh <<EOF;
cb097e7a
DM
285WARNING: $mapped_file not found, but .packed variant exists.
286Perhaps you need to run 'make test_prep'?
287EOF
333797b2
DG
288 next;
289 }
290 delete $perl_unseen{$packed_file};
291 }
292 else {
293 if ( $ignorable{$cpan_file} ) {
294 print $outfh " Ignored: $cpan_file\n" if $verbose;
295 next;
296 }
297
298 unless ($use_diff) {
299 print $outfh " CPAN only: $cpan_file",
300 ( $cpan_file eq $mapped_file )
301 ? "\n"
302 : " (expected $mapped_file)\n";
303 }
304 next;
305 }
306 }
307
308 my $abs_cpan_file = catfile( $cpan_dir, $cpan_file );
309
310 # should never happen
311 die "ERROR: can't find file $abs_cpan_file\n"
312 unless -f $abs_cpan_file;
313
314 # might happen if the FILES entry in Maintainers.pl is wrong
315 unless ( -f $mapped_file ) {
316 print $outfh "WARNING: perl file not found: $mapped_file\n";
317 next;
318 }
cb097e7a 319
6ec9eada 320 my $relative_mapped_file = relatively_mapped($mapped_file);
c4940a93 321
7134fed3 322 my $different =
5f92f74f
DG
323 file_diff( $outfh, $abs_cpan_file, $mapped_file, $reverse,
324 $diff_opts );
7134fed3 325 if ( $different && customized( $m, $relative_mapped_file ) ) {
4ba81d11
DG
326 if ($verbose) {
327 print $outfh " Customized: $relative_mapped_file\n";
328 }
329 }
7134fed3 330 elsif ($different) {
333797b2 331 if ($use_diff) {
5f92f74f
DG
332 $different =~ s{^(--- |\+\+\+ )/tmp/[^/]+/}{$1}gm;
333 print $outfh $different;
333797b2
DG
334 }
335 else {
336 if ( $cpan_file eq $relative_mapped_file ) {
337 print $outfh " Modified: $relative_mapped_file\n";
338 }
339 else {
340 print $outfh
341 " Modified: $cpan_file $relative_mapped_file\n";
342 }
7134fed3
DG
343
344 if ( $cpan_file =~ m{\.pm\z} ) {
345 my $pv = MM->parse_version($mapped_file) || 'unknown';
346 my $cv = MM->parse_version($abs_cpan_file) || 'unknown';
347 if ( $pv ne $cv ) {
348 print $outfh
349" Version mismatch in '$cpan_file':\n $cv (cpan) vs $pv (perl)\n";
350 }
351 }
352
333797b2
DG
353 }
354 }
355 elsif ($verbose) {
356 if ( $cpan_file eq $relative_mapped_file ) {
357 print $outfh " Unchanged: $cpan_file\n";
358 }
359 else {
360 print $outfh
361 " Unchanged: $cpan_file $relative_mapped_file\n";
362 }
363 }
364 }
365 for ( sort keys %perl_unseen ) {
6ec9eada
DG
366 my $relative_mapped_file = relatively_mapped($_);
367 if ( customized( $m, $relative_mapped_file ) ) {
368 print $outfh " Customized: $_\n";
369 }
370 else {
371 print $outfh " Perl only: $_\n" unless $use_diff;
372 }
333797b2 373 }
cb097e7a
DM
374 }
375}
376
6ec9eada
DG
377sub relatively_mapped {
378 my $relative = shift;
379 $relative =~ s/^(cpan|dist|ext)\/.*?\///;
380 return $relative;
381}
382
cb097e7a
DM
383# given FooBar-1.23_45.tar.gz, return FooBar
384
385sub distro_base {
386 my $d = shift;
387 $d =~ s/\.tar\.gz$//;
388 $d =~ s/\.gip$//;
389 $d =~ s/[\d\-_\.]+$//;
390 return $d;
391}
392
393# process --crosscheck action:
394# ie list all distributions whose CPAN versions differ from that listed in
395# Maintainers.pl
396
397sub do_crosscheck {
333797b2 398 my ( $outfh, $cache_dir, $mirror_url, $force, $modules ) = @_;
cb097e7a 399
333797b2 400 my $file = '02packages.details.txt';
cb097e7a 401 my $download_dir = $cache_dir || File::Temp::tempdir( CLEANUP => 1 );
333797b2
DG
402 my $path = catfile( $download_dir, $file );
403 my $gzfile = "$path.gz";
cb097e7a
DM
404
405 # grab 02packages.details.txt
406
333797b2 407 my $url = cpan_url( $mirror_url, "modules/02packages.details.txt.gz" );
cb097e7a 408
333797b2
DG
409 if ( !-f $gzfile or $force ) {
410 unlink $gzfile;
411 my_getstore( $url, $gzfile );
cb097e7a
DM
412 }
413 unlink $path;
333797b2
DG
414 IO::Uncompress::Gunzip::gunzip( $gzfile, $path )
415 or die
416 "ERROR: failed to ungzip $gzfile: $IO::Uncompress::Gunzip::GunzipError\n";
cb097e7a
DM
417
418 # suck in the data from it
e66db76d 419
cb097e7a 420 open my $fh, '<', $path
333797b2 421 or die "ERROR: open: $file: $!\n";
cb097e7a
DM
422
423 my %distros;
424 my %modules;
425
426 while (<$fh>) {
333797b2
DG
427 next if 1 .. /^$/;
428 chomp;
429 my @f = split ' ', $_;
430 if ( @f != 3 ) {
431 warn
432 "WARNING: $file:$.: line doesn't have three fields (skipping)\n";
433 next;
434 }
435 my $distro = $f[2];
436 $distro =~ s{^[A-Z]/[A-Z]{2}/}{}; # strip leading A/AB/
437 $modules{ $f[0] } = $distro;
438
439 ( my $short_distro = $distro ) =~ s{^.*/}{};
440
441 $distros{ distro_base($short_distro) }{$distro} = 1;
cb097e7a
DM
442 }
443
444 for my $module (@$modules) {
333797b2
DG
445 my $m = $Maintainers::Modules{$module}
446 or die "ERROR: No such module in Maintainers.pl: '$module'\n";
447
448 unless ( $m->{CPAN} ) {
449 print $outfh "\nWARNING: $module is not dual-life; skipping\n";
450 next;
451 }
452
453 # given an entry like
454 # Foo::Bar 1.23 foo-bar-1.23.tar.gz,
455 # first compare the module name against Foo::Bar, and failing that,
456 # against foo-bar
457
458 my $pdist = $m->{DISTRIBUTION};
459 die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $pdist;
460
461 my $cdist = $modules{$module};
462 ( my $short_pdist = $pdist ) =~ s{^.*/}{};
463
464 unless ( defined $cdist ) {
465 my $d = $distros{ distro_base($short_pdist) };
466 unless ( defined $d ) {
467 print $outfh "\n$module: Can't determine current CPAN entry\n";
468 next;
469 }
470 if ( keys %$d > 1 ) {
471 print $outfh
472 "\n$module: (found more than one CPAN candidate):\n";
473 print $outfh " perl: $pdist\n";
474 print $outfh " CPAN: $_\n" for sort keys %$d;
475 next;
476 }
477 $cdist = ( keys %$d )[0];
478 }
479
480 if ( $cdist ne $pdist ) {
481 print $outfh "\n$module:\n Perl: $pdist\n CPAN: $cdist\n";
482 }
cb097e7a
DM
483 }
484}
485
cb097e7a
DM
486# get the EXCLUDED and MAP entries for this module, or
487# make up defauts if they don't exist
488
489sub get_map {
333797b2 490 my ( $m, $module_name, $perl_files ) = @_;
cb097e7a 491
4ba81d11 492 my ( $excluded, $map, $customized ) = @$m{qw(EXCLUDED MAP CUSTOMIZED)};
cb097e7a 493
7134fed3 494 $excluded ||= [];
4ba81d11 495 $customized ||= [];
cb097e7a 496
4ba81d11 497 return $excluded, $map, $customized if $map;
cb097e7a
DM
498
499 # all files under ext/foo-bar (plus maybe some under t/lib)???
500
501 my $ext;
502 for (@$perl_files) {
333797b2
DG
503 if (m{^((?:ext|dist|cpan)/[^/]+/)}) {
504 if ( defined $ext and $ext ne $1 ) {
505
506 # more than one ext/$ext/
507 undef $ext;
508 last;
509 }
510 $ext = $1;
511 }
512 elsif (m{^t/lib/}) {
513 next;
514 }
515 else {
516 undef $ext;
517 last;
518 }
cb097e7a 519 }
e66db76d 520
333797b2
DG
521 if ( defined $ext ) {
522 $map = { '' => $ext },;
cb097e7a
DM
523 }
524 else {
333797b2
DG
525 ( my $base = $module_name ) =~ s{::}{/}g;
526 $base = "lib/$base";
527 $map = {
528 'lib/' => 'lib/',
529 '' => "$base/",
530 };
cb097e7a 531 }
4ba81d11 532 return $excluded, $map, $customized;
cb097e7a
DM
533}
534
cb097e7a
DM
535# Given an exclude list and a mapping hash, convert a CPAN filename
536# (eg 't/bar.t') to the equivalent perl filename (eg 'lib/Foo/Bar/t/bar.t').
537# Returns an empty list for an excluded file
538
539sub cpan_to_perl {
4ba81d11 540 my ( $excluded, $map, $customized, $cpan_file ) = @_;
cb097e7a
DM
541
542 for my $exclude (@$excluded) {
4ba81d11 543 next if $exclude ~~ $customized;
7134fed3 544
333797b2
DG
545 # may be a simple string to match exactly, or a pattern
546 if ( ref $exclude ) {
547 return if $cpan_file =~ $exclude;
548 }
549 else {
550 return if $cpan_file eq $exclude;
551 }
cb097e7a
DM
552 }
553
554 my $perl_file = $cpan_file;
555
556 # try longest prefix first, then alphabetically on tie-break
333797b2
DG
557 for
558 my $prefix ( sort { length($b) <=> length($a) || $a cmp $b } keys %$map )
cb097e7a 559 {
333797b2 560 last if $perl_file =~ s/^\Q$prefix/$map->{$prefix}/;
cb097e7a
DM
561 }
562 return $perl_file;
563}
564
cb097e7a
DM
565# do LWP::Simple::getstore, possibly without LWP::Simple being available
566
567my $lwp_simple_available;
568
569sub my_getstore {
333797b2
DG
570 my ( $url, $file ) = @_;
571 File::Path::mkpath( File::Basename::dirname($file) );
572 unless ( defined $lwp_simple_available ) {
573 eval { require LWP::Simple };
574 $lwp_simple_available = $@ eq '';
cb097e7a
DM
575 }
576 if ($lwp_simple_available) {
333797b2 577 return LWP::Simple::is_success( LWP::Simple::getstore( $url, $file ) );
cb097e7a 578 }
a08d2aad 579 elsif ( $url =~ qr{\Afile://(?:localhost)?/} ) {
333797b2 580 ( my $local_path = $url ) =~ s{\Afile://(?:localhost)?}{};
a08d2aad
DG
581 File::Copy::copy( $local_path, $file );
582 }
cb097e7a 583 else {
333797b2 584 return system( WGET_CMD, "-O", $file, $url ) == 0;
cb097e7a
DM
585 }
586}
587
cb097e7a
DM
588# download and unpack a distribution
589# Returns the full pathname of the extracted directory
590# (eg '/tmp/XYZ/Foo_bar-1.23')
591
10be9a51
DG
592# cache_dir: where to download the .tar.gz file to
593# mirror_url: CPAN mirror to download from
e66db76d 594# untar_dir: where to untar or unzup the file
10be9a51
DG
595# module: name of module
596# dist: name of the distribution
cb097e7a
DM
597
598sub get_distribution {
333797b2 599 my ( $src_dir, $mirror_url, $untar_dir, $module, $dist ) = @_;
cb097e7a
DM
600
601 $dist =~ m{.+/([^/]+)$}
333797b2
DG
602 or die
603 "ERROR: $module: invalid DISTRIBUTION name (no AUTHOR/ prefix): $dist\n";
cb097e7a
DM
604 my $filename = $1;
605
333797b2 606 my $download_file = catfile( $src_dir, $filename );
cb097e7a
DM
607
608 # download distribution
609
333797b2
DG
610 if ( -f $download_file and !-s $download_file ) {
611
612 # wget can leave a zero-length file on failed download
613 unlink $download_file;
cb097e7a
DM
614 }
615
333797b2 616 unless ( -f $download_file ) {
cb097e7a 617
333797b2
DG
618 # not cached
619 $dist =~ /^([A-Z])([A-Z])/
620 or die
621"ERROR: $module: invalid DISTRIBUTION name (not /^[A-Z]{2}/): $dist\n";
622
623 my $url =
624 cpan_url( $mirror_url, "modules/by-authors/id/$1/$1$2/$dist" );
625 my_getstore( $url, $download_file )
626 or die "ERROR: Could not fetch '$url'\n";
cb097e7a
DM
627 }
628
c4940a93 629 # get the expected name of the extracted distribution dir
cb097e7a 630
333797b2 631 my $path = catfile( $untar_dir, $filename );
cb097e7a 632
333797b2
DG
633 $path =~ s/\.tar\.gz$//
634 or $path =~ s/\.zip$//
635 or die
636 "ERROR: downloaded file does not have a recognised suffix: $path\n";
cb097e7a 637
c4940a93 638 # extract it unless we already have it cached or tarball is newer
333797b2
DG
639 if ( !-d $path || ( -M $download_file < -M $path ) ) {
640 my $ae = Archive::Extract->new( archive => $download_file );
641 $ae->extract( to => $untar_dir )
642 or die
643 "ERROR: failed to extract distribution '$download_file to temp. dir: "
644 . $ae->error() . "\n";
45c9b306
FR
645
646 $path = $ae->extract_path;
c4940a93
DG
647 }
648
cb097e7a
DM
649 die "ERROR: Extracted tarball does not appear as $path\n" unless -d $path;
650
651 return $path;
652}
653
cb097e7a
DM
654# produce the diff of a single file
655sub file_diff {
656 my $outfh = shift;
657 my $cpan_file = shift;
658 my $perl_file = shift;
659 my $reverse = shift;
660 my $diff_opts = shift;
661
333797b2 662 my @cmd = ( DIFF_CMD, split ' ', $diff_opts );
cb097e7a 663 if ($reverse) {
333797b2 664 push @cmd, $perl_file, $cpan_file;
cb097e7a
DM
665 }
666 else {
333797b2 667 push @cmd, $cpan_file, $perl_file;
cb097e7a 668 }
5f92f74f 669 return `@cmd`;
cb097e7a 670
cb097e7a
DM
671}
672
4ba81d11 673sub customized {
7134fed3
DG
674 my ( $module_data, $file ) = @_;
675 return grep { $file eq $_ } @{ $module_data->{CUSTOMIZED} };
4ba81d11
DG
676}
677
cb097e7a
DM
678run();
679