File Coverage

blib/lib/CPAN/Site/Index.pm
Criterion Covered Total %
statement 101 282 35.8
branch 33 128 25.7
condition 22 56 39.2
subroutine 21 34 61.7
pod 0 18 0.0
total 177 518 34.1


line stmt bran cond sub pod time code
1             # Copyrights 1998,2005-2017 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5 1     1   424 use warnings;
  1         1  
  1         24  
6 1     1   3 use strict;
  1         1  
  1         21  
7              
8             package CPAN::Site::Index;
9 1     1   7 use vars '$VERSION';
  1         1  
  1         62  
10             $VERSION = '1.14';
11              
12 1     1   3 use base 'Exporter';
  1         1  
  1         79  
13              
14             our @EXPORT_OK = qw/cpan_index cpan_mirror/;
15             our $VERSION; # required in test-env
16              
17 1     1   435 use Log::Report 'cpan-site', syntax => 'SHORT';
  1         63722  
  1         6  
18              
19 1     1   566 use version;
  1         1215  
  1         4  
20 1     1   50 use File::Find qw/find/;
  1         1  
  1         45  
21 1     1   398 use File::Copy qw/copy/;
  1         1561  
  1         47  
22 1     1   4 use File::Basename qw/basename dirname/;
  1         1  
  1         43  
23 1     1   441 use HTTP::Date qw/time2str/;
  1         2525  
  1         52  
24 1     1   389 use File::Spec::Functions qw/catfile catdir splitdir/;
  1         497  
  1         53  
25 1     1   552 use LWP::UserAgent ();
  1         24992  
  1         18  
26 1     1   596 use Archive::Tar ();
  1         57169  
  1         25  
27 1     1   567 use Archive::Zip qw(:ERROR_CODES :CONSTANTS);
  1         36096  
  1         154  
28 1     1   406 use CPAN::Checksums ();
  1         20241  
  1         27  
29 1     1   9 use IO::Zlib ();
  1         1  
  1         2587  
30              
31             my $tar_gz = qr/ \.tar\.gz$ | \.tar\.Z$ | \.tgz$/xi;
32             my $zip = qr/ \.zip$ /xi;
33             my $cpan_update = 0.04; # days between reload of full CPAN index
34             my $ua;
35              
36             sub safe_copy($$);
37             sub cpan_index($@);
38             sub register($$$);
39             sub package_inventory($$;$);
40             sub package_on_usual_location($);
41             sub inspect_archive;
42             sub inspect_tar_archive($$);
43             sub inspect_zip_archive($$);
44             sub collect_package_details($$$);
45             sub update_global_cpan($$);
46             sub load_file($$);
47             sub merge_global_cpan($$$);
48             sub create_details($$$$$);
49             sub calculate_checksums($);
50             sub read_details($);
51             sub remove_expired_details($$$);
52             sub mkdirhier(@);
53             sub cpan_mirror($$$@);
54              
55             sub safe_copy($$)
56 0     0 0 0 { my ($from, $to) = @_;
57 0         0 trace "copy $from to $to";
58 0 0       0 copy $from, $to
59             or fault __x"cannot copy {from} to {to}", from => $from, to => $to;
60             }
61              
62             sub cpan_index($@)
63 0     0 0 0 { my ($mycpan, $globalcpan, %opts) = @_;
64 0         0 my $lazy = $opts{lazy};
65 0         0 my $fallback = $opts{fallback};
66 0 0       0 my $undefs = exists $opts{undefs} ? $opts{undefs} : 1;
67              
68 0 0       0 unless($ua)
69 0         0 { $ua = LWP::UserAgent->new;
70 0 0       0 $ua->env_proxy if $opts{env_proxy};
71             }
72              
73 0 0       0 -d $mycpan
74             or error __x"archive top '{dir}' is not a directory"
75             , dir => $mycpan;
76              
77 0         0 my $program = basename $0;
78 0   0     0 $VERSION ||= 'undef'; # test env at home
79 0         0 trace "$program version $VERSION";
80              
81 0         0 my $global = catdir $mycpan, 'global';
82 0         0 my $mods = catdir $mycpan, 'modules';
83 0         0 my $authors = catdir $mycpan, 'authors';
84 0         0 mkdirhier $global, $mods, $authors;
85              
86 0         0 my $globdetails = update_global_cpan $mycpan, $globalcpan;
87              
88             # Create mailrc and modlist
89              
90 0         0 safe_copy catfile($global, '01mailrc.txt.gz')
91             , catfile($authors, '01mailrc.txt.gz');
92              
93 0         0 safe_copy catfile($global, '03modlist.data.gz')
94             , catfile($mods, '03modlist.data.gz');
95              
96             # Create packages details
97              
98 0         0 my $details = catfile $mods, '02packages.details.txt.gz';
99 0         0 my $newlist = catfile $mods, '02packages.details.tmp.gz';
100 0         0 my $newer;
101              
102 0         0 my $reuse_dists = {};
103 0 0 0     0 if($lazy && -f $details)
104 0         0 { $reuse_dists = read_details $details;
105 0         0 $newer = -M $details;
106 0         0 remove_expired_details $mycpan, $reuse_dists, $newer;
107             }
108              
109 0         0 my ($mypkgs, $distdirs)
110             = package_inventory $mycpan, $reuse_dists, $newer;
111              
112 0 0       0 merge_global_cpan $mycpan, $mypkgs, $globdetails
113             if $fallback;
114              
115 0         0 create_details $details, $newlist, $mypkgs, $lazy, $undefs;
116              
117 0 0       0 if(-f $details)
118 0         0 { trace "backup old details file to $details.bak";
119 0         0 safe_copy $details, "$details.bak";
120             }
121              
122 0 0       0 if(-f $newlist)
123 0         0 { trace "promoting $newlist to current";
124 0 0       0 rename $newlist, $details
125             or error __x"cannot rename '{from}' in '{to}'"
126             , from => $newlist, to => $details;
127             }
128              
129 0         0 calculate_checksums $distdirs;
130             }
131              
132             #
133             # Package Inventory
134             #
135              
136             # global variables for testing purposes (sorry)
137             our ($topdir, $findpkgs, %finddirs, $olddists, $index_age);
138              
139             sub register($$$)
140 28     28 0 33 { my ($package, $this_version, $dist) = @_;
141              
142             # warn "register $package, " . (defined $this_version ? $this_version : 'undef');
143              
144 28 50       37 if(ref $this_version)
145 0         0 { eval { $this_version = version->parse($this_version) };
  0         0  
146 0 0       0 if($@)
147 0         0 { alert __x"error when creating version object for {pkg}: {err}"
148             , pkg => $package, err => $@;
149 0         0 return;
150             }
151             }
152            
153 28 50       50 my $registered_version = exists $findpkgs->{$package} ? $findpkgs->{$package}[0] : undef;
154 28 100       35 $this_version =~ s/^v// if $this_version;
155              
156 28 50 33     44 return if defined $registered_version
157             && $registered_version > $this_version;
158              
159 28         85 $findpkgs->{$package} = [ $this_version, $dist ];
160             }
161              
162             sub package_inventory($$;$)
163 0     0 0 0 { (my $mycpan, $olddists, $index_age) = @_; #!!! see "my"
164 0         0 $topdir = catdir $mycpan, 'authors', 'id';
165 0         0 mkdirhier $topdir;
166              
167 0         0 $findpkgs = {};
168 0         0 trace "creating inventory from $topdir";
169              
170 0         0 find {wanted => \&inspect_archive, no_chdir => 1}, $topdir;
171 0         0 ($findpkgs, \%finddirs);
172             }
173              
174             sub package_on_usual_location($)
175 22     22 0 244 { my $file = shift;
176 22         42 my ($top, $subdir, @rest) = splitdir $file;
177 22 50       119 defined $subdir or return 0;
178              
179             !@rest # path is at top-level of distro
180 22 100       105 || $subdir eq 'lib'; # inside lib
181             }
182              
183             sub inspect_archive
184 3     3 0 9509 { my $fn = $File::Find::name;
185 3 50 33     125 return unless -f $fn
      33        
186             && ($fn =~ $tar_gz || $fn =~ $zip);
187              
188 3         27 (my $dist = $fn) =~ s!^\Q$topdir\E[\\/]!!;
189              
190 3 50 33     9 if(defined $index_age && -M $fn > $index_age)
191             {
192 0 0       0 unless(exists $olddists->{$dist})
193 0         0 { trace "not the latest: $dist";
194 0         0 return;
195             }
196              
197 0         0 trace "latest older than index: $dist";
198 0         0 foreach (@{$olddists->{$dist}})
  0         0  
199 0         0 { my ($pkg, $version) = @$_;
200 0         0 register $pkg, $version, $dist;
201             }
202 0         0 return;
203             }
204              
205 3         14 trace "inspecting archive $fn";
206 3         65 $finddirs{$File::Find::dir}++;
207              
208 3 50       26 return inspect_tar_archive $dist, $fn
209             if $fn =~ $tar_gz;
210              
211 0 0       0 return inspect_zip_archive $dist, $fn
212             if $fn =~ $zip;
213             }
214              
215             sub inspect_tar_archive($$)
216 3     3 0 5 { my ($dist, $fn) = @_;
217              
218 3         17 my $arch = Archive::Tar->new;
219 3 50       30 $arch->read($fn, 1)
220             or error __x"no files in tar archive '{fn}': {err}"
221             , fn => $fn, err => $arch->error;
222              
223 3         32460 foreach my $file ($arch->get_files)
224 60         386 { my $fn = $file->full_path;
225 60 100 100     763 $file->is_file && $fn =~ m/\.pm$/i && package_on_usual_location $fn
      100        
226             or next;
227 20         42 collect_package_details $fn, $dist, $file->get_content_by_ref;
228             }
229             }
230              
231             sub inspect_zip_archive($$)
232 0     0 0 0 { my ($dist, $fn) = @_;
233              
234 0         0 my $arch = Archive::Zip->new;
235 0 0       0 $arch->read($fn)==AZ_OK
236             or error __x"no files in zip archive '{fn}': {err}"
237             , fn => $fn, err => $arch->error;
238              
239 0         0 foreach my $member ($arch->membersMatching( qr/\.pm$/i ))
240 0         0 { my $fn = $member->fileName;
241 0 0 0     0 $member->isTextFile && package_on_usual_location $fn
242             or next;
243 0         0 my ($contents, $status) = $member->contents;
244 0 0       0 $status==AZ_OK
245             or error "error in zip file {fn}: {err}"
246             , fn => $fn, err => $status;
247 0         0 collect_package_details $fn, $dist, \$contents;
248             }
249             }
250              
251             sub collect_package_details($$$)
252 20     20 0 59 { my ($fn, $dist) = (shift, shift);
253 20         14 my @lines = split /\r?\n/, ${shift()};
  20         2090  
254 20         22 my $in_pod = 0;
255 20         16 my $package;
256 20         17 local $VERSION = undef; # may get destroyed by eval
257              
258 20         33 while(@lines)
259 4623         3461 { local $_ = shift @lines;
260 4623 50       5227 last if m/^__(?:END|DATA)__$/;
261              
262 4623 100       5187 $in_pod = ($1 ne 'cut') if m/^=(\w+)/;
263 4623 100 100     10337 next if $in_pod || m/^\s*#/;
264              
265 3388   100     10180 $_ .= shift @lines
      100        
266             while @lines && m/package|use|VERSION/ && !m/[;{]/;
267              
268 3388 100       4345 if( m/^\s* package \s* ((?:\w+\:\:)*\w+) (?:\s+ (\S*))? \s* [;{]/x )
269 28         52 { my ($thispkg, $v) = ($1, $2);
270 28         19 my $thisversion;
271 28 50       35 if($v)
272 0         0 { $thisversion = eval {qv($v)};
  0         0  
273 0 0       0 alert __x"illegal version for {pkg}, found '{version}': {err}"
274             , pkg => $thispkg, version => $v, err => $@ if $@;
275             }
276              
277             # second package in file?
278 28 100       40 register $package, $VERSION, $dist
279             if defined $package;
280              
281 28         41 ($package, $VERSION) = ($thispkg, $thisversion);
282 28         81 trace "pkg $package from $fn";
283             }
284              
285 3388 50       7047 if( m/^\s* \$ ${package}::VERSION \s* = \s* ["']?(\w+?)["']? \s* ;/x )
286 0         0 { $VERSION = $1;
287             }
288              
289 3388 100 100     9211 if( !$VERSION && m/^ (?:use\s+version\s*;\s*)?
290             (?:our)? \s* \$ ((?: \w+\:\:)*) VERSION \s* \= (.*)/x )
291 7 50       15 { defined $2 or next;
292 7         11 my ($ns, $vers) = ($1, $2);
293              
294             # some versions of CPAN.pm do contain lines like "$VERSION =~ ..."
295             # which also need to be processed.
296 7         360 eval "\$VERSION =$vers";
297 7 50       17 if(defined $VERSION)
298 7 100       21 { ($package = $ns) =~ s/\:\:$//
299             if length $ns;
300 7         19 trace "pkg $package version $VERSION";
301             }
302             }
303             }
304              
305 20 50       29 $VERSION = $VERSION->numify if ref $VERSION;
306 20 50       45 register $package, $VERSION, $dist
307             if defined $package;
308             }
309              
310             sub update_global_cpan($$)
311 0     0 0   { my ($mycpan, $globalcpan) = @_;
312              
313 0           my $global = catdir $mycpan, 'global';
314             my ($mailrc, $globdetails, $modlist) =
315 0           map { catfile $global, $_ }
  0            
316             qw/01mailrc.txt.gz 02packages.details.txt.gz 03modlist.data.gz/;
317              
318 0 0 0       return $globdetails
      0        
      0        
319             if -f $globdetails && -f $globdetails && -f $modlist
320             && -M $globdetails < $cpan_update;
321              
322 0           info "(re)loading global CPAN files";
323              
324 0           mkdirhier $global;
325 0           load_file "$globalcpan/authors/01mailrc.txt.gz", $mailrc;
326 0           load_file "$globalcpan/modules/02packages.details.txt.gz", $globdetails;
327 0           load_file "$globalcpan/modules/03modlist.data.gz", $modlist;
328 0           $globdetails;
329             }
330              
331             sub load_file($$)
332 0     0 0   { my ($from, $to) = @_;
333 0           my $response = $ua->get($from, ':content_file' => $to);
334 0 0         return if $response->is_success;
335              
336 0           unlink $to;
337 0           error __x"failed to get {uri} for {to}: {err}"
338             , uri => $from, to => $to, err => $response->status_line;
339             }
340              
341             sub merge_global_cpan($$$)
342 0     0 0   { my ($mycpan, $pkgs, $globdetails) = @_;
343              
344 0           trace "merge packages with CPAN core list in $globdetails";
345 0           my $cpan_pkgs = read_details $globdetails;
346              
347 0           while(my ($cpandist, $cpanpkgs) = each %$cpan_pkgs)
348 0           { foreach (@$cpanpkgs)
349 0           { my ($pkg, $version) = @$_;
350 0 0         next if exists $pkgs->{$pkg};
351 0           $pkgs->{$pkg} = [$version, $cpandist];
352             }
353             }
354             }
355              
356             sub create_details($$$$$)
357 0     0 0   { my ($details, $filename, $pkgs, $lazy, $undefs) = @_;
358              
359 0           trace "creating package details in $filename";
360 0 0         my $fh = IO::Zlib->new($filename, 'wb')
361             or fault __x"generating gzipped {fn}", fn => $filename;
362              
363 0           my $lines = keys %$pkgs;
364 0           my $date = time2str time;
365 0 0         my $how = $lazy ? "lazy" : "full";
366              
367 0           info "produced list of $lines packages $how";
368              
369 0           my $program = basename $0;
370 0           my $module = __PACKAGE__;
371 0           $fh->print (<<__HEADER);
372             File: 02packages.details.txt
373             URL: file://$details
374             Description: Packages listed in CPAN and local repository
375             Columns: package name, version, path
376             Intended-For: private CPAN
377             Line-Count: $lines
378             Written-By: $program with $module $CPAN::Site::Index::VERSION ($how)
379             Last-Updated: $date
380              
381             __HEADER
382              
383 0           foreach my $pkg (sort keys %$pkgs)
384 0           { my ($version, $path) = @{$pkgs->{$pkg}};
  0            
385              
386 0 0 0       $version = 'undef'
387             if !defined $version || $version eq '';
388              
389             next
390 0 0 0       if $version eq 'undef' && !$undefs;
391              
392 0           $path =~ s,\\,/,g;
393 0           $fh->printf("%-30s\t%s\t%s\n", $pkg, $version, $path);
394             }
395             }
396              
397             sub calculate_checksums($)
398 0     0 0   { my $dirs = shift;
399 0           trace "updating checksums";
400              
401 0           foreach my $dir (keys %$dirs)
402 0           { trace "summing $dir";
403 0 0         CPAN::Checksums::updatedir($dir)
404             or warning 'failed calculating checksums in {dir}', dir => $dir;
405             }
406             }
407              
408             sub read_details($)
409 0     0 0   { my $fn = shift;
410 0 0         -f $fn or return {};
411 0           trace "collecting all details from $fn";
412              
413 0 0         my $fh = IO::Zlib->new($fn, 'rb')
414             or fault __x"cannot read from {fn}", fn => $fn;
415              
416 0           my $line; # skip header, search first blank
417 0           do { $line = $fh->getline } until $line =~ m/^\s*$/;
  0            
418              
419 0           my $time_last_update = (stat $fn)[9];
420 0           my %dists;
421              
422 0           while(my $line = $fh->getline)
423 0           { chomp $line;
424 0           my ($pkg, $version, $dist) = split ' ', $line, 3;
425              
426 0 0         unless($dist)
427 0           { warning "$fn error line=\n $line";
428 0           next;
429             }
430              
431 0           push @{$dists{$dist}}, [$pkg, $version];
  0            
432             }
433              
434 0           \%dists;
435             }
436              
437             sub remove_expired_details($$$)
438 0     0 0   { my ($mycpan, $dists, $newer) = @_;
439 0           trace "extracting only existing local distributions";
440              
441 0           my $authors = catdir $mycpan, 'authors', 'id';
442 0           foreach my $dist (keys %$dists)
443 0           { my $fn = catfile $authors, $dist;
444 0 0         if(! -f $fn)
    0          
445             { # removed local or a global dist
446 0           delete $dists->{$dist};
447             }
448             elsif(-M $fn < $newer)
449 0           { trace "dist $dist file updated, reindexing";
450 0           delete $dists->{$dist};
451             }
452             }
453             }
454              
455             sub mkdirhier(@)
456 0     0 0   { foreach my $dir (@_)
457 0 0         { next if -d $dir;
458 0           mkdirhier dirname $dir;
459              
460 0 0         mkdir $dir, 0755
461             or fault __x"cannot create directory {dir}", dir => $dir;
462              
463 0           trace "created $dir";
464             }
465 0           1;
466             }
467              
468             sub cpan_mirror($$$@)
469 0     0 0   { my ($mycpan, $globalcpan, $mods, %opts) = @_;
470 0 0         @$mods or return;
471 0           my %need = map { ($_ => 1) } @$mods;
  0            
472 0           my $auth = catdir $mycpan, 'authors', 'id';
473              
474 0 0         unless($ua)
475 0           { $ua = LWP::UserAgent->new;
476 0 0         $ua->env_proxy if $opts{env_proxy};
477             }
478              
479 0           my $globdetails
480             = update_global_cpan $mycpan, $globalcpan;
481              
482 0 0         my $fh = IO::Zlib->new($globdetails, 'rb')
483             or fault __x"cannot read from {fn}", fn => $globdetails;
484              
485 0           while(my $line = $fh->getline) # skip header, search first blank
486 0 0         { last if $line =~ m/^\s*$/;
487             }
488              
489 0   0       $ua ||= LWP::UserAgent->new;
490 0           while(my $line = $fh->getline)
491 0           { my ($pkg, $version, $dist) = split ' ', $line;
492 0 0         delete $need{$pkg} or next;
493              
494 0           my $to = catfile $auth, split m#/#, $dist;
495 0 0         if(-f $to)
496 0           { trace __x"package {pkg} present in distribution {dist}"
497             , pkg => $pkg, dist => $dist;
498 0           next;
499             }
500              
501 0           my $source = "$globalcpan/authors/id/$dist";
502 0           mkdirhier dirname $to;
503 0           my $response = $ua->get($source, ':content_file' => $to);
504 0 0         unless($response->is_success)
505 0           { unlink $to;
506 0           error __x"failed to get {uri} for {to}: {err}"
507             , uri => $source, to => $to, err => $response->status_line;
508             }
509              
510 0           info __x"got {pkg} in {dist}", pkg => $pkg, dist => $dist;
511             }
512              
513             warning __x"package {pkg} does not exist", pkg => $_
514 0           for sort keys %need;
515             }
516              
517             1;