File Coverage

blib/lib/App/EPAN.pm
Criterion Covered Total %
statement 59 278 21.2
branch 0 70 0.0
condition 0 18 0.0
subroutine 20 43 46.5
pod 15 15 100.0
total 94 424 22.1


line stmt bran cond sub pod time code
1             package App::EPAN;
2              
3 1     1   72824 use 5.012;
  1         4  
4             { our $VERSION = '0.001001' }
5 1     1   6 use warnings;
  1         2  
  1         29  
6 1     1   725 use English qw( -no_match_vars );
  1         4016  
  1         6  
7 1     1   869 use version;
  1         2153  
  1         5  
8 1     1   699 use autodie;
  1         16936  
  1         5  
9 1     1   7871 use Getopt::Long qw< :config gnu_getopt >;
  1         13439  
  1         5  
10 1     1   820 use Pod::Usage qw< pod2usage >;
  1         53349  
  1         147  
11 1     1   767 use Dist::Metadata ();
  1         34697  
  1         34  
12 1     1   728 use Path::Class qw< file dir >;
  1         41583  
  1         98  
13 1     1   25 use Cwd qw< cwd >;
  1         2  
  1         42  
14 1     1   858 use File::Find::Rule ();
  1         9046  
  1         38  
15 1     1   907 use Compress::Zlib ();
  1         62777  
  1         41  
16 1     1   839 use Log::Log4perl::Tiny qw< :easy :dead_if_first >;
  1         17348  
  1         8  
17 1     1   1223 use Moo;
  1         13065  
  1         7  
18 1     1   2998 use IPC::Run ();
  1         28040  
  1         34  
19 1     1   682 use File::Copy ();
  1         2612  
  1         39  
20 1     1   681 use File::Which qw< which >;
  1         1061  
  1         793  
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   39 no strict 'refs';
  1         3  
  1         2245  
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           my $_03modlist_data_1 = <<'END_OF_03_MODLIST_DATA_1';
160             File: 03modlist.data
161             Description: These are the data that are published in the module
162             list, but they may be more recent than the latest posted
163             modulelist. Over time we'll make sure that these data
164             can be used to print the whole part two of the
165             modulelist. Currently this is not the case.
166             Modcount: 0
167             Written-By: PAUSE version 1.005
168             Date: Sun, 28 Jul 2013 07:41:15 GMT
169              
170             pac
171             END_OF_03_MODLIST_DATA_1
172 0           my $_03modlist_data_2 = <<'END_OF_03_MODLIST_DATA_2';
173             kage CPAN::Modulelist;
174             # Usage: print Data::Dumper->new([CPAN::Modulelist->data])->Dump or similar
175             # cannot 'use strict', because we normally run under Safe
176             # use strict;
177             sub data {
178             my $result = {};
179             my $primary = "modid";
180             for (@$CPAN::Modulelist::data){
181             my %hash;
182             @hash{@$CPAN::Modulelist::cols} = @$_;
183             $result->{$hash{$primary}} = \%hash;
184             }
185             return $result;
186             }
187             $CPAN::Modulelist::cols = [ ];
188             $CPAN::Modulelist::data = [ ];
189             END_OF_03_MODLIST_DATA_2
190              
191 0           $_03modlist_data_1 =~ s{\s+\z}{}mxs;
192 0           $_03modlist_data_2 =~ s{\A\s+}{}mxs;
193 0           $self->_save(
194             '03modlist.data', # name
195             "$_03modlist_data_1$_03modlist_data_2",
196             'modlist', # configuration key to look output file
197             $basedir->file(qw< modules 03modlist.data.gz >) # default
198             );
199             } ## end sub _do_index
200              
201             sub _save2 {
202 0     0     my ($self, $path, $contents) = @_;
203 0           my ($fh, $is_gz);
204 0 0         if (ref($path) eq 'GLOB') {
205 0           $fh = $path;
206 0           $is_gz = 0;
207             }
208             else {
209 0 0         $path->dir()->mkpath() unless -d $path->dir()->stringify();
210 0           $fh = $path->open('>');
211 0           $is_gz = $path->stringify() =~ m{\.gz$}mxs;
212             }
213              
214 0 0         if ($is_gz) {
215 0           my $gz = Compress::Zlib::gzopen($fh, 'wb');
216 0           $gz->gzwrite($contents);
217 0           $gz->gzclose();
218             }
219             else {
220 0           binmode $fh;
221 0           print {$fh} $contents;
  0            
222             }
223 0           return;
224             } ## end sub _save2
225              
226             sub _index_for {
227 0     0     my ($self, $path) = @_;
228 0           my @index = $self->_index_body_for($path);
229 0   0       our $VERSION ||= 'whateva';
230 0           my $header = <<"END_OF_HEADER";
231             File: 02packages.details.txt
232             URL: http://cpan.perl.org/modules/02packages.details.txt.gz
233             Description: Package names found in directory \$CPAN/authors/id/
234             Columns: package name, version, path
235             Intended-For: Automated fetch routines, namespace documentation.
236             Written-By: epan $VERSION
237 0           Line-Count: ${ \ scalar @index }
238 0           Last-Updated: ${ \ scalar localtime() }
239             END_OF_HEADER
240 0           return join "\n", $header, @index, '';
241             } ## end sub _index_for
242              
243             sub _collect_index_for {
244 0     0     my ($self, $path) = @_;
245 0           $path = dir($path);
246 0           my $idpath = $path->subdir(qw< authors id >);
247 0           my %data_for;
248 0           for my $file (File::Find::Rule->extras({follow => 1})->file()
249             ->in($idpath->stringify()))
250             {
251 0           INFO "indexing $file";
252 0           my $index_path =
253             file($file)->relative($idpath)->as_foreign('Unix')->stringify();
254 0           my $dm = Dist::Metadata->new(file => $file);
255 0           my $version_for = $dm->package_versions();
256              
257 0           $data_for{distro}{$index_path} = $version_for;
258 0           (my $bare_index_path = $index_path) =~
259             s{^(.)/(\1.)/(\2.*?)/}{$3/}mxs;
260 0           $data_for{bare_distro}{$bare_index_path} = $version_for;
261              
262 0           my %_localdata_for;
263 0           my $score = 0;
264 0           my $previous;
265 0           while (my ($module, $version) = each %$version_for) {
266 0   0       my $print_version = $version // 'undef';
267 0           DEBUG "data for $module: [$print_version] [$index_path]";
268 0           $_localdata_for{$module} = {
269             version => $version,
270             distro => $index_path,
271             _file => $file,
272             };
273 0 0         next if $score != 0;
274 0 0         next unless exists($data_for{module}{$module});
275 0           $previous = $data_for{module}{$module};
276 0           DEBUG 'some previous version exists';
277 0 0         if (! defined $version) {
    0          
278 0 0         $score = -1 if defined($previous->{version});
279             }
280             elsif (defined $previous->{version}) {
281 0           my $tv = version->parse($version);
282 0           my $pv = version->parse($previous->{version});
283 0           $score = $tv <=> $pv;
284             }
285 0           DEBUG "score: $score";
286             } ## end while (my ($module, $version...))
287              
288 0           DEBUG "FINAL SCORE $score";
289              
290 0 0         if ($score < 0) { # didn't win against something already in
291 0           DEBUG "marking $file as obsolete";
292 0           $data_for{obsolete}{$file} = 1;
293 0           next;
294             }
295              
296 0           DEBUG "getting $file data as winner (for the moment)";
297 0 0         if ($previous) {
298 0           my $oip = $previous->{distro};
299 0           DEBUG "marking $oip as obsolete";
300 0           $data_for{obsolete}{$previous->{_file}} = 1;
301             delete $data_for{module}{$_}
302 0           for keys %{$data_for{distro}{$oip}};
  0            
303             }
304             # copy stuff over to the "official" data for modules
305 0           $data_for{module}{$_} = $_localdata_for{$_} for keys %_localdata_for;
306             } ## end for my $file (File::Find::Rule...)
307 0           $self->last_index(\%data_for);
308 0 0         return %data_for if wantarray();
309 0           return \%data_for;
310             } ## end sub _collect_index_for
311              
312             sub _index_body_for {
313 0     0     my ($self, $path) = @_;
314              
315 0           my $data_for = $self->_collect_index_for($path);
316 0           my $module_data_for = $data_for->{module};
317 0           my @retval;
318 0           for my $module (sort keys %{$module_data_for}) {
  0            
319 0           my $md = $module_data_for->{$module};
320 0   0       my $version = $md->{version} || 'undef';
321 0           my $index_path = $md->{distro};
322 0           my $fw = 38 - length $version;
323 0 0         $fw = length $module if $fw < length $module;
324 0           push @retval, sprintf "%-${fw}s %s %s", $module, $version,
325             $index_path;
326             } ## end for my $module (sort keys...)
327 0 0         return @retval if wantarray();
328 0           return \@retval;
329             } ## end sub _index_body_for
330              
331             sub action_create {
332 0     0 1   my ($self) = @_;
333              
334 0           my $target = $self->target_dir;
335 0 0         LOGDIE "target directory $target exists, use update instead"
336             if -d $target;
337 0           $target->mkpath();
338              
339 0           return $self->action_update();
340             } ## end sub action_create
341              
342             sub action_update {
343 0     0 1   my ($self) = @_;
344              
345 0           my $target = $self->target_dir;
346 0 0         $target->mkpath() unless -d $target;
347              
348 0           my $dists = $target->stringify();
349 0           my $local = $target->subdir('local')->stringify();
350 0 0         my @command = (
351             qw< cpanm --reinstall --quiet --self-contained >,
352             ($self->execute_tests ? () : '--notest'),
353             '--local-lib-contained' => $local,
354             '--save-dists' => $dists,
355             $self->args(),
356             );
357              
358 0           my ($out, $err);
359             {
360 0           local $SIG{TERM} = sub {
361 0     0     WARN "cpanm: received TERM signal, ignoring";
362 0           };
363 0           INFO "calling @command";
364 0 0         IPC::Run::run \@command, \undef, \*STDOUT, \*STDERR
365             or LOGDIE "cpanm: $? ($err)";
366             }
367              
368 0           INFO 'onboarding completed, indexing...';
369 0           $self->_do_index($target);
370 0           my $data_for = $self->last_index();
371              
372 0           INFO 'saving distlist';
373 0           my @distros = $self->last_distlist();
374 0           $self->_save2($target->file('distlist.txt'), join "\n", @distros, '');
375              
376 0           INFO 'saving modlist';
377 0           my @modules = $self->last_modlist();
378 0           $self->_save2($target->file('modlist.txt'), join "\n", @modules, '');
379              
380 0           my $file = $target->file('install.sh');
381 0 0         if (!-e $file) {
382 0           $self->_save2($file, <<'END_OF_INSTALL');
383             #!/bin/bash
384             ME=$(readlink -f "$0")
385             MYDIR=$(dirname "$ME")
386              
387             TARGET="$MYDIR/local"
388             [ $# -gt 0 ] && TARGET=$1
389              
390             if [ -n "$TARGET" ]; then
391             "$MYDIR/cpanm" --mirror "file://$MYDIR" --mirror-only \
392             -L "$TARGET" \
393             $(<"$MYDIR/modlist.txt")
394             else
395             "$MYDIR/cpanm" --mirror "file://$MYDIR" --mirror-only \
396             $(<"$MYDIR/modlist.txt")
397             fi
398             END_OF_INSTALL
399 0           chmod 0777 & ~umask(), $file->stringify();
400             } ## end if (!-e $file)
401              
402 0           $file = $target->file('cpanm');
403 0 0         if (!-e $file) {
404 0           my $cpanm = which('cpanm');
405 0           File::Copy::copy($cpanm, $file->stringify());
406 0           chmod 0777 & ~umask(), $file->stringify();
407             }
408             } ## end sub action_update
409              
410             {
411 1     1   13 no strict 'subs';
  1         2  
  1         525  
412             *action_install = \&action_update;
413             *action_add = \&action_update;
414             }
415              
416             sub action_inject {
417 0     0 1   my ($self) = @_;
418              
419 0           my $target = $self->target_dir;
420 0 0         $target->mkpath() unless -d $target;
421              
422 0   0       my $author = $self->config('author') // $ENV{EPAN_AUTHOR} // 'LOCAL';
      0        
423 0           my $first = substr $author, 0, 1;
424 0           my $first_two = substr $author, 0, 2;
425 0           my $repo = $target->subdir(qw< authors id >, $first, $first_two, $author);
426 0           $repo->mkpath;
427 0           $repo = $repo->stringify;
428              
429 0           File::Copy::copy($_, $repo) for $self->args;
430              
431 0           INFO 'onboarding completed, indexing...';
432 0           $self->_do_index($target);
433              
434 0           return;
435             }
436              
437             sub action_list_obsoletes {
438 0     0 1   my ($self) = @_;
439 0           my $basedir = $self->target_dir;
440 0           my $data_for = $self->_collect_index_for($basedir);
441 0           my @obsoletes = sort {$a cmp $b} keys %{$data_for->{obsolete}};
  0            
  0            
442 0           say for @obsoletes;
443 0           return;
444             }
445              
446             sub action_purge_obsoletes {
447 0     0 1   my ($self) = @_;
448 0           my $basedir = $self->target_dir;
449 0           my $data_for = $self->_collect_index_for($basedir);
450 0           my @obsoletes = sort {$a cmp $b} keys %{$data_for->{obsolete}};
  0            
  0            
451 0           for my $file (@obsoletes) {
452 0           INFO "removing $file";
453 0           unlink $file;
454             }
455 0           return;
456             }
457              
458             sub action_list_actions {
459 0     0 1   my $self = shift;
460 1     1   9 no strict 'refs';
  1         3  
  1         354  
461 0           say 'Available actions:';
462 0           say for
463 0           sort {$a cmp $b}
464 0           map {s/^action_/- /; s/_/-/g; $_ }
  0            
  0            
465 0 0         grep {/^action_/ && $self->can($_)}
466 0           keys %{ref($self)."::"};
467 0           return;
468             }
469              
470             sub last_distlist {
471 0     0 1   my ($self) = @_;
472 0           return keys %{$self->last_index()->{bare_distro}};
  0            
473             }
474              
475             sub last_modlist {
476 0     0 1   my ($self) = @_;
477             my @retval =
478 0           map { (sort keys %$_)[0] }
479 0           values %{$self->last_index()->{bare_distro}};
  0            
480             } ## end sub last_modlist
481              
482             1;
483             __END__