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; |