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 qw(:config bundling); 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 --text' 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 --text' unless defined $diff_opts; 130 if (! defined $diff_opts) { 131 $diff_opts = ($^O =~ m/bsd$/i) ? '-u' : '-u --text'; 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 my $tail_pat = qr/\.(?:tar\.(?:g?z|bz2|Z)|zip|tgz|tbz)/; 439 $d =~ s{-v?([0-9._]+(?:-TRIAL[0-9]*)?)$tail_pat\z}{}; 440 return $d; 441} 442 443# process --crosscheck action: 444# ie list all distributions whose CPAN versions differ from that listed in 445# Maintainers.pl 446 447sub do_crosscheck { 448 my ( 449 $outfh, $cache_dir, $mirror_url, $verbose, 450 $force, $modules, $wanted_upstreams, 451 ) = @_; 452 453 my $file = '02packages.details.txt'; 454 my $download_dir = $cache_dir || File::Temp::tempdir( CLEANUP => 1 ); 455 my $path = catfile( $download_dir, $file ); 456 my $gzfile = "$path.gz"; 457 458 # grab 02packages.details.txt 459 460 my $url = cpan_url( $mirror_url, "modules/02packages.details.txt.gz" ); 461 462 if ( !-f $gzfile or $force ) { 463 unlink $gzfile; 464 my_getstore( $url, $gzfile ); 465 } 466 unlink $path; 467 IO::Uncompress::Gunzip::gunzip( $gzfile, $path ) 468 or die 469 "ERROR: failed to ungzip $gzfile: $IO::Uncompress::Gunzip::GunzipError\n"; 470 471 # suck in the data from it 472 473 open my $fh, '<', $path 474 or die "ERROR: open: $file: $!\n"; 475 476 my %distros; 477 my %modules; 478 479 while (<$fh>) { 480 next if 1 .. /^$/; 481 chomp; 482 my @f = split ' ', $_; 483 if ( @f != 3 ) { 484 warn 485 "WARNING: $file:$.: line doesn't have three fields (skipping)\n"; 486 next; 487 } 488 my $distro = $f[2]; 489 $distro =~ s{^[A-Z]/[A-Z]{2}/}{}; # strip leading A/AB/ 490 $modules{ $f[0] } = $distro; 491 492 ( my $short_distro = $distro ) =~ s{^.*/}{}; 493 494 $distros{ distro_base($short_distro) }{$distro} = 1; 495 } 496 497 my %wanted_upstream = map { ( $_ => 1 ) } @$wanted_upstreams; 498 for my $module (@$modules) { 499 my $m = $Maintainers::Modules{$module} 500 or die "ERROR: No such module in Maintainers.pl: '$module'\n"; 501 502 $verbose and warn "Checking $module\n"; 503 504 unless ( $m->{CPAN} ) { 505 print $outfh "\nWARNING: $module is not dual-life; skipping\n"; 506 next; 507 } 508 509 # given an entry like 510 # Foo::Bar 1.23 foo-bar-1.23.tar.gz, 511 # first compare the module name against Foo::Bar, and failing that, 512 # against foo-bar 513 514 my $pdist = $m->{DISTRIBUTION}; 515 die "ERROR: $module has no DISTRIBUTION entry\n" unless defined $pdist; 516 517 my $upstream = $m->{UPSTREAM} // 'undef'; 518 next if @$wanted_upstreams and !$wanted_upstream{$upstream}; 519 520 my $cdist = $modules{$module}; 521 ( my $short_pdist = $pdist ) =~ s{^.*/}{}; 522 523 unless ( defined $cdist ) { 524 my $d = $distros{ distro_base($short_pdist) }; 525 unless ( defined $d ) { 526 print $outfh "\n$module: Can't determine current CPAN entry\n"; 527 next; 528 } 529 if ( keys %$d > 1 ) { 530 print $outfh 531 "\n$module: (found more than one CPAN candidate):\n"; 532 print $outfh " Perl: $pdist\n"; 533 print $outfh " CPAN: $_\n" for sort keys %$d; 534 next; 535 } 536 $cdist = ( keys %$d )[0]; 537 } 538 539 if ( $cdist ne $pdist ) { 540 print $outfh "\n$module:\n Perl: $pdist\n CPAN: $cdist\n"; 541 } 542 } 543} 544 545# get the EXCLUDED and MAP entries for this module, or 546# make up defaults if they don't exist 547 548sub get_map { 549 my ( $m, $module_name, $perl_files ) = @_; 550 551 my ( $excluded, $map, $customized ) = @$m{qw(EXCLUDED MAP CUSTOMIZED)}; 552 553 $excluded ||= []; 554 $customized ||= []; 555 556 return $excluded, $map, $customized if $map; 557 558 # all files under ext/foo-bar (plus maybe some under t/lib)??? 559 560 my $ext; 561 for (@$perl_files) { 562 if (m{^((?:ext|dist|cpan)/[^/]+/)}) { 563 if ( defined $ext and $ext ne $1 ) { 564 565 # more than one ext/$ext/ 566 undef $ext; 567 last; 568 } 569 $ext = $1; 570 } 571 elsif (m{^t/lib/}) { 572 next; 573 } 574 else { 575 undef $ext; 576 last; 577 } 578 } 579 580 if ( defined $ext ) { 581 $map = { '' => $ext },; 582 } 583 else { 584 ( my $base = $module_name ) =~ s{::}{/}g; 585 $base = "lib/$base"; 586 $map = { 587 'lib/' => 'lib/', 588 '' => "$base/", 589 }; 590 } 591 return $excluded, $map, $customized; 592} 593 594# Given an exclude list and a mapping hash, convert a CPAN filename 595# (eg 't/bar.t') to the equivalent perl filename (eg 'lib/Foo/Bar/t/bar.t'). 596# Returns an empty list for an excluded file 597 598sub cpan_to_perl { 599 my ( $excluded, $map, $customized, $cpan_file ) = @_; 600 601 my %customized = map { ( $_ => 1 ) } @$customized; 602 for my $exclude (@$excluded) { 603 next if $customized{$exclude}; 604 605 # may be a simple string to match exactly, or a pattern 606 if ( ref $exclude ) { 607 return if $cpan_file =~ $exclude; 608 } 609 else { 610 return if $cpan_file eq $exclude; 611 } 612 } 613 614 my $perl_file = $cpan_file; 615 616 # try longest prefix first, then alphabetically on tie-break 617 for 618 my $prefix ( sort { length($b) <=> length($a) || $a cmp $b } keys %$map ) 619 { 620 last if $perl_file =~ s/^\Q$prefix/$map->{$prefix}/; 621 } 622 return $perl_file; 623} 624 625# fetch a file from a URL and store it in a file given by a filename 626 627sub my_getstore { 628 my ( $url, $file ) = @_; 629 File::Path::mkpath( File::Basename::dirname($file) ); 630 if ( $url =~ qr{\Afile://(?:localhost)?/} ) { 631 ( my $local_path = $url ) =~ s{\Afile://(?:localhost)?}{}; 632 File::Copy::copy( $local_path, $file ); 633 } else { 634 my $http = HTTP::Tiny->new; 635 my $response = $http->mirror($url, $file); 636 return $response->{success}; 637 } 638} 639 640# download and unpack a distribution 641# Returns the full pathname of the extracted directory 642# (eg '/tmp/XYZ/Foo_bar-1.23') 643 644# cache_dir: where to download the .tar.gz file to 645# mirror_url: CPAN mirror to download from 646# untar_dir: where to untar or unzup the file 647# module: name of module 648# dist: name of the distribution 649 650sub get_distribution { 651 my ( $src_dir, $mirror_url, $untar_dir, $module, $dist ) = @_; 652 653 $dist =~ m{.+/([^/]+)$} 654 or die 655 "ERROR: $module: invalid DISTRIBUTION name (no AUTHOR/ prefix): $dist\n"; 656 my $filename = $1; 657 658 my $download_file = catfile( $src_dir, $filename ); 659 660 # download distribution 661 662 if ( -f $download_file and !-s $download_file ) { 663 664 # failed download might leave a zero-length file 665 unlink $download_file; 666 } 667 668 unless ( -f $download_file ) { 669 670 # not cached 671 my $url = cpan_url_distribution( $mirror_url, $dist ); 672 my_getstore( $url, $download_file ) 673 or die "ERROR: Could not fetch '$url'\n"; 674 } 675 676 # get the expected name of the extracted distribution dir 677 678 my $path = catfile( $untar_dir, $filename ); 679 680 $path =~ s/\.tar\.gz$// 681 or $path =~ s/\.tgz$// 682 or $path =~ s/\.zip$// 683 or die 684 "ERROR: downloaded file does not have a recognised suffix: $path\n"; 685 686 # extract it unless we already have it cached or tarball is newer 687 if ( !-d $path || ( -M $download_file < -M $path ) ) { 688 $path = extract( $download_file, $untar_dir ) 689 or die 690 "ERROR: failed to extract distribution '$download_file to temp. dir: " 691 . $! . "\n"; 692 } 693 694 die "ERROR: Extracted tarball does not appear as $path\n" unless -d $path; 695 696 return $path; 697} 698 699# produce the diff of a single file 700sub file_diff { 701 my $outfh = shift; 702 my $cpan_file = shift; 703 my $perl_file = shift; 704 my $reverse = shift; 705 my $diff_opts = shift; 706 707 my @cmd = ( DIFF_CMD, split ' ', $diff_opts ); 708 if ($reverse) { 709 push @cmd, $perl_file, $cpan_file; 710 } 711 else { 712 push @cmd, $cpan_file, $perl_file; 713 } 714 return `@cmd`; 715 716} 717 718sub customized { 719 my ( $module_data, $file ) = @_; 720 return grep { $file eq $_ } @{ $module_data->{CUSTOMIZED} }; 721} 722 723sub extract { 724 my ($archive,$to) = @_; 725 my $cwd = cwd(); 726 chdir $to or die "$!\n"; 727 my @files; 728 EXTRACT: { 729 local $Archive::Tar::CHOWN = 0; 730 my $next; 731 unless ( $next = Archive::Tar->iter( $archive, 1 ) ) { 732 $! = $Archive::Tar::error; 733 last EXTRACT; 734 } 735 while ( my $file = $next->() ) { 736 push @files, $file->full_path; 737 unless ( $file->extract ) { 738 $! = $Archive::Tar::error; 739 last EXTRACT; 740 } 741 } 742 } 743 my $path = __get_extract_dir( \@files ); 744 chdir $cwd or die "$!\n"; 745 return $path; 746} 747 748sub __get_extract_dir { 749 my $files = shift || []; 750 751 return unless scalar @$files; 752 753 my($dir1, $dir2); 754 for my $aref ( [ \$dir1, 0 ], [ \$dir2, -1 ] ) { 755 my($dir,$pos) = @$aref; 756 757 ### add a catdir(), so that any trailing slashes get 758 ### take care of (removed) 759 ### also, a catdir() normalises './dir/foo' to 'dir/foo'; 760 ### which was the problem in bug #23999 761 my $res = -d $files->[$pos] 762 ? File::Spec->catdir( $files->[$pos], '' ) 763 : File::Spec->catdir( File::Basename::dirname( $files->[$pos] ) ); 764 765 $$dir = $res; 766 } 767 768 ### if the first and last dir don't match, make sure the 769 ### dirname is not set wrongly 770 my $dir; 771 772 ### dirs are the same, so we know for sure what the extract dir is 773 if( $dir1 eq $dir2 ) { 774 $dir = $dir1; 775 776 ### dirs are different.. do they share the base dir? 777 ### if so, use that, if not, fall back to '.' 778 } else { 779 my $base1 = [ File::Spec->splitdir( $dir1 ) ]->[0]; 780 my $base2 = [ File::Spec->splitdir( $dir2 ) ]->[0]; 781 782 $dir = File::Spec->rel2abs( $base1 eq $base2 ? $base1 : '.' ); 783 } 784 785 return File::Spec->rel2abs( $dir ); 786} 787 788run(); 789 790