File Coverage

blib/lib/App/EPAN.pm
Criterion Covered Total %
statement 59 275 21.4
branch 0 74 0.0
condition 0 30 0.0
subroutine 20 44 45.4
pod 15 15 100.0
total 94 438 21.4


line stmt bran cond sub pod time code
1             package App::EPAN;
2              
3 1     1   72102 use 5.012;
  1         3  
4             { our $VERSION = '0.002' }
5 1     1   6 use warnings;
  1         2  
  1         33  
6 1     1   714 use English qw( -no_match_vars );
  1         3977  
  1         6  
7 1     1   835 use version;
  1         2036  
  1         5  
8 1     1   656 use autodie;
  1         16795  
  1         6  
9 1     1   7911 use Getopt::Long qw< :config gnu_getopt >;
  1         13343  
  1         7  
10 1     1   795 use Pod::Usage qw< pod2usage >;
  1         50774  
  1         108  
11 1     1   701 use Dist::Metadata ();
  1         32415  
  1         33  
12 1     1   541 use Path::Class qw< file dir >;
  1         36725  
  1         83  
13 1     1   18 use Cwd qw< cwd >;
  1         2  
  1         40  
14 1     1   574 use File::Find::Rule ();
  1         8616  
  1         24  
15 1     1   658 use Compress::Zlib ();
  1         55457  
  1         36  
16 1     1   585 use Log::Log4perl::Tiny qw< :easy :dead_if_first >;
  1         15083  
  1         4  
17 1     1   933 use Moo;
  1         11814  
  1         5  
18 1     1   2783 use IPC::Run ();
  1         25874  
  1         25  
19 1     1   529 use File::Copy ();
  1         2416  
  1         30  
20 1     1   486 use File::Which qw< which >;
  1         1020  
  1         749  
21              
22             has configuration => (
23             is => 'rw',
24             lazy => 1,
25             predicate => 'has_config',
26             clearer => 'clear_config',
27             default => sub { {} },
28             );
29             has action => (is => 'rw',);
30             has last_index => (is => 'rw',);
31              
32             sub run {
33 0     0 1   my $package = shift;
34 0           my $self = $package->new();
35 0           $self->get_options(@_);
36              
37 0           my $action = $self->action();
38 0 0         pod2usage(-verbose => 99, -sections => 'USAGE') unless defined $action;
39 0 0         if (my $method = $self->can("action_$action")) {
40 0           $self->$method();
41             }
42             else {
43 0           FATAL "action '$action' is not supported\n";
44 0           $self->action_list_actions;
45 0           exit 1;
46             }
47 0           return;
48             } ## end sub run
49              
50             sub get_options {
51 0     0 1   my $self = shift;
52 0 0 0       my $action =
53             (scalar(@_) && length($_[0]) && (substr($_[0], 0, 1) ne '-'))
54             ? shift(@_)
55             : 'list-actions';
56 0           $action =~ s{-}{_}gmxs;
57 0           local @ARGV = @_;
58 0           $self->action($action);
59 0           my %config = ();
60 0 0         GetOptions(
61             \%config,
62             qw(
63             mailrc|m|1=s
64             output|packages-details|o|2=s
65             modlist|modlist-data|l|3=s
66             target|t=s
67             test|T!
68             author|a=s
69             usage! help! man! version!
70             )
71             ) or pod2usage(-verbose => 99, -sections => 'USAGE');
72 0   0       our $VERSION ||= 'whateva';
73             pod2usage(message => "$0 $VERSION", -verbose => 99, -sections => ' ')
74 0 0         if $config{version};
75 0 0         pod2usage(-verbose => 99, -sections => 'USAGE') if $config{usage};
76             pod2usage(-verbose => 99, -sections => 'USAGE|EXAMPLES|OPTIONS')
77 0 0         if $config{help};
78 0 0         pod2usage(-verbose => 2) if $config{man};
79 0           $self->configuration(
80             {
81             cmdline_config => \%config,
82             config => \%config,
83             args => [@ARGV],
84             }
85             );
86 0           return;
87             } ## end sub get_options
88              
89             sub args {
90 0     0 1   return @{$_[0]->configuration()->{args}};
  0            
91             }
92              
93             sub config {
94 0     0 1   my $self = shift;
95 0 0         return @{$self->configuration()->{config}}{@_} if wantarray();
  0            
96 0           return $self->configuration()->{config}{shift @_};
97             }
98              
99             sub target_dir {
100 0     0 1   my $self = shift;
101 0   0       return dir($self->config('target') // 'epan');
102             }
103              
104             sub execute_tests {
105 0     0 1   my $self = shift;
106 0           return $self->config('test');
107             }
108              
109 0     0 1   sub action_index { return shift->_do_index }
110              
111             {
112 1     1   17 no strict 'refs';
  1         3  
  1         2150  
113             *{action_idx} = \&action_index;
114             }
115              
116             sub _save {
117 0     0     my ($self, $name, $contents, $config_key, $output) = @_;
118              
119 0 0         if (defined(my $confout = $self->config($config_key))) {
120 0 0         $output =
    0          
121             !length($confout) ? undef
122             : $confout eq '-' ? \*STDOUT
123             : file($confout);
124             } ## end if (defined(my $confout...))
125 0 0         if (defined $output) {
126 0           INFO "saving output to $output";
127 0 0         $self->_save2($output,
128             scalar(ref($contents) ? $contents->() : $contents));
129             }
130             else {
131 0           INFO "empty filename for $name file, skipping";
132             }
133             } ## end sub _save
134              
135             sub _do_index {
136 0     0     my ($self, $basedir) = @_;
137 0   0       $basedir //= $self->target_dir;
138 0 0         LOGDIE "path '$basedir' does not exist (wrong -t option?)"
139             unless -d $basedir;
140              
141 0           $self->_save(
142             '01mailrc', # name
143             '', # contents
144             'mailrc', # configuration key to look output file
145             $basedir->file(qw< authors 01mailrc.txt.gz >) # default
146             );
147              
148             $self->_save(
149             '02packages.details', # name
150             sub { # where to get data from. Call is avoided if
151             # no file on output
152 0     0     INFO "getting contributions for regenerated index...";
153 0           $self->_index_for($basedir);
154             },
155 0           'output', # configuration key to look output file
156             $basedir->file(qw< modules 02packages.details.txt.gz >) # default
157             );
158              
159 0           $self->_save(
160             '03modlist.data', # name
161             <<'END_OF_03_MODLIST_DATA',
162             File: 03modlist.data
163             Description: These are the data that are published in the module
164             list, but they may be more recent than the latest posted
165             modulelist. Over time we'll make sure that these data
166             can be used to print the whole part two of the
167             modulelist. Currently this is not the case.
168             Modcount: 0
169             Written-By: PAUSE version 1.005
170             Date: Sun, 28 Jul 2013 07:41:15 GMT
171              
172             package CPAN::Modulelist;
173             # Usage: print Data::Dumper->new([CPAN::Modulelist->data])->Dump or similar
174             # cannot 'use strict', because we normally run under Safe
175             # use strict;
176             sub data {
177             my $result = {};
178             my $primary = "modid";
179             for (@$CPAN::Modulelist::data){
180             my %hash;
181             @hash{@$CPAN::Modulelist::cols} = @$_;
182             $result->{$hash{$primary}} = \%hash;
183             }
184             return $result;
185             }
186             $CPAN::Modulelist::cols = [ ];
187             $CPAN::Modulelist::data = [ ];
188             END_OF_03_MODLIST_DATA
189             'modlist', # configuration key to look output file
190             $basedir->file(qw< modules 03modlist.data.gz >) # default
191             );
192             } ## end sub _do_index
193              
194             sub _save2 {
195 0     0     my ($self, $path, $contents) = @_;
196 0           my ($fh, $is_gz);
197 0 0         if (ref($path) eq 'GLOB') {
198 0           $fh = $path;
199 0           $is_gz = 0;
200             }
201             else {
202 0 0         $path->dir()->mkpath() unless -d $path->dir()->stringify();
203 0           $fh = $path->open('>');
204 0           $is_gz = $path->stringify() =~ m{\.gz$}mxs;
205             }
206              
207 0 0         if ($is_gz) {
208 0           my $gz = Compress::Zlib::gzopen($fh, 'wb');
209 0           $gz->gzwrite($contents);
210 0           $gz->gzclose();
211             }
212             else {
213 0           binmode $fh;
214 0           print {$fh} $contents;
  0            
215             }
216 0           return;
217             } ## end sub _save2
218              
219             sub _index_for {
220 0     0     my ($self, $path) = @_;
221 0   0       $path //= $self->target_dir;
222 0           my @index = $self->_index_body_for($path);
223 0   0       our $VERSION ||= 'whateva';
224 0           my $header = <<"END_OF_HEADER";
225             File: 02packages.details.txt
226             URL: http://cpan.perl.org/modules/02packages.details.txt.gz
227             Description: Package names found in directory \$CPAN/authors/id/
228             Columns: package name, version, path
229             Intended-For: Automated fetch routines, namespace documentation.
230             Written-By: epan $VERSION
231 0           Line-Count: ${ \ scalar @index }
232 0           Last-Updated: ${ \ scalar localtime() }
233             END_OF_HEADER
234 0           return join "\n", $header, @index, '';
235             } ## end sub _index_for
236              
237             sub _collect_index_for {
238 0     0     my ($self, $path) = @_;
239 0   0       $path //= $self->target_dir;
240 0           $path = dir($path);
241 0 0         LOGDIE "path '$path' does not exist (wrong -t option?)" unless -d $path;
242              
243 0           my $idpath = $path->subdir(qw< authors id >);
244 0           my %data_for;
245 0           for my $file (File::Find::Rule->extras({follow => 1})->file()
246             ->in($idpath->stringify()))
247             {
248 0           INFO "indexing $file";
249 0           my $index_path =
250             file($file)->relative($idpath)->as_foreign('Unix')->stringify();
251 0           my $dm = Dist::Metadata->new(file => $file);
252 0           my $version_for = $dm->package_versions();
253              
254 0           $data_for{distro}{$index_path} = $version_for;
255 0           (my $bare_index_path = $index_path) =~
256             s{^(.)/(\1.)/(\2.*?)/}{$3/}mxs;
257 0           $data_for{bare_distro}{$bare_index_path} = $version_for;
258              
259 0           my %_localdata_for;
260 0           my $score = 0;
261 0           my $previous;
262 0           while (my ($module, $version) = each %$version_for) {
263 0   0       my $print_version = $version // 'undef';
264 0           DEBUG "data for $module: [$print_version] [$index_path]";
265 0           $_localdata_for{$module} = {
266             version => $version,
267             distro => $index_path,
268             _file => $file,
269             };
270 0 0         next if $score != 0;
271 0 0         next unless exists($data_for{module}{$module});
272 0           $previous = $data_for{module}{$module};
273 0           DEBUG 'some previous version exists';
274 0 0         if (! defined $version) {
    0          
275 0 0         $score = -1 if defined($previous->{version});
276             }
277             elsif (defined $previous->{version}) {
278 0           my $tv = version->parse($version);
279 0           my $pv = version->parse($previous->{version});
280 0           $score = $tv <=> $pv;
281             }
282 0           DEBUG "score: $score";
283             } ## end while (my ($module, $version...))
284              
285 0           DEBUG "FINAL SCORE $score";
286              
287 0 0         if ($score < 0) { # didn't win against something already in
288 0           DEBUG "marking $file as obsolete";
289 0           $data_for{obsolete}{$file} = 1;
290 0           next;
291             }
292              
293 0           DEBUG "getting $file data as winner (for the moment)";
294 0 0         if ($previous) {
295 0           my $oip = $previous->{distro};
296 0           DEBUG "marking $oip as obsolete";
297 0           $data_for{obsolete}{$previous->{_file}} = 1;
298             delete $data_for{module}{$_}
299 0           for keys %{$data_for{distro}{$oip}};
  0            
300             }
301             # copy stuff over to the "official" data for modules
302 0           $data_for{module}{$_} = $_localdata_for{$_} for keys %_localdata_for;
303             } ## end for my $file (File::Find::Rule...)
304 0           $self->last_index(\%data_for);
305 0 0         return %data_for if wantarray();
306 0           return \%data_for;
307             } ## end sub _collect_index_for
308              
309             sub _index_body_for {
310 0     0     my ($self, $path) = @_;
311 0   0       $path //= $self->target_dir;
312              
313 0           my $data_for = $self->_collect_index_for($path);
314 0           my $module_data_for = $data_for->{module};
315 0           my @retval;
316 0           for my $module (sort keys %{$module_data_for}) {
  0            
317 0           my $md = $module_data_for->{$module};
318 0   0       my $version = $md->{version} || 'undef';
319 0           my $index_path = $md->{distro};
320 0           my $fw = 38 - length $version;
321 0 0         $fw = length $module if $fw < length $module;
322 0           push @retval, sprintf "%-${fw}s %s %s", $module, $version,
323             $index_path;
324             } ## end for my $module (sort keys...)
325 0 0         return @retval if wantarray();
326 0           return \@retval;
327             } ## end sub _index_body_for
328              
329             sub action_create {
330 0     0 1   my ($self) = @_;
331              
332 0           my $target = $self->target_dir;
333 0 0         LOGDIE "target directory $target exists, use update instead"
334             if -d $target;
335 0           $target->mkpath();
336              
337 0           return $self->action_update;
338             } ## end sub action_create
339              
340             sub action_update {
341 0     0 1   my ($self) = @_;
342              
343 0           my $target = $self->target_dir;
344 0 0         $target->mkpath() unless -d $target;
345              
346 0           my $dists = $target->stringify();
347 0           my $local = $target->subdir('local')->stringify();
348 0 0         my @command = (
349             qw< cpanm --reinstall --quiet --self-contained >,
350             ($self->execute_tests ? () : '--notest'),
351             '--local-lib-contained' => $local,
352             '--save-dists' => $dists,
353             $self->args(),
354             );
355              
356 0           my ($out, $err);
357             {
358 0           local $SIG{TERM} = sub {
359 0     0     WARN "cpanm: received TERM signal, ignoring";
360 0           };
361 0           INFO "calling @command";
362 0 0         IPC::Run::run \@command, \undef, \*STDOUT, \*STDERR
363             or LOGDIE "cpanm: $? ($err)";
364             }
365              
366 0           INFO 'onboarding completed, indexing...';
367 0           $self->_do_index($target);
368 0           my $data_for = $self->last_index();
369              
370 0           INFO 'saving distlist';
371 0           my @distros = $self->last_distlist();
372 0           $self->_save2($target->file('distlist.txt'), join "\n", @distros, '');
373              
374 0           INFO 'saving modlist';
375 0           my @modules = $self->last_modlist();
376 0           $self->_save2($target->file('modlist.txt'), join "\n", @modules, '');
377              
378 0           my $file = $target->file('install.sh');
379 0 0         if (!-e $file) {
380 0           $self->_save2($file, <<'END_OF_INSTALL');
381             #!/bin/bash
382             ME=$(readlink -f "$0")
383             MYDIR=$(dirname "$ME")
384              
385             TARGET="$MYDIR/local"
386             [ $# -gt 0 ] && TARGET=$1
387              
388             if [ -n "$TARGET" ]; then
389             "$MYDIR/cpanm" --mirror "file://$MYDIR" --mirror-only \
390             -L "$TARGET" \
391             $(<"$MYDIR/modlist.txt")
392             else
393             "$MYDIR/cpanm" --mirror "file://$MYDIR" --mirror-only \
394             $(<"$MYDIR/modlist.txt")
395             fi
396             END_OF_INSTALL
397 0           chmod 0777 & ~umask(), $file->stringify();
398             } ## end if (!-e $file)
399              
400 0           $file = $target->file('cpanm');
401 0 0         if (!-e $file) {
402 0           my $cpanm = which('cpanm');
403 0           File::Copy::copy($cpanm, $file->stringify());
404 0           chmod 0777 & ~umask(), $file->stringify();
405             }
406             } ## end sub action_update
407              
408             {
409 1     1   8 no strict 'subs';
  1         2  
  1         504  
410             *action_install = \&action_update;
411             *action_add = \&action_update;
412             }
413              
414             sub action_inject {
415 0     0 1   my ($self) = @_;
416              
417 0           my $target = $self->target_dir;
418 0 0         $target->mkpath() unless -d $target;
419              
420 0   0       my $author = $self->config('author') // $ENV{EPAN_AUTHOR} // 'LOCAL';
      0        
421 0           my $first = substr $author, 0, 1;
422 0           my $first_two = substr $author, 0, 2;
423 0           my $repo = $target->subdir(qw< authors id >, $first, $first_two, $author);
424 0           $repo->mkpath;
425 0           $repo = $repo->stringify;
426              
427 0           File::Copy::copy($_, $repo) for $self->args;
428              
429 0           INFO 'onboarding completed, indexing...';
430 0           $self->_do_index($target);
431              
432 0           return;
433             }
434              
435             sub _list_obsoletes {
436 0     0     my ($self) = @_;
437 0           my $basedir = $self->target_dir;
438 0           my $data_for = $self->_collect_index_for($basedir);
439 0           return sort {$a cmp $b} keys %{$data_for->{obsolete}};
  0            
  0            
440             }
441              
442             sub action_list_obsoletes {
443 0     0 1   my ($self) = @_;
444 0           say for $self->_list_obsoletes;
445 0           return;
446             }
447              
448             sub action_purge_obsoletes {
449 0     0 1   my ($self) = @_;
450 0           for my $file ($self->_list_obsoletes) {
451 0           INFO "removing $file";
452 0           unlink $file;
453             }
454 0           return;
455             }
456              
457             sub action_list_actions {
458 0     0 1   my $self = shift;
459 1     1   8 no strict 'refs';
  1         3  
  1         324  
460 0           say 'Available actions:';
461 0           say for
462 0           sort {$a cmp $b}
463 0           map {s/^action_/- /; s/_/-/g; $_ }
  0            
  0            
464 0 0         grep {/^action_/ && $self->can($_)}
465 0           keys %{ref($self)."::"};
466 0           return;
467             }
468              
469             sub last_distlist {
470 0     0 1   my ($self) = @_;
471 0           return keys %{$self->last_index()->{bare_distro}};
  0            
472             }
473              
474             sub last_modlist {
475 0     0 1   my ($self) = @_;
476             my @retval =
477 0           map { (sort keys %$_)[0] }
478 0           values %{$self->last_index()->{bare_distro}};
  0            
479             } ## end sub last_modlist
480              
481             1;
482             __END__