File Coverage

blib/lib/Dist/Surveyor.pm
Criterion Covered Total %
statement 157 230 68.2
branch 28 98 28.5
condition 15 59 25.4
subroutine 22 25 88.0
pod 0 9 0.0
total 222 421 52.7


line stmt bran cond sub pod time code
1             package Dist::Surveyor;
2              
3             =head1 NAME
4              
5             Dist::Surveyor - Survey installed modules and determine the specific distribution versions they came from
6              
7             =head1 SYNOPSIS
8              
9             my $options = {
10             opt_match => $opt_match,
11             opt_perlver => $opt_perlver,
12             opt_remnants => $opt_remnants,
13             distro_key_mod_names => $distro_key_mod_names,
14             };
15             my @installed_releases = determine_installed_releases($options, \@libdirs);
16              
17             =head1 DESCRIPTION
18              
19             Surveys your huge ball of Perl modules, jammed together inside a directory,
20             and tells you exactly which module is installed there.
21              
22             For quick start, and a fine example of this module usage, see L.
23              
24             This module have one exported function - determine_installed_releases
25              
26             =cut
27              
28 1     1   472 use strict;
  1         6  
  1         24  
29 1     1   4 use warnings;
  1         2  
  1         18  
30              
31 1     1   360 use version;
  1         1650  
  1         5  
32 1     1   67 use Carp; # core
  1         1  
  1         84  
33 1     1   537 use Data::Dumper; # core
  1         5704  
  1         61  
34 1     1   7 use File::Find; # core
  1         1  
  1         59  
35 1     1   6 use File::Spec; # core
  1         1  
  1         41  
36 1     1   5 use List::Util qw(max sum); # core
  1         1  
  1         94  
37 1     1   445 use Dist::Surveyor::Inquiry; # internal
  1         3  
  1         61  
38 1     1   2493 use Module::CoreList;
  1         121272  
  1         13  
39 1     1   1573 use Module::Metadata;
  1         7768  
  1         70  
40              
41             our $VERSION = '0.021';
42              
43 1     1   11 use constant ON_WIN32 => $^O eq 'MSWin32';
  1         4  
  1         99  
44 1     1   9 use constant ON_VMS => $^O eq 'VMS';
  1         4  
  1         3990  
45              
46             if (ON_VMS) {
47             require File::Spec::Unix;
48             }
49              
50             our ($DEBUG, $VERBOSE);
51             *DEBUG = \$::DEBUG;
52             *VERBOSE = \$::VERBOSE;
53              
54             require Exporter;
55             our @ISA = qw{Exporter};
56             our @EXPORT = qw{determine_installed_releases};
57              
58             =head1 determine_installed_releases($options, $search_dirs)
59              
60             $options includes:
61              
62             =over
63              
64             =item opt_match
65              
66             A regex qr//. If exists, will ignore modules that doesn't match this regex
67              
68             =item opt_perlver
69              
70             Skip modules that are included as core in this Perl version
71              
72             =item opt_remnants
73              
74             If true, output will include old distribution versions that have left old modules behind
75              
76             =item distro_key_mod_names
77              
78             A hash-ref, with a list of irregular named releases. i.e. 'libwww-perl' => 'LWP'.
79              
80             =back
81              
82             $search_dirs is an array-ref containing the list of directories to survey.
83              
84             Returns a list, where each element is a hashref representing one installed distibution.
85             This hashref is what MetaCPAN returns for C,
86             with two additional keys:
87              
88             =over
89              
90             =item *
91              
92             'url' - that same as 'download_url', but without the hostname. can be used to
93             download the file for your favorite mirror
94              
95             =item *
96              
97             'dist_data' - Hashref containing info about the release, i.e. percent_installed.
98             (fully installed releases will have '100.00')
99              
100             =back
101              
102             =cut
103              
104             sub determine_installed_releases {
105 1     1 0 24354 my ($options, $search_dirs) = @_;
106 1   33     37 $options->{opt_perlver} ||= version->parse( $] )->numify;
107              
108 1         8 my %installed_mod_info;
109              
110 1         24 warn "Finding modules in @$search_dirs\n";
111 1         11 my ($installed_mod_files, $installed_meta) = find_installed_modules(@$search_dirs);
112              
113             # get the installed version of each installed module and related info
114 1         20 warn "Finding candidate releases for the ".keys(%$installed_mod_files)." installed modules\n";
115 1         11 foreach my $module ( sort keys %$installed_mod_files ) {
116 6         27 my $mod_file = $installed_mod_files->{$module};
117              
118 6 50       37 if (my $opt_match = $options->{opt_match}) {
119 0 0       0 if ($module !~ m/$opt_match/o) {
120 0         0 delete $installed_mod_files->{$module};
121 0         0 next;
122             }
123             }
124              
125 6 50       42 module_progress_indicator($module) unless $VERBOSE;
126 6         28 my $mi = get_installed_mod_info($options, $module, $mod_file);
127 6 50       39 $installed_mod_info{$module} = $mi if $mi;
128             }
129              
130              
131             # Map modules to dists using the accumulated %installed_mod_info info
132              
133 1         40 warn "*** Mapping modules to releases\n";
134              
135 1         5 my %best_dist;
136 1         8 foreach my $mod ( sort keys %installed_mod_info ) {
137 6         25 my $mi = $installed_mod_info{$mod};
138              
139 6 50       36 module_progress_indicator($mod) unless $VERBOSE;
140              
141             # find best match among the cpan releases that included this module
142             my $ccdr = $installed_mod_info{$mod}{candidate_cpan_dist_releases}
143 6 50       33 or next; # no candidates, warned about above (for mods with a version)
144              
145 6         60 my $best_dist_cache_key = join " ", sort keys %$ccdr;
146 6         18 our %best_dist_cache;
147 6   66     52 my $best = $best_dist_cache{$best_dist_cache_key}
148             ||= pick_best_cpan_dist_release($ccdr, \%installed_mod_info);
149              
150 6         19 my $note = "";
151 6 0 33     23 if ((@$best > 1) and $installed_meta->{perllocalpod}) {
152             # try using perllocal.pod to narrow the options, if there is one
153             # XXX TODO move this logic into the per-candidate-distro loop below
154             # it doesn't make much sense to be here at the per-module level
155             my @in_perllocal = grep {
156 0         0 my $distname = $_->{distribution};
  0         0  
157             my ($v, $dist_mod_name) = perllocal_distro_mod_version(
158 0         0 $options->{distro_key_mod_names}, $distname, $installed_meta->{perllocalpod});
159 0 0       0 warn "$dist_mod_name in perllocal.pod: ".($v ? "YES" : "NO")."\n"
    0          
160             if $DEBUG;
161 0         0 $v;
162             } @$best;
163 0 0 0     0 if (@in_perllocal && @in_perllocal < @$best) {
164 0         0 $note = sprintf "narrowed from %d via perllocal", scalar @$best;
165 0         0 $best = \@in_perllocal;
166             }
167             }
168              
169 6 50 33     51 if (@$best > 1 or $note) { # note the poor match for this module
170             # but not if there's no version (as that's common)
171 0         0 my $best_desc = join " or ", map { $_->{release} } @$best;
  0         0  
172 0         0 my $pct = sprintf "%.2f%%", $best->[0]{fraction_installed} * 100;
173             warn "$mod $mi->{version} odd best match: $best_desc $note ($best->[0]{fraction_installed})\n"
174 0 0 0     0 if $note or $VERBOSE or ($mi->{version} and $best->[0]{fraction_installed} < 0.3);
      0        
      0        
175             # if the module has no version and multiple best matches
176             # then it's unlikely make a useful contribution, so ignore it
177             # XXX there's a risk that we'd ignore all the modules of a release
178             # where none of the modules has a version, but that seems unlikely.
179 0 0       0 next if not $mi->{version};
180             }
181              
182 6         21 for my $dist (@$best) {
183             # two level hash to make it easier to handle versions
184 6   100     55 my $di = $best_dist{ $dist->{distribution} }{ $dist->{release} } ||= { dist => $dist };
185 6         16 push @{ $di->{modules} }, $mi;
  6         22  
186 6         19 $di->{or}{$_->{release}}++ for grep { $_ != $dist } @$best;
  6         42  
187             }
188              
189             }
190              
191 1         34 warn "*** Refining releases\n";
192              
193             # $best_dist{ Foo }{ Foo-1.23 }{ dist=>$dist_struct, modules=>, or=>{ Foo-1.22 => $dist_struct } }
194              
195 1         8 my @installed_releases; # Dist-Name => { ... }
196              
197 1         11 for my $distname ( sort keys %best_dist ) {
198 3         13 my $releases = $best_dist{$distname};
199 3         17 push @installed_releases, refine_releases($options, $distname, $releases);
200             }
201              
202             # sorting into dependency order could be added later, maybe
203              
204 1         54 return @installed_releases;
205             }
206              
207             sub refine_releases {
208 3     3 0 14 my ($options, $distname, $releases) = @_;
209              
210             my @dist_by_version = sort {
211 3         20 $a->{dist}{version_obj} <=> $b->{dist}{version_obj} or
212             $a->{dist}{fraction_installed} <=> $b->{dist}{fraction_installed}
213 0 0       0 } values %$releases;
214             my @dist_by_fraction = sort {
215 3         11 $a->{dist}{fraction_installed} <=> $b->{dist}{fraction_installed} or
216             $a->{dist}{version_obj} <=> $b->{dist}{version_obj}
217 0 0       0 } values %$releases;
218            
219 3         9 my @remnant_dists = @dist_by_version;
220 3         7 my $installed_dist = pop @remnant_dists;
221              
222             # is the most recent candidate dist version also the one with the
223             # highest fraction_installed?
224 3 50       15 if ($dist_by_version[-1] == $dist_by_fraction[-1]) {
    0          
225             # this is the common case: we'll assume that's installed and the
226             # rest are remnants of earlier versions
227             }
228             elsif ($dist_by_fraction[-1]{dist}{fraction_installed} == 100) {
229 0         0 warn "Unsure which $distname is installed from among @{[ keys %$releases ]}\n";
  0         0  
230 0         0 @remnant_dists = @dist_by_fraction;
231 0         0 $installed_dist = pop @remnant_dists;
232 0         0 warn "Selecting the one that apprears to be 100% installed\n";
233             }
234             else {
235             # else grumble so the user knows to ponder the possibilities
236 0         0 warn "Can't determine which $distname is installed from among @{[ keys %$releases ]}\n";
  0         0  
237 0         0 warn Dumper([\@dist_by_version, \@dist_by_fraction]);
238 0         0 warn "\tSelecting based on latest version\n";
239             }
240              
241 3 50 33     20 if (@remnant_dists or $DEBUG) {
242 0 0       0 warn "Distributions with remnants (chosen release is first):\n"
243             unless our $dist_with_remnants_warning++;
244 0         0 warn "@{[ map { $_->{dist}{release} } reverse @dist_by_fraction ]}\n";
  0         0  
  0         0  
245 0         0 for ($installed_dist, @remnant_dists) {
246 0         0 my $fi = $_->{dist}{fraction_installed};
247 0         0 my $modules = $_->{modules};
248 0         0 my $mv_desc = join(", ", map { "$_->{module} $_->{version}" } @$modules);
  0         0  
249             warn sprintf "\t%s\t%s%% installed: %s\n",
250             $_->{dist}{release},
251             $_->{dist}{percent_installed},
252 0 0       0 (@$modules > 4 ? "(".@$modules." modules)" : $mv_desc),
253             }
254             }
255              
256 3         9 my @installed_releases;
257             # note ordering: remnants first
258 3 50       39 for (($options->{opt_remnants} ? @remnant_dists : ()), $installed_dist) {
259             my ($author, $release)
260 3         7 = @{$_->{dist}}{qw(author release)};
  3         18  
261              
262 3         122 my $release_data = get_release_info($author, $release);
263 3 50       55 next unless $release_data;
264            
265             # shortcuts
266 3         36 (my $url = $release_data->{download_url}) =~ s{ .*? \b authors/ }{authors/}x;
267              
268             push @installed_releases, {
269             #
270             %$release_data,
271             # extra items mushed inhandy shortcuts
272             url => $url,
273             # raw data structures
274             dist_data => $_->{dist},
275 3         65 };
276             }
277             #die Dumper(\@installed_releases);
278 3         21 return @installed_releases;
279             }
280              
281             # for each installed module, get the list of releases that it exists in
282             # Parameters:
283             # $options - uses only opt_perlver
284             # $module - module name (i.e. 'Dist::Surveyor')
285             # $mod_file - the location of this module on the filesystem
286             # Return:
287             # undef if this module should be skipped
288             # otherwise, a hashref containing:
289             # file => $mod_file,
290             # module => $module,
291             # version => $mod_version,
292             # version_obj => same as version, but as an object,
293             # size => $mod_file_size,
294             # # optional flags:
295             # file_size_mismatch => 1,
296             # cpan_dist_fallback => 1, # could not find this module/version on cpan,
297             # # but found a release with that version, containing such module
298             # version_not_on_cpan> 1, # can not find this file on CPAN.
299             # # releases info
300             # candidate_cpan_dist_releases => hashref,
301             #
302             # candidate_cpan_dist_releases hashref contain a map of all the releases
303             # that this module exists in. see get_candidate_cpan_dist_releases for more
304             # info.
305             sub get_installed_mod_info {
306 6     6 0 21 my ($options, $module, $mod_file) = @_;
307              
308 6         14 my $mod_version = do {
309             # silence warnings about duplicate VERSION declarations
310             # eg Catalyst::Controller::DBIC::API* 2.002001
311 6 0   0   65 local $SIG{__WARN__} = sub { warn @_ if $_[0] !~ /already declared with version/ };
  0         0  
312 6         73 my $mm = Module::Metadata->new_from_file($mod_file);
313 6         58994 $mm->version; # only one version for one package in file
314             };
315 6   100     259 $mod_version ||= 0; # XXX
316 6         190 my $mod_file_size = -s $mod_file;
317              
318             # Eliminate modules that will be supplied by the target perl version
319 6 50       52 if ( my $cv = $Module::CoreList::version{ $options->{opt_perlver} }->{$module} ) {
320 0         0 $cv =~ s/ //g;
321 0 0       0 if (version->parse($cv) >= version->parse($mod_version)) {
322 0         0 warn "$module is core in perl $options->{opt_perlver} (lib: $mod_version, core: $cv) - skipped\n";
323 0         0 return;
324             }
325             }
326              
327 6         80 my $mi = {
328             file => $mod_file,
329             module => $module,
330             version => $mod_version,
331             version_obj => version->parse($mod_version),
332             size => $mod_file_size,
333             };
334              
335             # ignore modules we know aren't indexed
336 6 50       24 return $mi if $module =~ /^Moose::Meta::Method::Accessor::Native::/;
337              
338             # XXX could also consider file mtime: releases newer than the mtime
339             # of the module file can't be the origin of that module file.
340             # (assuming clocks and file times haven't been messed with)
341              
342 6         12 eval {
343 6         197 my $ccdr = get_candidate_cpan_dist_releases($module, $mod_version, $mod_file_size);
344 6 50       103 if (not %$ccdr) {
345 0         0 $ccdr = get_candidate_cpan_dist_releases($module, $mod_version, 0);
346 0 0 0     0 if (%$ccdr) {
    0          
347             # probably either a local change/patch or installed direct from repo
348             # but with a version number that matches a release
349 0 0 0     0 warn "$module $mod_version on CPAN but with different file size (not $mod_file_size)\n"
350             if $mod_version or $VERBOSE;
351 0         0 $mi->{file_size_mismatch}++;
352             }
353             elsif ($ccdr = get_candidate_cpan_dist_releases_fallback($module, $mod_version) and %$ccdr) {
354 0 0 0     0 warn "$module $mod_version not on CPAN but assumed to be from @{[ sort keys %$ccdr ]}\n"
  0         0  
355             if $mod_version or $VERBOSE;
356 0         0 $mi->{cpan_dist_fallback}++;
357             }
358             else {
359 0         0 $mi->{version_not_on_cpan}++;
360             # Possibly:
361             # - a local change/patch or installed direct from repo
362             # with a version number that was never released.
363             # - a private module never released on cpan.
364             # - a build-time create module eg common/sense.pm.PL
365             warn "$module $mod_version not found on CPAN\n"
366             if $mi->{version} # no version implies uninteresting
367 0 0 0     0 or $VERBOSE;
368             # XXX could try finding the module with *any* version on cpan
369             # to help with later advice. ie could select as candidates
370             # the version above and the version below the number we have,
371             # and set a flag to inform later logic.
372             }
373             }
374 6 50       36 $mi->{candidate_cpan_dist_releases} = $ccdr if %$ccdr;
375             };
376 6 50       22 if ($@) {
377 0         0 warn "Failed get_candidate_cpan_dist_releases($module, $mod_version, $mod_file_size): $@";
378             }
379 6         27 return $mi;
380             }
381              
382             # pick_best_cpan_dist_release - memoized
383             # for each %$ccdr adds a fraction_installed based on %$installed_mod_info
384             # returns ref to array of %$ccdr values that have the max fraction_installed
385              
386             sub pick_best_cpan_dist_release {
387 5     5 0 16 my ($ccdr, $installed_mod_info) = @_;
388              
389 5         28 for my $release (sort keys %$ccdr) {
390 48         187 my $release_info = $ccdr->{$release};
391             $release_info->{fraction_installed}
392 48         224 = dist_fraction_installed($release_info->{author}, $release, $installed_mod_info);
393             $release_info->{percent_installed} # for informal use
394 48         801 = sprintf "%.2f", $release_info->{fraction_installed} * 100;
395             }
396              
397 5         36 my $max_fraction_installed = max( map { $_->{fraction_installed} } values %$ccdr );
  48         139  
398 5         17 my @best = grep { $_->{fraction_installed} == $max_fraction_installed } values %$ccdr;
  48         126  
399              
400 5         43 return \@best;
401             }
402              
403              
404             # returns a number from 0 to 1 representing the fraction of the modules
405             # in a particular release match the coresponding modules in %$installed_mod_info
406             sub dist_fraction_installed {
407 48     48 0 198 my ($author, $release, $installed_mod_info) = @_;
408              
409 48         154 my $tag = "$author/$release";
410 48         1988 my $mods_in_rel = get_module_versions_in_release($author, $release);
411 48         984 my $mods_in_rel_count = keys %$mods_in_rel;
412             my $mods_inst_count = sum( map {
413 48   50     261 my $mi = $installed_mod_info->{ $_->{name} };
414             # XXX we stash the version_obj into the mods_in_rel hash
415             # (though with little/no caching effect with current setup)
416             $_->{version_obj} ||= eval { version->parse($_->{version}) };
417             my $hit = ($mi && $mi->{version_obj} == $_->{version_obj}) ? 1 : 0;
418             # demote to a low-scoring partial match if the file size differs
419             # XXX this isn't good as the effect varies with the number of modules
420             $hit = 0.1 if $mi && $mi->{size} != $_->{size};
421             warn sprintf "%s %s %s %s: %s\n", $tag, $_->{name}, $_->{version_obj}, $_->{size},
422             ($hit == 1) ? "matches"
423             : ($mi) ? "differs ($mi->{version_obj}, $mi->{size})"
424             : "not installed",
425             if $DEBUG;
426             $hit;
427             } values %$mods_in_rel) || 0;
428              
429 48 50       222 my $fraction_installed = ($mods_in_rel_count) ? $mods_inst_count/$mods_in_rel_count : 0;
430 48 50 33     315 warn "$author/$release:\tfraction_installed $fraction_installed ($mods_inst_count/$mods_in_rel_count)\n"
431             if $VERBOSE or !$mods_in_rel_count;
432              
433 48         268 return $fraction_installed;
434             }
435              
436             sub get_file_mtime {
437 0     0 0 0 my ($file) = @_;
438             # try to find the time the file was 'installed'
439             # by looking for the commit date in svn or git
440             # else fallback to the file modification time
441 0         0 return (stat($file))[9];
442             }
443              
444              
445             sub find_installed_modules {
446 1     1 0 5 my (@dirs) = @_;
447              
448             ### File::Find uses follow_skip => 1 by default, which doesn't die
449             ### on duplicates, unless they are directories or symlinks.
450             ### Ticket #29796 shows this code dying on Alien::WxWidgets,
451             ### which uses symlinks.
452             ### File::Find doc says to use follow_skip => 2 to ignore duplicates
453             ### so this will stop it from dying.
454 1         5 my %find_args = ( follow_skip => 2 );
455              
456             ### File::Find uses lstat, which quietly becomes stat on win32
457             ### it then uses -l _ which is not allowed by the statbuffer because
458             ### you did a stat, not an lstat (duh!). so don't tell win32 to
459             ### follow symlinks, as that will break badly
460             # XXX disabled because we want the postprocess hook to work
461             #$find_args{'follow_fast'} = 1 unless ON_WIN32;
462              
463             ### never use the @INC hooks to find installed versions of
464             ### modules -- they're just there in case they're not on the
465             ### perl install, but the user shouldn't trust them for *other*
466             ### modules!
467             ### XXX CPANPLUS::inc is now obsolete, remove the calls
468             #local @INC = CPANPLUS::inc->original_inc;
469              
470             # sort @dirs to put longest first to make it easy to handle
471             # elements that are within other elements (e.g., an archdir)
472 1         5 my @dirs_ordered = sort { length $b <=> length $a } @dirs;
  0         0  
473              
474 1         5 my %seen_mod;
475             my %dir_done;
476 1         0 my %meta; # return metadata about the search
477 1         5 for my $dir (@dirs_ordered) {
478 1 50       6 next if $dir eq '.';
479              
480             ### not a directory after all
481             ### may be coderef or some such
482 1 50       24 next unless -d $dir;
483              
484             ### make sure to clean up the directories just in case,
485             ### as we're making assumptions about the length
486             ### This solves rt.cpan issue #19738
487              
488             ### John M. notes: On VMS cannonpath can not currently handle
489             ### the $dir values that are in UNIX format.
490 1         12 $dir = File::Spec->canonpath($dir) unless ON_VMS;
491              
492             ### have to use F::S::Unix on VMS, or things will break
493 1         3 my $file_spec = ON_VMS ? 'File::Spec::Unix' : 'File::Spec';
494              
495             ### XXX in some cases File::Find can actually die!
496             ### so be safe and wrap it in an eval.
497 1 50       3 eval {
498             File::Find::find(
499             { %find_args,
500             postprocess => sub {
501 6     6   156 $dir_done{$File::Find::dir}++;
502             },
503             wanted => sub {
504              
505 12 100   12   82 unless (/\.pm$/i) {
506             # skip all dot-dirs (eg .git .svn)
507 6 50 33     142 $File::Find::prune = 1
508             if -d $File::Find::name and /^\.\w/;
509             # don't reenter a dir we've already done
510             $File::Find::prune = 1
511 6 50       31 if $dir_done{$File::Find::name};
512             # remember perllocal.pod if we see it
513 6 50       22 push @{$meta{perllocalpod}}, $File::Find::name
  0         0  
514             if $_ eq 'perllocal.pod';
515 6         668 return;
516             }
517 6         17 my $mod = $File::Find::name;
518              
519             ### make sure it's in Unix format, as it
520             ### may be in VMS format on VMS;
521 6         10 $mod = VMS::Filespec::unixify($mod) if ON_VMS;
522              
523 6         19 $mod = substr( $mod, length($dir) + 1, -3 );
524 6         49 $mod = join '::', $file_spec->splitdir($mod);
525              
526 6 50       26 return if $seen_mod{$mod};
527 6         111 $seen_mod{$mod} = $File::Find::name;
528              
529             ### ignore files that don't contain a matching package declaration
530             ### warn about those that do contain some kind of package declaration
531             #use File::Slurp;
532             #my $content = read_file($File::Find::name);
533             #unless ( $content =~ m/^ \s* package \s+ (\#.*\n\s*)? $mod \b/xm ) {
534             #warn "No 'package $mod' seen in $File::Find::name\n"
535             #if $VERBOSE && $content =~ /\b package \b/x;
536             #return;
537             #}
538              
539             },
540             },
541 1         187 $dir
542             );
543 1         19 1;
544             }
545             or die "File::Find died: $@";
546              
547             }
548              
549 1         9 return (\%seen_mod, \%meta);
550             }
551              
552              
553             sub perllocal_distro_mod_version {
554 0     0 0 0 my ($distro_key_mod_names, $distname, $perllocalpod) = @_;
555              
556 0         0 ( my $dist_mod_name = $distname ) =~ s/-/::/g;
557 0   0     0 my $key_mod_name = $distro_key_mod_names->{$distname} || $dist_mod_name;
558              
559 0         0 our $perllocal_distro_mod_version;
560 0 0       0 if (not $perllocal_distro_mod_version) { # initial setup
561 0 0 0     0 warn "Only first perllocal.pod file will be processed: @$perllocalpod\n"
562             if ref $perllocalpod eq 'ARRAY' and @$perllocalpod > 1;
563              
564 0         0 $perllocal_distro_mod_version = {};
565             # extract data from perllocal.pod
566 0 0       0 if (my $plp = shift @$perllocalpod) {
567             # The VERSION isn't always the same as that in the distro file
568 0 0       0 if (eval { require ExtUtils::Perllocal::Parser }) {
  0         0  
569 0         0 my $p = ExtUtils::Perllocal::Parser->new;
570             $perllocal_distro_mod_version = { map {
571 0         0 $_->name => $_->{data}{VERSION}
572 0         0 } $p->parse_from_file($plp) };
573 0         0 warn "Details of ".keys(%$perllocal_distro_mod_version)." distributions found in $plp\n";
574             }
575             else {
576 0         0 warn "Wanted to use perllocal.pod but can't because ExtUtils::Perllocal::Parser isn't available\n";
577             }
578             }
579             else {
580 0         0 warn "No perllocal.pod found to aid disambiguation\n";
581             }
582             }
583              
584 0         0 return $perllocal_distro_mod_version->{$key_mod_name};
585             }
586              
587              
588             sub module_progress_indicator {
589 12     12 0 42 my ($module) = @_;
590 12         78 my $crnt = (split /::/, $module)[0];
591 12   100     71 our $last ||= '';
592 12 100       81 if ($last ne $crnt) {
593 4         91 warn "\t$crnt...\n";
594 4         30 $last = $crnt;
595             }
596             }
597              
598             =head1 OTHERS
599              
600             This module checks $::DEBUG and $::VERBOSE for obvious proposes.
601              
602             This module uses L to communicate with MetaCPAN.
603             Check that module's documentation for options and caching.
604              
605             You can use L to take the list of releases
606             and create a mini-cpan containing them.
607              
608             =head1 AUTHOR
609              
610             Written by Tim Bunce ETim.Bunce@pobox.comE
611              
612             Maintained by Fomberg Shmuel Eshmuelfomberg@gmail.comE, Dan Book Edbook@cpan.orgE
613            
614             =head1 COPYRIGHT AND LICENSE
615            
616             Copyright 2011-2013 by Tim Bunce.
617            
618             This library is free software; you can redistribute it and/or modify
619             it under the same terms as Perl itself.
620            
621             =cut
622              
623             1;