File Coverage

blib/lib/App/MechaCPAN.pm
Criterion Covered Total %
statement 239 305 78.3
branch 69 116 59.4
condition 4 33 12.1
subroutine 37 39 94.8
pod 0 9 0.0
total 349 502 69.5


line stmt bran cond sub pod time code
1             package App::MechaCPAN;
2              
3 20     20   13004 use v5.14;
  20         65  
4 20     20   96 use strict;
  20         44  
  20         458  
5 20     20   105 use Cwd qw/cwd/;
  20         37  
  20         1048  
6 20     20   108 use Carp;
  20         36  
  20         969  
7 20     20   101 use Config;
  20         44  
  20         724  
8 20     20   3016 use Symbol qw/geniosym/;
  20         5355  
  20         978  
9 20     20   7418 use autodie;
  20         226704  
  20         88  
10 20     20   135731 use Term::ANSIColor qw//;
  20         107252  
  20         586  
11 20     20   8288 use IPC::Open3;
  20         40539  
  20         885  
12 20     20   7481 use IO::Select;
  20         23156  
  20         806  
13 20     20   134 use List::Util qw/first/;
  20         43  
  20         1858  
14 20     20   4706 use File::Temp qw/tempfile tempdir/;
  20         90113  
  20         933  
15 20     20   115 use File::Spec qw//;
  20         44  
  20         251  
16 20     20   10867 use Archive::Tar;
  20         1267705  
  20         1126  
17 20     20   12220 use Getopt::Long qw//;
  20         148450  
  20         592  
18              
19 20     20   141 use Exporter qw/import/;
  20         42  
  20         1119  
20              
21             BEGIN
22             {
23             our @EXPORT_OK
24 20     20   87 = qw/url_re info success dest_dir inflate_archive run restart_script/;
25 20         25271 our %EXPORT_TAGS = ( go => [@EXPORT_OK] );
26             }
27              
28             our $VERSION = '0.19';
29              
30             require App::MechaCPAN::Perl;
31             require App::MechaCPAN::Install;
32             require App::MechaCPAN::Deploy;
33              
34             my $loaded_at_compile;
35             my $restarted_key = 'APP_MECHACPAN_RESTARTED';
36             my $is_restarted_process = delete $ENV{$restarted_key};
37             INIT
38             {
39             $loaded_at_compile = 1;
40             &restart_script();
41             }
42              
43             $loaded_at_compile //= 0;
44              
45             our @args = (
46             'diag-run!',
47             'verbose|v!',
48             'quiet|q!',
49             'no-log!',
50             'directory|d=s',
51             );
52              
53             # Timeout when there's no output in seconds
54             our $TIMEOUT = $ENV{MECHACPAN_TIMEOUT} // 60;
55             our $VERBOSE; # Print output from sub commands to STDERR
56             our $QUIET; # Do not print any progress to STDERR
57             our $LOGFH; # File handle to send the logs to
58             our $LOG_ON = 1; # Default if to log or not
59              
60             sub main
61             {
62 31     31 0 85993 my @argv = @_;
63              
64 31 50       423 if ( $0 =~ m/zhuli/ )
65             {
66 0 0       0 if ( $argv[0] =~ m/^do the thing/i )
67             {
68 0 0       0 success( "zhuli$$", 'Running deployment' )
69             unless $is_restarted_process;
70 0         0 $argv[0] = 'deploy';
71             }
72 0 0 0     0 if ( $argv[0] =~ m/^do$/i
      0        
73             && $argv[1] =~ m/^the$/i
74             && $argv[2] =~ m/^thing$/i )
75             {
76 0 0       0 success( "zhuli$$", 'Running deployment' )
77             unless $is_restarted_process;
78 0         0 @argv = ( 'deploy', @argv[ 3 .. $#argv ] );
79             }
80             }
81              
82 31         806 my @args = (
83             @App::MechaCPAN::args,
84             @App::MechaCPAN::Perl::args,
85             @App::MechaCPAN::Install::args,
86             @App::MechaCPAN::Deploy::args,
87             );
88 31         92 @args = keys %{ { map { $_ => 1 } @args } };
  31         114  
  558         2044  
89              
90 31         228 my $options = {};
91 31         317 my $getopt_ret
92             = Getopt::Long::GetOptionsFromArray( \@argv, $options, @args );
93 31 50       38625 return -1
94             if !$getopt_ret;
95              
96             my $merge_options = sub
97             {
98 59     59   144 my $arg = shift;
99 59 100       194 if ( ref $arg eq 'HASH' )
100             {
101 5         36 $options = { %$arg, %$options };
102 5         23 return 0;
103             }
104 54         176 return 1;
105 31         676 };
106              
107 31         120 @argv = grep { $merge_options->($_) } @argv;
  59         152  
108              
109 31         86622 my $orig_dir = cwd;
110 31 100       411 if ( exists $options->{directory} )
111             {
112 3 100       104 if ( !-d $options->{directory} )
113             {
114 1         88 die "Cannot find directory: $options->{directory}\n";
115             }
116 2         34 chdir $options->{directory};
117             }
118              
119 30         2189 local $LOGFH;
120 30   33     303 local $VERBOSE = $options->{verbose} // $VERBOSE;
121 30   33     222 local $QUIET = $options->{quiet} // $QUIET;
122              
123 30         193 my $dest_dir = &dest_dir;
124 30         251 my $cmd = ucfirst lc shift @argv;
125 30         183 my $pkg = join( '::', __PACKAGE__, $cmd );
126 30         135 my $action = eval { $pkg->can('go') };
  30         876  
127              
128 30 50       178 if ( !defined $action )
129             {
130 0         0 warn "Could not find action to run: $cmd\n";
131 0         0 return -1;
132             }
133              
134 30 100       177 if ( $options->{'diag-run'} )
135             {
136 4         99 warn "Would run '$cmd'\n";
137 4         47 chdir $orig_dir;
138 4         2145 return 0;
139             }
140              
141 26         180 $options->{is_restarted_process} = $is_restarted_process;
142              
143 26 100       920 if ( !-d $dest_dir )
144             {
145 17         212 mkdir $dest_dir;
146             }
147              
148 26 50       294 unless ( $options->{'no-log'} )
149             {
150 26         103 my $log_dir = "$dest_dir/logs";
151 26 100       466 if ( !-d $log_dir )
152             {
153 17         87 mkdir $log_dir;
154             }
155              
156 26         10027 my $log_path;
157 26         457 ( $LOGFH, $log_path ) = tempfile( "$log_dir/log.$$.XXXX", UNLINK => 0 );
158             }
159              
160 26 50       11700 my $ret = eval { $pkg->$action( $options, @argv ) || 0; };
  26         297  
161 26         2284 chdir $orig_dir;
162              
163 26 100       2589 if ( !defined $ret )
164             {
165 5         133 warn $@;
166 5         3757 return -1;
167             }
168              
169 21         229 return $ret;
170             }
171              
172             sub url_re
173             {
174 1     1 0 5 state $url_re = qr[
175             ^
176             (?: ftp | http | https | file )
177             :
178             ]xmsi;
179 1         8 return $url_re;
180             }
181              
182             sub info
183             {
184 279     279 0 727 my $key = shift;
185 279         641 my $line = shift;
186              
187 279 100       1103 if ( !defined $line )
188             {
189 1         2 $line = $key;
190 1         5 undef $key;
191             }
192              
193 279         1141 status( $key, 'YELLOW', $line );
194             }
195              
196             sub success
197             {
198 35     35 0 119 my $key = shift;
199 35         184 my $line = shift;
200              
201 35 100       248 if ( !defined $line )
202             {
203 1         3 $line = $key;
204 1         4 undef $key;
205             }
206              
207 35         165 status( $key, 'GREEN', $line );
208             }
209              
210             my $RESET = Term::ANSIColor::color('RESET');
211             my $BOLD = Term::ANSIColor::color('BOLD');
212              
213             sub _show_line
214             {
215 0     0   0 my $key = shift;
216 0         0 my $color = shift;
217 0         0 my $line = shift;
218              
219             # Clean up the line
220 0         0 $line =~ s/\n/ /xmsg;
221              
222 0         0 state @key_lines;
223              
224 0     0   0 my $idx = first { $key_lines[$_] eq $key } 0 .. $#key_lines;
  0         0  
225              
226 0 0       0 if ( !defined $key )
227             {
228 0         0 $idx = -1;
229             }
230              
231 0 0       0 if ( !defined $idx )
232             {
233 0         0 unshift @key_lines, $key;
234 0         0 $idx = 0;
235              
236             # Scroll Up 1 line
237 0         0 print STDERR "\n";
238             }
239 0         0 $idx++;
240              
241             # Don't bother with fancy line movements if we are verbose
242 0 0       0 if ($VERBOSE)
243             {
244 0         0 print STDERR "$color$line$RESET\n";
245 0         0 return;
246             }
247              
248             # We use some ANSI escape codes, so they are:
249             # \e[.F - Move up from current line, which is always the end of the list
250             # \e[K - Clear the line
251             # $color - Colorize the text
252             # $line - Print the text
253             # $RESET - Reset the colorize
254             # \e[.E - Move down from the current line, back to the end of the list
255 0         0 print STDERR "\e[${idx}F";
256 0         0 print STDERR "\e[K";
257 0         0 print STDERR "$color$line$RESET";
258 0         0 print STDERR "\e[${idx}E";
259              
260 0         0 return;
261             }
262              
263             sub status
264             {
265 314     314 0 786 my $key = shift;
266 314         820 my $color = shift;
267 314         655 my $line = shift;
268              
269 314 50       1022 if ( !defined $line )
270             {
271 0         0 $line = $color;
272 0         0 $color = 'RESET';
273             }
274              
275             return
276 314 50       1506 if $QUIET;
277              
278 0   0     0 $color = eval { Term::ANSIColor::color($color) } // $RESET;
  0         0  
279              
280 0         0 state @last_key;
281              
282             # Undo the last line that is bold
283 0 0 0     0 if ( @last_key && !$VERBOSE )
284             {
285 0         0 _show_line(@last_key);
286             }
287              
288 0         0 _show_line( $key, $color . $BOLD, $line );
289              
290 0         0 @last_key = ( $key, $color, $line );
291             }
292 20 100   20   13963 END { print STDERR "\n" unless $QUIET; }
293             INIT { print STDERR "\n" unless $QUIET; }
294              
295             package MechaCPAN::DestGuard
296             {
297 20     20   152 use Cwd qw/cwd/;
  20         45  
  20         926  
298 20     20   116 use Scalar::Util qw/refaddr weaken/;
  20         46  
  20         1461  
299 20     20   121 use overload '""' => sub { my $s = shift; return $$s }, fallback => 1;
  20     193   45  
  20         353  
  193         16980  
  193         4106  
300             my $dest_dir;
301              
302             sub get
303             {
304 94     94   290 my $result = $dest_dir;
305 94 100       499 if ( !defined $result )
306             {
307 45         117316 my $pwd = cwd;
308 45         517 $dest_dir = \"$pwd/local";
309 45         197 bless $dest_dir;
310 45         129 $result = $dest_dir;
311 45         325 weaken $dest_dir;
312             }
313 94         416 return $dest_dir;
314             }
315              
316             sub DESTROY
317             {
318 45     45   3515 undef $dest_dir;
319             }
320             }
321              
322             sub dest_dir
323             {
324 94     94 0 561 my $result = MechaCPAN::DestGuard::get();
325 94         784 return $result;
326             }
327              
328             sub inflate_archive
329             {
330 36     36 0 141 my $src = shift;
331              
332             # $src can be a file path or a URL.
333 36 50       578 if ( !-e $src )
334             {
335 0         0 local $File::Fetch::WARN;
336 0         0 my $ff = File::Fetch->new( uri => $src );
337 0 0       0 $ff->scheme('http')
338             if $ff->scheme eq 'https';
339 0         0 my $content = '';
340 0         0 my $where = $ff->fetch( to => \$content );
341 0 0 0     0 die $ff->error || "Could not download $src"
342             if !defined $where;
343 0         0 $src = $where;
344             }
345              
346 36         1696 my $dir = tempdir(
347             TEMPLATE => File::Spec->tmpdir . '/mechacpan_XXXXXXXX',
348             CLEANUP => 1
349             );
350 36         129781 my $orig = cwd;
351              
352 36         366 my $error_free = eval {
353 36         526 chdir $dir;
354 36         19001 my $tar = Archive::Tar->new;
355 36         1057 $tar->error(1);
356 36         766 my $ret = $tar->read( "$src", 1, { extract => 1 } );
357 36 50       7499325 die $tar->error
358             unless $ret;
359 36         1107 1;
360             };
361 36         144 my $err = $@;
362              
363 36         336 chdir $orig;
364              
365 36 50       3489 die $err
366             unless $error_free;
367              
368 36         474 return $dir;
369             }
370              
371             sub _genio
372             {
373 280     280   609 state $iswin32 = $^O eq 'MSWin32';
374 280         622 my $write_hdl;
375             my $read_hdl;
376              
377 280 50       794 if ($iswin32)
378             {
379 20     20   16929 use Socket;
  20         54168  
  20         27256  
380 0         0 socketpair( $read_hdl, $write_hdl, AF_UNIX, SOCK_STREAM, PF_UNSPEC );
381 0         0 shutdown( $read_hdl, 1 );
382 0         0 shutdown( $write_hdl, 0 );
383             }
384             else
385             {
386 280         1431 $write_hdl = $read_hdl = geniosym;
387             }
388              
389 280         9669 $write_hdl->blocking(0);
390 280         1962 $write_hdl->autoflush(1);
391 280         14242 $read_hdl->blocking(0);
392 280         980 $read_hdl->autoflush(1);
393              
394 280         8473 return ( $read_hdl, $write_hdl );
395             }
396              
397             sub run
398             {
399 140     140 0 3701 my $cmd = shift;
400 140         627 my @args = @_;
401              
402 140         422 my $out = "";
403 140         396 my $err = "";
404              
405 140         399 my $dest_out_fh = $LOGFH;
406 140         317 my $dest_err_fh = $LOGFH;
407 140         349 my $print_output = $VERBOSE;
408 140         410 my $wantoutput = defined wantarray;
409              
410 140 100       638 if ( ref $cmd eq 'GLOB' )
411             {
412 2         11 $dest_out_fh = $cmd;
413 2         13 $cmd = shift @args;
414             }
415              
416             # If the output is asked for (non-void context), don't show it anywhere
417 140 100       645 if ($wantoutput)
418             {
419 1         11 open $dest_out_fh, ">", \$out;
420 1         4036 open $dest_err_fh, ">", \$err;
421 1         70 undef $print_output;
422             }
423              
424 140         876 my ( $output, $output_chld ) = _genio;
425 140         556 my ( $error, $error_chld ) = _genio;
426              
427 140 50       631 warn( join( "\t", $cmd, @args ) . "\n" )
428             if $VERBOSE;
429              
430 140 100       1228 print $dest_err_fh ( 'Running: ', join( "\t", $cmd, @args ) . "\n" )
431             if defined $dest_err_fh;
432              
433 140 50       1195 my $pid = open3(
    50          
434             undef,
435             $output_chld->fileno ? '>&' . $output_chld->fileno : $output_chld,
436             $error_chld->fileno ? '>&' . $error_chld->fileno : $error_chld,
437             $cmd, @args
438             );
439 140         535047 undef $output_chld;
440 140         522 undef $error_chld;
441              
442 140         1969 my $select = IO::Select->new;
443              
444 140         2937 $select->add( $output, $error );
445              
446 140         12477 my $alrm_code = "TIMEOUT\n";
447 140     1   3445 local $SIG{ALRM} = sub { die $alrm_code };
  1         3000155  
448 140         449 local $@;
449              
450 140         478 eval {
451 140         710 alarm $TIMEOUT;
452 140         838 while ( my @ready = $select->can_read )
453             {
454 2150         1172224996 alarm $TIMEOUT;
455 2150         5487 foreach my $fh (@ready)
456             {
457 2280         22207 my $line = <$fh>;
458              
459 2280 100       9069 if ( !defined $line )
460             {
461 278         1645 $select->remove($fh);
462 278         15436 next;
463             }
464              
465 2002 50       5681 print STDERR $line if $print_output;
466              
467 2002 100       8249 if ( $fh eq $output )
468             {
469 1183 100       5536 print $dest_out_fh $line
470             if defined $dest_out_fh;
471 1183 100       5527 $out .= $line
472             unless $wantoutput;
473             }
474              
475 2002 100       11952 if ( $fh eq $error )
476             {
477 819 100       2066 print $dest_err_fh $line
478             if defined $dest_err_fh;
479 819 50       3617 $err .= $line
480             unless $wantoutput;
481             }
482              
483             }
484             }
485             };
486              
487 140         1749 my $error = $@;
488 140         499 alarm 0;
489              
490 140 100       670 if ( $error eq $alrm_code )
491             {
492 1         22 info "Idle timeout (${TIMEOUT}s) exceeded, killing";
493 1         11 kill "KILL", $pid;
494             }
495              
496 140         5093 waitpid( $pid, 0 );
497              
498 140 100       1155 if ($?)
499             {
500 6         56 my $code = qq/Exit Code: / . ( $? >> 8 );
501 6 100       60 my $sig = ( $? & 127 ) ? qq/Signal: / . ( $? & 127 ) : '';
502 6 50       52 my $core = $? & 128 ? 'Core Dumped' : '';
503              
504 6         89 croak ""
505             . Term::ANSIColor::color('RED')
506             . qq/\nCould not execute '/
507             . join( ' ', $cmd, @args ) . qq/'/
508             . qq/\nPID: $pid/
509             . qq/\t$code/
510             . qq/\t$sig/
511             . qq/\t$core/
512             . Term::ANSIColor::color('GREEN')
513             . qq/\n$out/
514             . Term::ANSIColor::color('YELLOW')
515             . qq/\n$err/
516             . Term::ANSIColor::color('RESET') . "\n";
517             }
518              
519             return
520 134 100       6785 if !defined wantarray;
521              
522 1 50       7 if (wantarray)
523             {
524 1         55 return split( /\r?\n/, $out );
525             }
526              
527 0         0 return $out;
528             }
529              
530             sub restart_script
531             {
532 1     1 0 5 my $dest_dir = &dest_dir;
533 1         4 my $local_perl = File::Spec->canonpath("$dest_dir/perl/bin/perl");
534 1         8 my $this_perl = File::Spec->canonpath($^X);
535 1 50       12 if ( $^O ne 'VMS' )
536             {
537             $this_perl .= $Config{_exe}
538 1 50       31 unless $this_perl =~ m/$Config{_exe}$/i;
539             $local_perl .= $Config{_exe}
540 1 50       17 unless $local_perl =~ m/$Config{_exe}$/i;
541             }
542              
543 1         2905 state $orig_cwd = cwd;
544 1         12 state $orig_0 = $0;
545              
546 1         2966 my $current_cwd = cwd;
547 1         23 chdir $orig_cwd;
548              
549 1 0 33     115 if (
      33        
      0        
      0        
550             $loaded_at_compile # IF we were loaded during compile-time
551             && -e -x $local_perl # AND the local perl is there
552             && $this_perl ne $local_perl # AND if we're not running it
553             && -e -f -r $0 # AND we are a readable file
554             && !$^P # AND we're not debugging
555             )
556             {
557             # ReExecute using the local perl
558 0         0 my @inc_add;
559 0         0 my @paths = qw/
560             sitearchexp sitelibexp
561             vendorarchexp vendorlibexp
562             archlibexp privlibexp
563             otherlibdirsexp
564             /;
565 0         0 my %site_inc = map { $_ => 1 } @Config{@paths}, '.';
  0         0  
566              
567 0         0 foreach my $lib ( split ':', $ENV{PERL5LIB} )
568             {
569 0         0 $site_inc{$lib} = 1;
570 0         0 $site_inc{"$lib/$Config{archname}"} = 1;
571             }
572              
573 0         0 foreach my $lib (@INC)
574             {
575             push( @inc_add, $lib )
576 0 0       0 unless exists $site_inc{$lib};
577             }
578              
579             # Make sure anything from PERL5LIB and local::lib are removed since it's
580             # most likely the wrong version as well.
581 0         0 @inc_add = grep { $_ !~ m/^$ENV{PERL_LOCAL_LIB_ROOT}/xms } @inc_add;
  0         0  
582 0         0 undef @ENV{qw/PERL_LOCAL_LIB_ROOT PERL5LIB/};
583              
584             # If we've running, inform the new us that they are a restarted process
585 0 0       0 $ENV{$restarted_key} = 1
586             if ${^GLOBAL_PHASE} eq 'RUN';
587              
588             # Cleanup any files opened already. They arn't useful after we exec
589 0         0 File::Temp::cleanup();
590              
591 0         0 exec( $local_perl, map( {"-I$_"} @inc_add ), $0, @ARGV );
  0         0  
592             }
593              
594 1         8 chdir $current_cwd;
595             }
596              
597             1;
598             __END__