File Coverage

blib/lib/App/EPAN.pm
Criterion Covered Total %
statement 59 274 21.5
branch 0 70 0.0
condition 0 18 0.0
subroutine 20 43 46.5
pod 15 15 100.0
total 94 420 22.3


line stmt bran cond sub pod time code
1             package App::EPAN;
2              
3 1     1   69880 use 5.012;
  1         4  
4             { our $VERSION = '0.001002' }
5 1     1   5 use warnings;
  1         1  
  1         30  
6 1     1   639 use English qw( -no_match_vars );
  1         3871  
  1         6  
7 1     1   821 use version;
  1         2366  
  1         5  
8 1     1   665 use autodie;
  1         16362  
  1         5  
9 1     1   7673 use Getopt::Long qw< :config gnu_getopt >;
  1         12751  
  1         5  
10 1     1   710 use Pod::Usage qw< pod2usage >;
  1         49875  
  1         99  
11 1     1   634 use Dist::Metadata ();
  1         31830  
  1         31  
12 1     1   533 use Path::Class qw< file dir >;
  1         37048  
  1         82  
13 1     1   22 use Cwd qw< cwd >;
  1         2  
  1         42  
14 1     1   587 use File::Find::Rule ();
  1         8591  
  1         25  
15 1     1   708 use Compress::Zlib ();
  1         55020  
  1         35  
16 1     1   625 use Log::Log4perl::Tiny qw< :easy :dead_if_first >;
  1         15081  
  1         5  
17 1     1   976 use Moo;
  1         12045  
  1         4  
18 1     1   2732 use IPC::Run ();
  1         28741  
  1         34  
19 1     1   795 use File::Copy ();
  1         2568  
  1         39  
20 1     1   608 use File::Which qw< which >;
  1         1178  
  1         813  
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             sub action_index {
110 0     0 1   my $self = shift;
111 0           return $self->_do_index($self->target_dir);
112             }
113              
114             {
115 1     1   21 no strict 'refs';
  1         2  
  1         2093  
116             *{action_idx} = \&action_index;
117             }
118              
119             sub _save {
120 0     0     my ($self, $name, $contents, $config_key, $output) = @_;
121              
122 0 0         if (defined(my $confout = $self->config($config_key))) {
123 0 0         $output =
    0          
124             !length($confout) ? undef
125             : $confout eq '-' ? \*STDOUT
126             : file($confout);
127             } ## end if (defined(my $confout...))
128 0 0         if (defined $output) {
129 0           INFO "saving output to $output";
130 0 0         $self->_save2($output,
131             scalar(ref($contents) ? $contents->() : $contents));
132             }
133             else {
134 0           INFO "empty filename for $name file, skipping";
135             }
136             } ## end sub _save
137              
138             sub _do_index {
139 0     0     my ($self, $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           my @index = $self->_index_body_for($path);
222 0   0       our $VERSION ||= 'whateva';
223 0           my $header = <<"END_OF_HEADER";
224             File: 02packages.details.txt
225             URL: http://cpan.perl.org/modules/02packages.details.txt.gz
226             Description: Package names found in directory \$CPAN/authors/id/
227             Columns: package name, version, path
228             Intended-For: Automated fetch routines, namespace documentation.
229             Written-By: epan $VERSION
230 0           Line-Count: ${ \ scalar @index }
231 0           Last-Updated: ${ \ scalar localtime() }
232             END_OF_HEADER
233 0           return join "\n", $header, @index, '';
234             } ## end sub _index_for
235              
236             sub _collect_index_for {
237 0     0     my ($self, $path) = @_;
238 0           $path = dir($path);
239 0           my $idpath = $path->subdir(qw< authors id >);
240 0           my %data_for;
241 0           for my $file (File::Find::Rule->extras({follow => 1})->file()
242             ->in($idpath->stringify()))
243             {
244 0           INFO "indexing $file";
245 0           my $index_path =
246             file($file)->relative($idpath)->as_foreign('Unix')->stringify();
247 0           my $dm = Dist::Metadata->new(file => $file);
248 0           my $version_for = $dm->package_versions();
249              
250 0           $data_for{distro}{$index_path} = $version_for;
251 0           (my $bare_index_path = $index_path) =~
252             s{^(.)/(\1.)/(\2.*?)/}{$3/}mxs;
253 0           $data_for{bare_distro}{$bare_index_path} = $version_for;
254              
255 0           my %_localdata_for;
256 0           my $score = 0;
257 0           my $previous;
258 0           while (my ($module, $version) = each %$version_for) {
259 0   0       my $print_version = $version // 'undef';
260 0           DEBUG "data for $module: [$print_version] [$index_path]";
261 0           $_localdata_for{$module} = {
262             version => $version,
263             distro => $index_path,
264             _file => $file,
265             };
266 0 0         next if $score != 0;
267 0 0         next unless exists($data_for{module}{$module});
268 0           $previous = $data_for{module}{$module};
269 0           DEBUG 'some previous version exists';
270 0 0         if (! defined $version) {
    0          
271 0 0         $score = -1 if defined($previous->{version});
272             }
273             elsif (defined $previous->{version}) {
274 0           my $tv = version->parse($version);
275 0           my $pv = version->parse($previous->{version});
276 0           $score = $tv <=> $pv;
277             }
278 0           DEBUG "score: $score";
279             } ## end while (my ($module, $version...))
280              
281 0           DEBUG "FINAL SCORE $score";
282              
283 0 0         if ($score < 0) { # didn't win against something already in
284 0           DEBUG "marking $file as obsolete";
285 0           $data_for{obsolete}{$file} = 1;
286 0           next;
287             }
288              
289 0           DEBUG "getting $file data as winner (for the moment)";
290 0 0         if ($previous) {
291 0           my $oip = $previous->{distro};
292 0           DEBUG "marking $oip as obsolete";
293 0           $data_for{obsolete}{$previous->{_file}} = 1;
294             delete $data_for{module}{$_}
295 0           for keys %{$data_for{distro}{$oip}};
  0            
296             }
297             # copy stuff over to the "official" data for modules
298 0           $data_for{module}{$_} = $_localdata_for{$_} for keys %_localdata_for;
299             } ## end for my $file (File::Find::Rule...)
300 0           $self->last_index(\%data_for);
301 0 0         return %data_for if wantarray();
302 0           return \%data_for;
303             } ## end sub _collect_index_for
304              
305             sub _index_body_for {
306 0     0     my ($self, $path) = @_;
307              
308 0           my $data_for = $self->_collect_index_for($path);
309 0           my $module_data_for = $data_for->{module};
310 0           my @retval;
311 0           for my $module (sort keys %{$module_data_for}) {
  0            
312 0           my $md = $module_data_for->{$module};
313 0   0       my $version = $md->{version} || 'undef';
314 0           my $index_path = $md->{distro};
315 0           my $fw = 38 - length $version;
316 0 0         $fw = length $module if $fw < length $module;
317 0           push @retval, sprintf "%-${fw}s %s %s", $module, $version,
318             $index_path;
319             } ## end for my $module (sort keys...)
320 0 0         return @retval if wantarray();
321 0           return \@retval;
322             } ## end sub _index_body_for
323              
324             sub action_create {
325 0     0 1   my ($self) = @_;
326              
327 0           my $target = $self->target_dir;
328 0 0         LOGDIE "target directory $target exists, use update instead"
329             if -d $target;
330 0           $target->mkpath();
331              
332 0           return $self->action_update();
333             } ## end sub action_create
334              
335             sub action_update {
336 0     0 1   my ($self) = @_;
337              
338 0           my $target = $self->target_dir;
339 0 0         $target->mkpath() unless -d $target;
340              
341 0           my $dists = $target->stringify();
342 0           my $local = $target->subdir('local')->stringify();
343 0 0         my @command = (
344             qw< cpanm --reinstall --quiet --self-contained >,
345             ($self->execute_tests ? () : '--notest'),
346             '--local-lib-contained' => $local,
347             '--save-dists' => $dists,
348             $self->args(),
349             );
350              
351 0           my ($out, $err);
352             {
353 0           local $SIG{TERM} = sub {
354 0     0     WARN "cpanm: received TERM signal, ignoring";
355 0           };
356 0           INFO "calling @command";
357 0 0         IPC::Run::run \@command, \undef, \*STDOUT, \*STDERR
358             or LOGDIE "cpanm: $? ($err)";
359             }
360              
361 0           INFO 'onboarding completed, indexing...';
362 0           $self->_do_index($target);
363 0           my $data_for = $self->last_index();
364              
365 0           INFO 'saving distlist';
366 0           my @distros = $self->last_distlist();
367 0           $self->_save2($target->file('distlist.txt'), join "\n", @distros, '');
368              
369 0           INFO 'saving modlist';
370 0           my @modules = $self->last_modlist();
371 0           $self->_save2($target->file('modlist.txt'), join "\n", @modules, '');
372              
373 0           my $file = $target->file('install.sh');
374 0 0         if (!-e $file) {
375 0           $self->_save2($file, <<'END_OF_INSTALL');
376             #!/bin/bash
377             ME=$(readlink -f "$0")
378             MYDIR=$(dirname "$ME")
379              
380             TARGET="$MYDIR/local"
381             [ $# -gt 0 ] && TARGET=$1
382              
383             if [ -n "$TARGET" ]; then
384             "$MYDIR/cpanm" --mirror "file://$MYDIR" --mirror-only \
385             -L "$TARGET" \
386             $(<"$MYDIR/modlist.txt")
387             else
388             "$MYDIR/cpanm" --mirror "file://$MYDIR" --mirror-only \
389             $(<"$MYDIR/modlist.txt")
390             fi
391             END_OF_INSTALL
392 0           chmod 0777 & ~umask(), $file->stringify();
393             } ## end if (!-e $file)
394              
395 0           $file = $target->file('cpanm');
396 0 0         if (!-e $file) {
397 0           my $cpanm = which('cpanm');
398 0           File::Copy::copy($cpanm, $file->stringify());
399 0           chmod 0777 & ~umask(), $file->stringify();
400             }
401             } ## end sub action_update
402              
403             {
404 1     1   12 no strict 'subs';
  1         2  
  1         642  
405             *action_install = \&action_update;
406             *action_add = \&action_update;
407             }
408              
409             sub action_inject {
410 0     0 1   my ($self) = @_;
411              
412 0           my $target = $self->target_dir;
413 0 0         $target->mkpath() unless -d $target;
414              
415 0   0       my $author = $self->config('author') // $ENV{EPAN_AUTHOR} // 'LOCAL';
      0        
416 0           my $first = substr $author, 0, 1;
417 0           my $first_two = substr $author, 0, 2;
418 0           my $repo = $target->subdir(qw< authors id >, $first, $first_two, $author);
419 0           $repo->mkpath;
420 0           $repo = $repo->stringify;
421              
422 0           File::Copy::copy($_, $repo) for $self->args;
423              
424 0           INFO 'onboarding completed, indexing...';
425 0           $self->_do_index($target);
426              
427 0           return;
428             }
429              
430             sub action_list_obsoletes {
431 0     0 1   my ($self) = @_;
432 0           my $basedir = $self->target_dir;
433 0           my $data_for = $self->_collect_index_for($basedir);
434 0           my @obsoletes = sort {$a cmp $b} keys %{$data_for->{obsolete}};
  0            
  0            
435 0           say for @obsoletes;
436 0           return;
437             }
438              
439             sub action_purge_obsoletes {
440 0     0 1   my ($self) = @_;
441 0           my $basedir = $self->target_dir;
442 0           my $data_for = $self->_collect_index_for($basedir);
443 0           my @obsoletes = sort {$a cmp $b} keys %{$data_for->{obsolete}};
  0            
  0            
444 0           for my $file (@obsoletes) {
445 0           INFO "removing $file";
446 0           unlink $file;
447             }
448 0           return;
449             }
450              
451             sub action_list_actions {
452 0     0 1   my $self = shift;
453 1     1   9 no strict 'refs';
  1         4  
  1         353  
454 0           say 'Available actions:';
455 0           say for
456 0           sort {$a cmp $b}
457 0           map {s/^action_/- /; s/_/-/g; $_ }
  0            
  0            
458 0 0         grep {/^action_/ && $self->can($_)}
459 0           keys %{ref($self)."::"};
460 0           return;
461             }
462              
463             sub last_distlist {
464 0     0 1   my ($self) = @_;
465 0           return keys %{$self->last_index()->{bare_distro}};
  0            
466             }
467              
468             sub last_modlist {
469 0     0 1   my ($self) = @_;
470             my @retval =
471 0           map { (sort keys %$_)[0] }
472 0           values %{$self->last_index()->{bare_distro}};
  0            
473             } ## end sub last_modlist
474              
475             1;
476             __END__