File Coverage

blib/lib/CPAN/FindDependencies.pm
Criterion Covered Total %
statement 210 214 98.1
branch 86 96 89.5
condition 33 38 86.8
subroutine 26 27 96.3
pod 1 1 100.0
total 356 376 94.6


line stmt bran cond sub pod time code
1             package CPAN::FindDependencies;
2              
3 11     11   1047087 use strict;
  11         122  
  11         339  
4 11     11   64 use warnings;
  11         21  
  11         346  
5 11     11   107 use vars qw(@net_log $VERSION @ISA @EXPORT_OK);
  11         24  
  11         826  
6              
7 11     11   9976 use Archive::Tar;
  11         1091818  
  11         847  
8 11     11   7920 use Archive::Zip;
  11         847283  
  11         577  
9 11     11   108 use File::Temp qw(tempfile);
  11         27  
  11         782  
10 11     11   6319 use File::Type;
  11         97909  
  11         562  
11 11     11   7997 use LWP::UserAgent;
  11         503897  
  11         510  
12 11     11   40330 use Module::CoreList;
  11         1556052  
  11         162  
13 11     11   7782 use Scalar::Util qw(blessed);
  11         30  
  11         1941  
14 11     11   8353 use CPAN::Meta;
  11         325786  
  11         450  
15 11     11   5993 use CPAN::FindDependencies::Dependency;
  11         39  
  11         429  
16 11     11   4694 use CPAN::FindDependencies::MakeMaker qw(getreqs_from_mm);
  11         35  
  11         672  
17 11     11   5475 use Parse::CPAN::Packages;
  11         10075182  
  11         437  
18 11     11   6056 use URI::file;
  11         56847  
  11         776  
19              
20             require Exporter;
21             @ISA = qw(Exporter);
22             @EXPORT_OK = qw(finddeps);
23              
24             $VERSION = '2.99_01';
25              
26 11     11   95 use constant MAXINT => ~0;
  11         26  
  11         30567  
27              
28             =head1 NAME
29              
30             CPAN::FindDependencies - find dependencies for modules on the CPAN
31              
32             =head1 SYNOPSIS
33              
34             use CPAN::FindDependencies;
35             my @dependencies = CPAN::FindDependencies::finddeps("CPAN");
36             foreach my $dep (@dependencies) {
37             print ' ' x $dep->depth();
38             print $dep->name().' ('.$dep->distribution().")\n";
39             }
40              
41             =head1 INCOMPATIBLE CHANGES
42              
43             Up to version 2.49 you used the C<02packages> argument to specify where a
44             cached C<02packages.details.txt.gz> could be found. That argument no longer
45             exists as of version 3.00, use the C<mirror> argument instead.
46              
47             Up to version 2.49, C<maxdepth =E<gt> 0> would incorrectly return the whole
48             tree. From version 3.00 it cuts the tree off at its root so will only return
49             the module that you asked about. Not very useful, but correct.
50              
51             In version 2.49 you used the C<configreqs> argument to specify that you were
52             interested in configure-time requirements as well as build- and run-time
53             requirements. That option no longer exists as of version 3.00, it will always
54             report on configure, build, test, and run-time requirements.
55              
56              
57             =head1 HOW IT WORKS
58              
59             The module uses the CPAN packages index to map modules to distributions and
60             vice versa, and then fetches distributions' metadata or Makefile.PL files from
61             a CPAN mirror to determine pre-requisites. This means that a
62             working interwebnet connection is required.
63              
64             =head1 FUNCTIONS
65              
66             There is just one function, which is not exported by default
67             although you can make that happen in the usual fashion.
68              
69             =head2 finddeps
70              
71             Takes a single compulsory parameter, the name of a module
72             (ie Some::Module); and the following optional
73             named parameters:
74              
75             =over
76              
77             =item nowarnings
78              
79             Warnings about modules where we can't find their META.yml or Makefile.PL, and
80             so can't divine their pre-requisites, will be suppressed;
81              
82             =item fatalerrors
83              
84             Failure to get a module's dependencies will be a fatal error
85             instead of merely emitting a warning;
86              
87             =item perl
88              
89             Use this version of perl to figure out what's in core. If not
90             specified, it defaults to 5.005. Three part version numbers
91             (eg 5.8.8) are supported but discouraged.
92              
93             =item cachedir
94              
95             A directory to use for caching. It defaults to no caching. Even if
96             caching is turned on, this is only for META.yml or Makefile.PL files.
97              
98             The cache is never automatically cleared out. It is your responsibility
99             to clear out old data.
100              
101             =item maxdepth
102              
103             Cuts off the dependency tree at the specified depth. Your specified
104             module is at depth 0, your dependencies at depth 1, their dependencies
105             at depth 2, and so on.
106              
107             If you don't specify any maxdepth at all it will grovel over the
108             entire tree.
109              
110             =item mirror
111              
112             This can be provided more than once, if for example you want to use
113             a private L<Pinto> repository for your own code while using a public
114             CPAN mirror for open source dependencies. The argument comes in two parts
115             separated by a comma - the base URL from which to fetch files, and
116             optionally the URL or a file from which to fetch the index
117             C<02packages.details.txt.gz> file to use with that mirror.
118              
119             mirror https://cpan.mydomain.net,file:///home/me/mycache/02packages.txt.gz
120              
121             If you want to use the default CPAN mirror (https://cpan.metacpan.org/)
122             but also specify an index location you can use C<DEFAULT> for the mirror URL.
123              
124             So for example, to use your own special private mirror, including fetching
125             02packages from it, but also use the default mirror with a cached local
126             copy of its 02packages, specify two mirrors thus:
127              
128             mirror => 'https://cpan.mydomain.net',
129             mirror => 'DEFAULT,file:///home/me/mycache/02packages.txt.gz'
130              
131             The index is cached for three minutes or until your process finishes, whichever is soonest. This is because it is HUMUNGOUS and parsing it takes ages even when it's loaded from a local disk, and I don't want the tests to take forever.
132              
133             =item usemakefilepl
134              
135             If set to true, then for any module that doesn't have a META.yml,
136             try to use its Makefile.PL as well. Note that this involves
137             downloading code from the Internet and running it. This obviously
138             opens you up to all kinds of bad juju, hence why it is disabled
139             by default. NB that this fetches Makefile.PL from
140             L<https://fastapi.metacpan.org> B<only> so will not work for private mirrors.
141             This is a deliberate choice, your own private code ought to be packaged
142             properly with a META file, you should only care about divining dependencies
143             from Makefile.PL if you rely on really old stuff on public CPAN mirrors.
144              
145             =item recommended
146              
147             Adds recommended modules to the list of dependencies, if set to a true value.
148              
149             =item suggested
150              
151             Adds suggested modules to the list of dependencies, if set to a true value.
152              
153              
154             =back
155              
156             It returns a list of CPAN::FindDependencies::Dependency objects, whose
157             useful methods are:
158              
159             =over
160              
161             =item name
162              
163             The module's name;
164              
165             =item distribution
166              
167             The distribution containing this module;
168              
169             =item version
170              
171             The minimum required version of his module (if specified in the requirer's
172             pre-requisites list);
173              
174             =item depth
175              
176             How deep in the dependency tree this module is;
177              
178             =item warning
179              
180             If any warning was generated (even if suppressed) for the module,
181             it will be recorded here.
182              
183             =back
184              
185             Any modules listed as dependencies but which are in the perl core
186             distribution for the version of perl you specified are suppressed.
187              
188             These objects are returned in a semi-defined order. You can be sure
189             that a module will be immediately followed by one of its dependencies,
190             then that dependency's dependencies, and so on, followed by the 'root'
191             module's next dependency, and so on. You can reconstruct the tree
192             by paying attention to the depth of each object.
193              
194             The ordering of any particular module's immediate 'children' can be
195             assumed to be random - it's actually hash key order.
196              
197             =head1 SECURITY
198              
199             If you set C<usemakefilepl> to a true value, this module may download code
200             from the internet and execute it. You should think carefully before enabling
201             that feature.
202              
203             =head1 BUGS/WARNINGS/LIMITATIONS
204              
205             You must have web access to L<http://metacpan.org/> and (unless
206             you tell it where else to look for the index)
207             L<http://www.cpan.org/>, or have all the data cached locally..
208             If any
209             metadata or Makefile.PL files are missing, the distribution's dependencies will
210             not be found and a warning will be spat out.
211              
212             Startup can be slow, especially if it needs to fetch the index from
213             the interweb.
214              
215             Dynamic dependencies - for example, dependencies that only apply on some
216             platforms - can't be reliably resolved. They *may* be resolved if you use the
217             unsafe Makefile.PL, but even that can't be relied on.
218              
219             =head1 FEEDBACK
220              
221             I welcome feedback about my code, including constructive criticism
222             and bug reports. The best bug reports include files that I can add
223             to the test suite, which fail with the current code in my git repo and
224             will pass once I've fixed the bug
225              
226             Feature requests are far more likely to get implemented if you submit
227             a patch yourself.
228              
229             =head1 SOURCE CODE REPOSITORY
230              
231             L<git://github.com/DrHyde/perl-modules-CPAN-FindDependencies.git>
232              
233             =head1 SEE ALSO
234              
235             L<CPAN>
236              
237             L<http://deps.cpantesters.org/>
238              
239             L<http://metacpan.org>
240              
241             =head1 AUTHOR, LICENCE and COPYRIGHT
242              
243             Copyright 2007 - 2019 David Cantrell E<lt>F<david@cantrell.org.uk>E<gt>
244              
245             This software is free-as-in-speech software, and may be used,
246             distributed, and modified under the terms of either the GNU
247             General Public Licence version 2 or the Artistic Licence. It's
248             up to you which one you use. The full text of the licences can
249             be found in the files GPL2.txt and ARTISTIC.txt, respectively.
250              
251             =head1 THANKS TO
252              
253             Stephan Loyd (for fixing problems with some META.yml files)
254              
255             Alexandr Ciornii (for a patch to stop it segfaulting on Windows)
256              
257             Brian Phillips (for the code to report on required versions of modules)
258              
259             Ian Tegebo (for the code to extract deps from Makefile.PL)
260              
261             Georg Oechsler (for the code to also list 'recommended' modules)
262              
263             Jonathan Stowe (for making it work through HTTP proxies)
264              
265             Kenneth Olwing (for support for 'configure_requires')
266              
267             =head1 CONSPIRACY
268              
269             This module is also free-as-in-mason software.
270              
271             =cut
272              
273             my $default_mirror = 'https://cpan.metacpan.org/';
274              
275             sub finddeps {
276 19     19 1 162165 @net_log = ();
277 19         164 my($module, @args) = @_;
278              
279 19         234 my $self = bless({ indices => [], mirrors => [], seen => {} }, __PACKAGE__);
280              
281 19         121 while(@args) {
282 61         359 my $optname = shift(@args);
283 61         279 my $optarg = shift(@args);
284 61 100       272 if($optname ne 'mirror' ) {
285 45         519 $self->{$optname} = $optarg
286             } else {
287 16         91 my($mirror, $packages) = split(/,/, $optarg);
288 16 100       96 $mirror = $default_mirror if($mirror eq 'DEFAULT');
289 16 100       155 $mirror .= '/' unless($mirror =~ m{/$});
290 16 100       81 $packages = "${mirror}modules/02packages.details.txt.gz"
291             unless($packages);
292             ($mirror, $packages) = map {
293 16 100       62 $_ =~ /^https?:\/\// ? $_ : ''.URI::file->new_abs($_);
  32         14591  
294             } ($mirror, $packages);
295 16         147932 push @{$self->{mirrors}}, {
  16         944  
296             mirror => $mirror,
297             packages => $packages
298             };
299             }
300             }
301 19 100       122 unless(@{$self->{mirrors}}) {
  19         239  
302 5         11 push @{$self->{mirrors}}, {
  5         38  
303             mirror => $default_mirror,
304             packages => "${default_mirror}modules/02packages.details.txt.gz"
305             }
306             }
307              
308 19 100       300 $self->{maxdepth} = MAXINT unless(defined($self->{maxdepth}));
309              
310 19   100     508 $self->{perl} ||= 5.005;
311             die(__PACKAGE__.": $self->{perl} is a broken version number\n")
312 19 50       351 if($self->{perl} =~ /[^0-9.]/);
313 19 100       292 if($self->{perl} =~ /\..*\./) {
314 2         37 my @parts = split(/\./, $self->{perl});
315 2         38 $self->{perl} = $parts[0] + $parts[1] / 1000 + $parts[2] / 1000000;
316             }
317              
318 19         314 my $first_found = $self->_first_found($module);
319 19 50       1055 return $self->_finddeps(
320             module => $module,
321             version => ($first_found ? $first_found->version() : 0)
322             );
323             }
324              
325             # indices are cached for performance, cos even if the
326             # file is fetched from disk uncompressing/parsing take ages.
327             # the cache lasts three minutes.
328             our %_parsed_index_cache = ();
329             sub _indices {
330 136     136   377 my $self = shift;
331 136 100       304 if(!@{$self->{indices}}) {
  136         594  
332 19     0   587 local $SIG{__WARN__} = sub {};
333             $self->{indices} = [map {
334 21         108 my $url = $_->{packages};
335 21 100 100     340 if(!(exists($_parsed_index_cache{$url}) && $_parsed_index_cache{$url}->{expiry} > time())) {
336 14         223 $_parsed_index_cache{$url}->{expiry} = time() + 180;
337 14   50     195 $_parsed_index_cache{$url}->{index} = Parse::CPAN::Packages->new(
338             $self->_get($url) || die(__PACKAGE__.": Couldn't fetch 02packages index file from $url\n")
339             );
340             }
341             $_parsed_index_cache{$url}->{index}
342 19         110 } @{$self->{mirrors}}]
  21         191570373  
  19         114  
343             }
344 136         307 return @{$self->{indices}};
  136         1338  
345             }
346              
347             # look through all the mirrors' 02packages for a module and return a
348             # Parse::CPAN::Packages::Package for the first one it finds
349             sub _first_found {
350 84     84   259 my $self = shift;
351 84         259 my $module = shift;
352 84         464 return (map { $_->package($module) } grep { $_->package($module) } $self->_indices())[0];
  81         3191  
  96         12688  
353             }
354              
355             sub _yell {
356 14     14   47 my $self = shift;
357 14         50 my $msg = shift;
358 14         67 $msg = __PACKAGE__.": $msg";
359 14 100       95 $msg = "$msg\n" unless(substr($msg, -1, 1) eq "\n");
360 14 100       76 if(!$self->{nowarnings}) {
361 3 100       12 if($self->{fatalerrors} ) {
362 1         20 die('FATAL: '.$msg);
363             } else {
364 2         32 warn('WARNING: '.$msg);
365             }
366             }
367             }
368              
369             sub _get {
370 40     40   152 my $self = shift;
371 40         133 my $url = shift;
372 40         711 my $ua = LWP::UserAgent->new();
373 40         50904 $ua->env_proxy();
374 40         52702 $ua->agent(__PACKAGE__."/$VERSION");
375 40         2764 push @net_log, $url;
376 40         313 my $response = $ua->get($url);
377 40 100       7482816 if($response->is_success()) {
378 28         597 return $response->content();
379             } else {
380 12         566 return undef;
381             }
382             }
383              
384             sub _incore {
385 178     178   579 my $self = shift;
386 178         641 my %args = @_;
387 178         2416 my $core = $Module::CoreList::version{$args{perl}}{$args{module}};
388 178 100       605 $core =~ s/_/00/g if($core);
389 178         402 $args{version} =~ s/_/00/g;
390 178 100 100     2011 return ($core && $core >= $args{version}) ? $core : undef;
391             }
392              
393             sub _finddeps {
394 190     190   599 my $self = shift;
395 190         821 my %args = @_;
396 190         577 my( $module, $depth, $version) = @args{qw(module depth version)};
397 190   100     698 $depth ||= 0;
398              
399             return () if(
400             $module eq 'perl' ||
401             $self->_incore(
402             module => $module,
403             perl => $self->{perl},
404 190 100 100     1143 version => $version)
405             );
406              
407 65         142 my $dist = do {
408 65         201 my $package = $self->_first_found($module);
409 65 100       2800 $package ? $package->distribution() : undef;
410             };
411              
412 65 100       917 return () unless(blessed($dist));
413              
414 62         1568 my $author = $dist->cpanid();
415 62         1705 my $distname = $dist->distvname();
416              
417 62 100       919 return () if($self->{seen}->{$distname}++);
418              
419 55         1176 my %reqs = $self->_getreqs(
420             author => $author,
421             distname => $distname,
422             distfile => $dist->filename(),
423             );
424              
425             return (
426             CPAN::FindDependencies::Dependency->_new(
427             depth => $depth,
428             distribution => $dist,
429             cpanmodule => $module,
430             indices => [$self->_indices()],
431             version => $version || 0,
432             $reqs{'-warning'} ? (warning => $reqs{'-warning'}) : ()
433             ),
434             (!exists($reqs{'-warning'}) && $depth != $self->{maxdepth}) ? (map {
435             # print "Looking at $_\n";
436 52 100 100     38863 $self->_finddeps(
    100 100        
437             module => $_,
438             depth => $depth + 1,
439 171         677 version => $reqs{$_}
440             );
441             } sort keys %reqs) : ()
442             );
443             }
444              
445             # caching wrapper around _get
446             # can be asked to fetch a .meta, an archive, or a Makefile.PL,
447             # so it knows how to figure out what the cache filename is
448             # for those, based on the URL
449             # can be asked to get whichever first succeeds of multiple options.
450             # currently those are always a metadata file or an archive, which
451             # will resolve to the same cache file.
452             sub _get_cached {
453 127     127   313 my $self = shift;
454 127         564 my %args = @_;
455 127         405 my($src, $post_process) = @args{qw(src post_process)};
456 127         217 my $contents;
457             # asked to check multiple sources? Return the first which has
458             # content (or what's cached)
459 127 100       399 if(ref($src)) {
460 55         116 foreach my $this_url (@{$src}) {
  55         173  
461 67 100       392 last if($contents = $self->_get_cached(
462             post_process => $post_process,
463             src => $this_url
464             ));
465             }
466 55         252 return $contents;
467             }
468              
469 72         170 my $cachefile = $src;
470 72 100       315 if($cachefile =~ /Makefile.PL/) {
471 5         66 $cachefile =~ s{.*/([^/]+)/Makefile.PL$}{$1.MakefilePL};
472             } else {
473 67         881 $cachefile =~ s{.*/(.*?)\.(meta|zip|tar\.bz2|tar\.gz|tgz)$}{$1.meta};
474             }
475              
476 72 100 66     3492 if($self->{cachedir} && -d $self->{cachedir} && -r $self->{cachedir}."/$cachefile") {
      100        
477             open(my $cachefh, $self->{cachedir}."/$cachefile") ||
478 46 50       2642 $self->_yell('Error reading '.$self->{cachedir}."/$cachefile: $!");
479 46         353 local $/ = undef;
480 46         1847 $contents = <$cachefh>;
481 46         841 close($cachefh);
482             } else {
483 26         161 $contents = $self->_get($src);
484 26 100 66     3113 if($contents && $post_process ) {
485 14         70 $contents = $post_process->($contents);
486             }
487 26 100 100     282 if($contents && $self->{cachedir} && -d $self->{cachedir}) {
      66        
488             open(my $cachefh, '>', $self->{cachedir}."/$cachefile") ||
489 3 50       303 $self->_yell('Error writing '.$self->{cachedir}."/$cachefile: $!");
490 3         29 print $cachefh $contents;
491 3         122 close($cachefh);
492             }
493             }
494 72         574 return $contents;
495             }
496              
497             sub _getreqs {
498 55     55   704 my $self = shift;
499 55         355 my %args = @_;
500 55         245 my($author, $distname, $distfile) = @args{qw(author distname distfile)};
501              
502 55         106 my $meta_file;
503 55         104 foreach my $source (@{$self->{mirrors}}) {
  55         283  
504             $meta_file = $self->_get_cached(
505             src => [
506             $source->{mirror}."authors/id/".
507             substr($author, 0, 1).'/'.
508             substr($author, 0, 2).'/'.
509             "$author/$distname.meta",
510             $source->{mirror}."authors/id/".
511             substr($author, 0, 1).'/'.
512             substr($author, 0, 2).'/'.
513             "$author/$distfile"
514             ],
515             post_process => sub {
516             # _get_cached normally just returns a file, but we're
517             # asking it to either fetch a metadata file or if that can't be
518             # found fetch an archive from which we want to extract a file,
519             # and then cache that extracted file's contents. This function
520             # takes a blob of data and if it looks like a zip or a tarball
521             # tries to extract a META.json or META.yml and return its content
522             # (or the empty string if not found), otherwise if it doesn't
523             # look like an archive, assume that the input was a valid metadata
524             # file after all and just return it.
525 14     14   46 my $file_data = shift;
526 14         96 my $meta_file_re = qr/^([^\/]+\/)?META\.(json|yml)/;
527 14         49 my $rval = '';
528              
529             # We should be able to avoid writing to disk by something like
530             # this but it doesn't work, for either zip or tar <shrug>
531             # # my $tar = Archive::Tar->new();
532             # # $tar->read([string opened as file])
533 14         281 my($scopeguard, $tempfile) = tempfile('CPAN-FindDependencies-XXXXXXXX', UNLINK => 1, TMPDIR => 1);
534 14 50       11695 open(my $fh, '>', "$tempfile") || die("Can't write $tempfile: $!\n");
535 14         88 binmode($fh); # Windows smells of wee
536 14         1738 print $fh $file_data;
537 14         1675 close($fh);
538              
539             my $tar_extractor = sub {
540 11         317 my $tar = Archive::Tar->new(shift());
541             # sort to ensure that we get JSON by preference, META.json
542             # often contains more info
543 11 100       126162 if(my @members = sort { $a cmp $b } grep { /$meta_file_re/ } $tar->list_files()) {
  0         0  
  88         3214  
544 2         20 return $tar->get_content($members[0])
545             }
546 14         231 };
547              
548 14 100       281 if(File::Type->mime_type($file_data) eq 'application/zip') {
    100          
    100          
549 1         374 my $zip = Archive::Zip->new($tempfile);
550 1 50       2708 if(my @members = sort { $a cmp $b } $zip->membersMatching($meta_file_re)) {
  0         0  
551 1         111 $rval = $zip->contents($members[0])
552             }
553             } elsif(File::Type->mime_type($file_data) =~ m{^application/x-(gzip|tar)$}) {
554 10         10698 $rval = $tar_extractor->($tempfile);
555             } elsif(File::Type->mime_type($file_data) eq 'application/x-bzip2') {
556 1 50       8428 open(my $fh, '-|', qw(bzip2 -dc), $tempfile) ||
557             $self->_yell("Can't unbzip2 $tempfile: $!");
558 1 50       44 if($fh) { $rval = $tar_extractor->($fh); }
  1         34  
559 1         242 close($fh);
560 2         4998 } else { $rval = $file_data; } # oh, it must have been a meta file
561 14         2121 return $rval;
562             },
563 55         1350 );
564 55 100       935 last if($meta_file);
565             }
566 55 100       190 if ($meta_file) {
567 46         125 my $meta_data = eval { CPAN::Meta->load_string($meta_file); };
  46         641  
568 46 100 66     1206646 if ($@ || !defined($meta_data)) {
569 4         38 $self->_yell("$author/$distname: failed to parse metadata")
570             } else {
571 42         266 my $reqs = $meta_data->effective_prereqs();
572             return %{
573 42         51456 $reqs->merged_requirements(
574             [qw(configure build test runtime)],
575             [
576             'requires',
577             ($self->{recommended} ? 'recommends' : ()),
578 42 100       587 ($self->{suggested} ? 'suggests' : ())
    50          
579             ]
580             )->as_string_hash()
581             };
582             }
583             } else {
584 9         85 $self->_yell("$author/$distname: no metadata");
585             }
586            
587             # We could have failed to parse the metadata file, but we still want to try the Makefile.PL
588 12 100       136 if(!$self->{usemakefilepl}) {
589 7         59 return ('-warning', 'no metadata');
590             } else {
591 5         32 my $makefilepl = $self->_get_cached(
592             src => "https://fastapi.metacpan.org/source/$author/$distname/Makefile.PL",
593             );
594 5 50       37 if($makefilepl) {
595 5         109 my $result = getreqs_from_mm($makefilepl);
596 3 100       30 if ('HASH' eq ref $result) {
597 2         16 return %{ $result };
  2         158  
598             } else {
599 1         31 $self->_yell("$author/$distname: $result");
600 1         45 return ('-warning', $result);
601             }
602             } else {
603 0           $self->_yell("$author/$distname: no metadata nor Makefile.PL");
604 0           return ('-warning', 'no metadata nor Makefile.PL');
605             }
606             }
607             }
608              
609             1;