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