xref: /openbsd-src/gnu/usr.bin/perl/Porting/core-cpan-diff (revision 1ad61ae0a79a724d2d3ec69e69c8e1d1ff6b53a0)
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