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