This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
Various updates and fixes to some of the SysV IPC ops and their tests
[perl5.git] / Porting / core-cpan-diff
... / ...
CommitLineData
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