File Coverage

blib/lib/App/MechaCPAN.pm
Criterion Covered Total %
statement 280 348 80.4
branch 82 134 61.1
condition 6 39 15.3
subroutine 46 48 95.8
pod 0 16 0.0
total 414 585 70.7


line stmt bran cond sub pod time code
1             package App::MechaCPAN;
2              
3 25     25   16279 use v5.14;
  25         73  
4 25     25   106 use strict;
  25         37  
  25         499  
5 25     25   96 use Cwd qw/cwd/;
  25         33  
  25         965  
6 25     25   126 use Carp;
  25         41  
  25         1197  
7 25     25   134 use Config;
  25         43  
  25         956  
8 25     25   4148 use Symbol qw/geniosym/;
  25         6046  
  25         1269  
9 25     25   9185 use autodie;
  25         312498  
  25         100  
10 25     25   162926 use Term::ANSIColor qw//;
  25         175490  
  25         644  
11 25     25   9910 use IPC::Open3;
  25         57437  
  25         1126  
12 25     25   9242 use IO::Select;
  25         34495  
  25         1086  
13 25     25   157 use List::Util qw/first/;
  25         42  
  25         1935  
14 25     25   6432 use File::Temp qw/tempfile tempdir/;
  25         115854  
  25         1166  
15 25     25   133 use File::Spec qw//;
  25         379  
  25         400  
16 25     25   16599 use Archive::Tar;
  25         1792114  
  25         1347  
17 25     25   16546 use Getopt::Long qw//;
  25         221805  
  25         759  
18              
19 25     25   173 use Exporter qw/import/;
  25         66  
  25         1489  
20              
21             BEGIN
22             {
23 25     25   114 our @EXPORT_OK = qw/
24             url_re git_re git_extract_re
25             has_git has_updated_git min_git_ver
26             logmsg info success error
27             dest_dir inflate_archive
28             run restart_script
29             /;
30 25         24698 our %EXPORT_TAGS = ( go => [@EXPORT_OK] );
31             }
32              
33             our $VERSION = '0.23';
34              
35             require App::MechaCPAN::Perl;
36             require App::MechaCPAN::Install;
37             require App::MechaCPAN::Deploy;
38              
39             my $loaded_at_compile;
40             my $restarted_key = 'APP_MECHACPAN_RESTARTED';
41             my $is_restarted_process = delete $ENV{$restarted_key};
42             INIT
43             {
44             $loaded_at_compile = 1;
45             &restart_script();
46             }
47              
48             $loaded_at_compile //= 0;
49              
50             our @args = (
51             'diag-run!',
52             'verbose|v!',
53             'quiet|q!',
54             'no-log!',
55             'directory|d=s',
56             );
57              
58             # Timeout when there's no output in seconds
59             our $TIMEOUT = $ENV{MECHACPAN_TIMEOUT} // 60;
60             our $VERBOSE; # Print output from sub commands to STDERR
61             our $QUIET; # Do not print any progress to STDERR
62             our $LOGFH; # File handle to send the logs to
63             our $LOG_ON = 1; # Default if to log or not
64              
65             sub main
66             {
67 46     46 0 164097 my @argv = @_;
68              
69 46 50       761 if ( $0 =~ m/zhuli/ )
70             {
71 0 0       0 if ( $argv[0] =~ m/^do the thing/i )
72             {
73 0 0       0 success( "zhuli$$", 'Running deployment' )
74             unless $is_restarted_process;
75 0         0 $argv[0] = 'deploy';
76             }
77 0 0 0     0 if ( $argv[0] =~ m/^do$/i
      0        
78             && $argv[1] =~ m/^the$/i
79             && $argv[2] =~ m/^thing$/i )
80             {
81 0 0       0 success( "zhuli$$", 'Running deployment' )
82             unless $is_restarted_process;
83 0         0 @argv = ( 'deploy', @argv[ 3 .. $#argv ] );
84             }
85             }
86              
87 46         2427 my @args = (
88             @App::MechaCPAN::args,
89             @App::MechaCPAN::Perl::args,
90             @App::MechaCPAN::Install::args,
91             @App::MechaCPAN::Deploy::args,
92             );
93 46         166 @args = keys %{ { map { $_ => 1 } @args } };
  46         241  
  966         4446  
94              
95 46         378 my $options = {};
96 46         1400 my $getopt_ret
97             = Getopt::Long::GetOptionsFromArray( \@argv, $options, @args );
98 46 50       74506 return -1
99             if !$getopt_ret;
100              
101             my $merge_options = sub
102             {
103 94     94   164 my $arg = shift;
104 94 100       309 if ( ref $arg eq 'HASH' )
105             {
106 8         74 $options = { %$arg, %$options };
107 8         40 return 0;
108             }
109 86         267 return 1;
110 46         584 };
111              
112 46         184 @argv = grep { $merge_options->($_) } @argv;
  94         242  
113              
114 46         188794 my $orig_dir = cwd;
115 46 100       1004 if ( exists $options->{directory} )
116             {
117 3 100       85 if ( !-d $options->{directory} )
118             {
119 1         127 die "Cannot find directory: $options->{directory}\n";
120             }
121 2         51 chdir $options->{directory};
122             }
123              
124 45         3193 local $LOGFH;
125 45   33     1304 local $VERBOSE = $options->{verbose} // $VERBOSE;
126 45   33     814 local $QUIET = $options->{quiet} // $QUIET;
127              
128 45         747 my $cmd = ucfirst lc shift @argv;
129 45         536 my $pkg = join( '::', __PACKAGE__, $cmd );
130 45         309 my $action = eval { $pkg->can('go') };
  45         1703  
131 45         192 my $munge = eval { $pkg->can('munge_args') };
  45         387  
132              
133 45 50       379 if ( !defined $action )
134             {
135 0         0 warn "Could not find action to run: $cmd\n";
136 0         0 return -1;
137             }
138              
139 45 100       345 if ( $options->{'diag-run'} )
140             {
141 4         211 warn "Would run '$cmd'\n";
142 4         73 chdir $orig_dir;
143 4         3459 return 0;
144             }
145              
146 41         451 $options->{is_restarted_process} = $is_restarted_process;
147              
148 41 100       299 if ( defined $munge )
149             {
150 5         105 @argv = $pkg->$munge( $options, @argv );
151             }
152              
153 41         388 my $dest_dir = &dest_dir;
154 41 100       1141 if ( !-d $dest_dir )
155             {
156 20         519 mkdir $dest_dir;
157             }
158              
159 41 50       551 unless ( $options->{'no-log'} )
160             {
161 41         197 my $log_dir = "$dest_dir/logs";
162 41 100       698 if ( !-d $log_dir )
163             {
164 20         159 mkdir $log_dir;
165             }
166              
167 41         1806 my $log_path;
168 41         1296 ( $LOGFH, $log_path ) = tempfile( "$log_dir/log.$$.XXXX", UNLINK => 0 );
169             }
170              
171 41 50       23628 my $ret = eval { $pkg->$action( $options, @argv ) || 0; };
  41         719  
172 41         504 chdir $orig_dir;
173              
174 41 100       4303 if ( !defined $ret )
175             {
176 9         108 error($@);
177 9         8882 return -1;
178             }
179              
180 32         491 return $ret;
181             }
182              
183             sub _git_str
184             {
185 15     15   33 state $_git_str;
186              
187 15 100       51 if ( !defined $_git_str )
188             {
189 2         5 $_git_str = '';
190 2         2 my $git_version_str = eval { run(qw/git --version/); };
  2         7  
191 2 50       20 if ( defined $git_version_str )
192             {
193 2         45 ($_git_str) = $git_version_str =~ m/git version (\d+[.]\d+[.]\d+)/;
194             }
195             }
196              
197 15         133 return $_git_str;
198             }
199              
200             sub min_git_ver
201             {
202 15     15 0 215 return '1.7.7';
203             }
204              
205             sub has_updated_git
206             {
207 13     13 0 56 my $git_version_str = _git_str;
208 13 50       41 if ($git_version_str)
209             {
210 25     25   9463 use version 0.77;
  25         39419  
  25         167  
211 13 100       154 if ( version->parse($git_version_str) >= version->parse(min_git_ver) )
212             {
213 9         84 return 1;
214             }
215             }
216              
217 4         47 return;
218             }
219              
220             sub has_git
221             {
222 8   66 8 0 1826 return _git_str && has_updated_git;
223             }
224              
225             sub url_re
226             {
227 34     34 0 1942 state $url_re = qr[
228             ^
229             (?: ftp | http | https | file )
230             : //
231             ]xmsi;
232 34         462 return $url_re;
233             }
234              
235             sub git_re
236             {
237 39     39 0 1759 state $git_re = qr[
238             ^ (?: git | ssh ) :
239             |
240             [.]git (?: @|$ )
241             ]xmsi;
242 39         832 return $git_re;
243             }
244              
245             sub git_extract_re
246             {
247 6     6 0 30 state $re = qr[
248             ^
249             ( # git url capture
250             .* ://
251             (?: \w*@)? # Might have an @ for user@url
252             .*? # Capture the rest
253             )
254             (?: # git commit id capture
255             @
256             ([^@]*) # Evertyhing after @ is a commit_id
257             )?
258             $
259             ]xmsi;
260              
261 6         77 return $re;
262             }
263              
264             sub logmsg
265             {
266 643     643 0 2372 my @lines = @_;
267              
268             return
269 643 100       1822 unless defined $LOGFH;
270              
271 534         1700 foreach my $line (@lines)
272             {
273 534 50       3138 if ( $line !~ m/\n$/xms )
274             {
275 534         1844 $line .= "\n";
276             }
277 534         2356 print $LOGFH $line;
278             }
279              
280 534         1216 return;
281             }
282              
283             sub info
284             {
285 539     539 0 1499 my $key = shift;
286 539         1121 my $line = shift;
287              
288 539 100       1632 if ( !defined $line )
289             {
290 12         40 $line = $key;
291 12         42 undef $key;
292             }
293              
294 539         2798 status( $key, 'YELLOW', $line );
295             }
296              
297             sub success
298             {
299 85     85 0 789 my $key = shift;
300 85         273 my $line = shift;
301              
302 85 100       345 if ( !defined $line )
303             {
304 11         28 $line = $key;
305 11         32 undef $key;
306             }
307              
308 85         402 status( $key, 'GREEN', $line );
309             }
310              
311             sub error
312             {
313 10     10 0 59 my $key = shift;
314 10         47 my $line = shift;
315              
316 10 50       56 if ( !defined $line )
317             {
318 0         0 $line = $key;
319 0         0 undef $key;
320             }
321              
322 10         90 status( $key, 'RED', $line );
323             }
324              
325             my $RESET = Term::ANSIColor::color('RESET');
326             my $BOLD = Term::ANSIColor::color('BOLD');
327              
328             sub _show_line
329             {
330 0     0   0 my $key = shift;
331 0         0 my $color = shift;
332 0         0 my $line = shift;
333              
334             # Clean up the line
335 0         0 $line =~ s/\n/ /xmsg;
336              
337 0         0 state @key_lines;
338              
339 0     0   0 my $idx = first { $key_lines[$_] eq $key } 0 .. $#key_lines;
  0         0  
340              
341 0 0       0 if ( !defined $key )
342             {
343             # Scroll Up 1 line
344 0         0 print STDERR "\n";
345 0         0 $idx = -1;
346             }
347              
348 0 0       0 if ( !defined $idx )
349             {
350 0         0 unshift @key_lines, $key;
351 0         0 $idx = 0;
352              
353             # Scroll Up 1 line
354 0         0 print STDERR "\n";
355             }
356 0         0 $idx++;
357              
358             # Don't bother with fancy line movements if we are verbose
359 0 0       0 if ($VERBOSE)
360             {
361 0         0 print STDERR "$color$line$RESET\n";
362 0         0 return;
363             }
364              
365             # We use some ANSI escape codes, so they are:
366             # \e[.F - Move up from current line, which is always the end of the list
367             # \e[K - Clear the line
368             # $color - Colorize the text
369             # $line - Print the text
370             # $RESET - Reset the colorize
371             # \e[.E - Move down from the current line, back to the end of the list
372 0         0 print STDERR "\e[${idx}F";
373 0         0 print STDERR "\e[K";
374 0         0 print STDERR "$color$line$RESET\n";
375 0 0       0 print STDERR "\e[" . ( $idx - 1 ) . "E"
376             if $idx > 1;
377              
378 0         0 return;
379             }
380              
381             sub status
382             {
383 634     634 0 1362 my $key = shift;
384 634         1959 my $color = shift;
385 634         1190 my $line = shift;
386              
387 634 50       1549 if ( !defined $line )
388             {
389 0         0 $line = $color;
390 0         0 $color = 'RESET';
391             }
392              
393 634         2806 logmsg($line);
394              
395             return
396 634 50       2343 if $QUIET;
397              
398 0   0     0 $color = eval { Term::ANSIColor::color($color) } // $RESET;
  0         0  
399              
400 0         0 state @last_key;
401              
402             # Undo the last line that is bold
403 0 0 0     0 if ( @last_key && !$VERBOSE && $last_key[0] ne $key )
      0        
404             {
405 0         0 _show_line(@last_key);
406             }
407              
408 0         0 _show_line( $key, $color . $BOLD, $line );
409              
410 0         0 @last_key = ( $key, $color, $line );
411             }
412 25 100   25   58657 END { print STDERR "\n" unless $QUIET; }
413             INIT { print STDERR "\n" unless $QUIET; }
414              
415             package MechaCPAN::DestGuard
416             {
417 25     25   26191 use Cwd qw/cwd/;
  25         53  
  25         1329  
418 25     25   151 use Scalar::Util qw/refaddr weaken/;
  25         46  
  25         8323  
419 25     25   178 use overload '""' => sub { my $s = shift; return $$s }, fallback => 1;
  25     278   57  
  25         212  
  278         35444  
  278         6657  
420             my $dest_dir;
421              
422             sub get
423             {
424 127     127   531 my $result = $dest_dir;
425 127 100       796 if ( !defined $result )
426             {
427 56         202127 my $pwd = cwd;
428 56         1132 $dest_dir = \"$pwd/local";
429 56         410 bless $dest_dir;
430 56         321 $result = $dest_dir;
431 56         623 weaken $dest_dir;
432             }
433 127         858 return $dest_dir;
434             }
435              
436             sub DESTROY
437             {
438 56     56   7038 undef $dest_dir;
439             }
440             }
441              
442             sub dest_dir
443             {
444 127     127 0 1271 my $result = MechaCPAN::DestGuard::get();
445 127         1174 return $result;
446             }
447              
448             sub inflate_archive
449             {
450 54     54 0 222 my $src = shift;
451              
452             # $src can be a file path or a URL.
453 54 50       790 if ( !-e $src )
454             {
455 0         0 local $File::Fetch::WARN;
456 0         0 my $ff = File::Fetch->new( uri => $src );
457 0 0       0 $ff->scheme('http')
458             if $ff->scheme eq 'https';
459 0         0 my $content = '';
460 0         0 my $where = $ff->fetch( to => \$content );
461 0 0 0     0 die $ff->error || "Could not download $src"
462             if !defined $where;
463 0         0 $src = $where;
464             }
465              
466 54         3480 my $dir = tempdir(
467             TEMPLATE => File::Spec->tmpdir . '/mechacpan_XXXXXXXX',
468             CLEANUP => 1,
469             );
470 54         271773 my $orig = cwd;
471              
472 54         1050 my $error_free = eval {
473 54         1662 chdir $dir;
474 54         36869 my $tar = Archive::Tar->new;
475 54         2618 $tar->error(1);
476 54         2038 my $ret = $tar->read( "$src", 1, { extract => 1 } );
477 54 50       15406470 die $tar->error
478             unless $ret;
479 54         3879 1;
480             };
481 54         312 my $err = $@;
482              
483 54         914 chdir $orig;
484              
485 54 50       7475 die $err
486             unless $error_free;
487              
488 54         927 return $dir;
489             }
490              
491             sub _genio
492             {
493 388     388   720 state $iswin32 = $^O eq 'MSWin32';
494 388         806 my $write_hdl;
495             my $read_hdl;
496              
497 388 50       1046 if ($iswin32)
498             {
499 25     25   23825 use Socket;
  25         79009  
  25         36197  
500 0         0 socketpair( $read_hdl, $write_hdl, AF_UNIX, SOCK_STREAM, PF_UNSPEC );
501 0         0 shutdown( $read_hdl, 1 );
502 0         0 shutdown( $write_hdl, 0 );
503             }
504             else
505             {
506 388         2306 $write_hdl = $read_hdl = geniosym;
507             }
508              
509 388         14774 $write_hdl->blocking(0);
510 388         3544 $write_hdl->autoflush(1);
511 388         22563 $read_hdl->blocking(0);
512 388         1255 $read_hdl->autoflush(1);
513              
514 388         11233 return ( $read_hdl, $write_hdl );
515             }
516              
517             sub run
518             {
519 194     194 0 7365 my $cmd = shift;
520 194         1135 my @args = @_;
521              
522 194         590 my $out = "";
523 194         596 my $err = "";
524              
525 194         443 my $dest_out_fh = $LOGFH;
526 194         409 my $dest_err_fh = $LOGFH;
527 194         434 my $print_output = $VERBOSE;
528 194         630 my $wantoutput = defined wantarray;
529              
530 194 100       732 if ( ref $cmd eq 'GLOB' )
531             {
532 2         15 $dest_out_fh = $cmd;
533 2         10 $cmd = shift @args;
534             }
535              
536             # If the output is asked for (non-void context), don't show it anywhere
537             #<<<
538 194 100       541 if ($wantoutput)
539             {
540 3         11 undef $dest_out_fh; open $dest_out_fh, ">", \$out;
  3         29  
541 3         9970 undef $dest_err_fh; open $dest_err_fh, ">", \$err;
  3         23  
542 3         172 undef $print_output;
543             }
544             #>>>
545              
546 194         1400 my ( $output, $output_chld ) = _genio;
547 194         601 my ( $error, $error_chld ) = _genio;
548              
549 194 50       743 warn( join( "\t", $cmd, @args ) . "\n" )
550             if $VERBOSE;
551              
552 194 100       1974 print $dest_err_fh ( 'Running: ', join( "\t", $cmd, @args ) . "\n" )
553             if defined $dest_err_fh;
554              
555 194 50       1897 my $pid = open3(
    50          
556             undef,
557             $output_chld->fileno ? '>&' . $output_chld->fileno : $output_chld,
558             $error_chld->fileno ? '>&' . $error_chld->fileno : $error_chld,
559             $cmd, @args
560             );
561 194         1109787 undef $output_chld;
562 194         709 undef $error_chld;
563              
564 194         4945 my $select = IO::Select->new;
565              
566 194         4799 $select->add( $output, $error );
567              
568 194         22112 my $alrm_code = "TIMEOUT\n";
569 194     1   10252 local $SIG{ALRM} = sub { die $alrm_code };
  1         3000140  
570 194         755 local $@;
571              
572 194         821 eval {
573 194         1281 alarm $TIMEOUT;
574 194         1326 while ( my @ready = $select->can_read )
575             {
576 2335         1392627881 alarm $TIMEOUT;
577 2335         6953 foreach my $fh (@ready)
578             {
579 2522         28023 my $line = <$fh>;
580              
581 2522 100       9762 if ( !defined $line )
582             {
583 386         2304 $select->remove($fh);
584 386         17986 next;
585             }
586              
587 2136 50       4874 print STDERR $line if $print_output;
588              
589 2136 100       7175 if ( $fh eq $output )
590             {
591 1425 100       5901 print $dest_out_fh $line
592             if defined $dest_out_fh;
593 1425 100       9750 $out .= $line
594             unless $wantoutput;
595             }
596              
597 2136 100       11328 if ( $fh eq $error )
598             {
599 711 100       1717 print $dest_err_fh $line
600             if defined $dest_err_fh;
601 711 50       3307 $err .= $line
602             unless $wantoutput;
603             }
604              
605             }
606             }
607             };
608              
609 194         2125 my $error = $@;
610 194         1307 alarm 0;
611              
612 194 100       978 if ( $error eq $alrm_code )
613             {
614 1         40 info "Idle timeout (${TIMEOUT}s) exceeded, killing";
615 1         19 kill "KILL", $pid;
616             }
617              
618 194         5309 waitpid( $pid, 0 );
619              
620 194 100       1936 if ($?)
621             {
622 7         102 my $code = qq/Exit Code: / . ( $? >> 8 );
623 7 100       119 my $sig = ( $? & 127 ) ? qq/Signal: / . ( $? & 127 ) : '';
624 7 50       78 my $core = $? & 128 ? 'Core Dumped' : '';
625              
626 7         127 croak ""
627             . Term::ANSIColor::color('RED')
628             . qq/\nCould not execute '/
629             . join( ' ', $cmd, @args ) . qq/'/
630             . qq/\nPID: $pid/
631             . qq/\t$code/
632             . qq/\t$sig/
633             . qq/\t$core/
634             . Term::ANSIColor::color('GREEN')
635             . qq/\n$out/
636             . Term::ANSIColor::color('YELLOW')
637             . qq/\n$err/
638             . Term::ANSIColor::color('RESET') . "\n";
639             }
640              
641             return
642 187 100       10956 if !defined wantarray;
643              
644 3 100       39 if (wantarray)
645             {
646 1         104 return split( /\r?\n/, $out );
647             }
648              
649 2         170 return $out;
650             }
651              
652             sub restart_script
653             {
654 11     11 0 78 my $dest_dir = &dest_dir;
655 11         48 my $local_perl = File::Spec->canonpath("$dest_dir/perl/bin/perl");
656 11         68 my $this_perl = File::Spec->canonpath($^X);
657 11 50       154 if ( $^O ne 'VMS' )
658             {
659             $this_perl .= $Config{_exe}
660 11 50       587 unless $this_perl =~ m/$Config{_exe}$/i;
661             $local_perl .= $Config{_exe}
662 11 50       210 unless $local_perl =~ m/$Config{_exe}$/i;
663             }
664              
665 11         11288 state $orig_cwd = cwd;
666 11         86 state $orig_0 = $0;
667              
668 11         41056 my $current_cwd = cwd;
669 11         379 chdir $orig_cwd;
670              
671 11 0 33     1625 if (
      33        
      0        
      0        
672             $loaded_at_compile # IF we were loaded during compile-time
673             && -e -x $local_perl # AND the local perl is there
674             && $this_perl ne $local_perl # AND if we're not running it
675             && -e -f -r $0 # AND we are a readable file
676             && !$^P # AND we're not debugging
677             )
678             {
679             # ReExecute using the local perl
680 0         0 my @inc_add;
681 0         0 my @paths = qw/
682             sitearchexp sitelibexp
683             vendorarchexp vendorlibexp
684             archlibexp privlibexp
685             otherlibdirsexp
686             /;
687 0         0 my %site_inc = map { $_ => 1 } @Config{@paths}, '.';
  0         0  
688              
689 0         0 foreach my $lib ( split ':', $ENV{PERL5LIB} )
690             {
691 0         0 $site_inc{$lib} = 1;
692 0         0 $site_inc{"$lib/$Config{archname}"} = 1;
693             }
694              
695 0         0 foreach my $lib (@INC)
696             {
697             push( @inc_add, $lib )
698 0 0       0 unless exists $site_inc{$lib};
699             }
700              
701             # Make sure anything from PERL5LIB and local::lib are removed since it's
702             # most likely the wrong version as well.
703 0         0 @inc_add = grep { $_ !~ m/^$ENV{PERL_LOCAL_LIB_ROOT}/xms } @inc_add;
  0         0  
704 0         0 undef @ENV{qw/PERL_LOCAL_LIB_ROOT PERL5LIB/};
705              
706             # If we've running, inform the new us that they are a restarted process
707 0 0       0 $ENV{$restarted_key} = 1
708             if ${^GLOBAL_PHASE} eq 'RUN';
709              
710             # Cleanup any files opened already. They arn't useful after we exec
711 0         0 File::Temp::cleanup();
712              
713 0         0 exec( $local_perl, map( {"-I$_"} @inc_add ), $0, @ARGV );
  0         0  
714             }
715              
716 11         74 chdir $current_cwd;
717             }
718              
719             1;
720             __END__