File Coverage

blib/lib/App/MechaCPAN.pm
Criterion Covered Total %
statement 263 330 79.7
branch 77 126 61.1
condition 4 36 11.1
subroutine 41 43 95.3
pod 0 13 0.0
total 385 548 70.2


line stmt bran cond sub pod time code
1             package App::MechaCPAN;
2              
3 22     22   14243 use v5.14;
  22         82  
4 22     22   139 use strict;
  22         45  
  22         542  
5 22     22   116 use Cwd qw/cwd/;
  22         50  
  22         1021  
6 22     22   117 use Carp;
  22         48  
  22         1182  
7 22     22   123 use Config;
  22         42  
  22         838  
8 22     22   4496 use Symbol qw/geniosym/;
  22         6695  
  22         1104  
9 22     22   9442 use autodie;
  22         304882  
  22         114  
10 22     22   182431 use Term::ANSIColor qw//;
  22         154336  
  22         1069  
11 22     22   13953 use IPC::Open3;
  22         59056  
  22         1402  
12 22     22   11891 use IO::Select;
  22         33317  
  22         1533  
13 22     22   249 use List::Util qw/first/;
  22         61  
  22         2256  
14 22     22   7517 use File::Temp qw/tempfile tempdir/;
  22         138085  
  22         1440  
15 22     22   175 use File::Spec qw//;
  22         60  
  22         403  
16 22     22   14708 use Archive::Tar;
  22         1756748  
  22         1558  
17 22     22   16310 use Getopt::Long qw//;
  22         193934  
  22         820  
18              
19 22     22   223 use Exporter qw/import/;
  22         56  
  22         1516  
20              
21             BEGIN
22             {
23 22     22   117 our @EXPORT_OK = qw/
24             url_re git_re git_extract_re
25             logmsg info success error
26             dest_dir inflate_archive
27             run restart_script
28             /;
29 22         37731 our %EXPORT_TAGS = ( go => [@EXPORT_OK] );
30             }
31              
32             our $VERSION = '0.20';
33              
34             require App::MechaCPAN::Perl;
35             require App::MechaCPAN::Install;
36             require App::MechaCPAN::Deploy;
37              
38             my $loaded_at_compile;
39             my $restarted_key = 'APP_MECHACPAN_RESTARTED';
40             my $is_restarted_process = delete $ENV{$restarted_key};
41             INIT
42             {
43             $loaded_at_compile = 1;
44             &restart_script();
45             }
46              
47             $loaded_at_compile //= 0;
48              
49             our @args = (
50             'diag-run!',
51             'verbose|v!',
52             'quiet|q!',
53             'no-log!',
54             'directory|d=s',
55             );
56              
57             # Timeout when there's no output in seconds
58             our $TIMEOUT = $ENV{MECHACPAN_TIMEOUT} // 60;
59             our $VERBOSE; # Print output from sub commands to STDERR
60             our $QUIET; # Do not print any progress to STDERR
61             our $LOGFH; # File handle to send the logs to
62             our $LOG_ON = 1; # Default if to log or not
63              
64             sub main
65             {
66 36     36 0 155560 my @argv = @_;
67              
68 36 50       532 if ( $0 =~ m/zhuli/ )
69             {
70 0 0       0 if ( $argv[0] =~ m/^do the thing/i )
71             {
72 0 0       0 success( "zhuli$$", 'Running deployment' )
73             unless $is_restarted_process;
74 0         0 $argv[0] = 'deploy';
75             }
76 0 0 0     0 if ( $argv[0] =~ m/^do$/i
      0        
77             && $argv[1] =~ m/^the$/i
78             && $argv[2] =~ m/^thing$/i )
79             {
80 0 0       0 success( "zhuli$$", 'Running deployment' )
81             unless $is_restarted_process;
82 0         0 @argv = ( 'deploy', @argv[ 3 .. $#argv ] );
83             }
84             }
85              
86 36         1237 my @args = (
87             @App::MechaCPAN::args,
88             @App::MechaCPAN::Perl::args,
89             @App::MechaCPAN::Install::args,
90             @App::MechaCPAN::Deploy::args,
91             );
92 36         138 @args = keys %{ { map { $_ => 1 } @args } };
  36         174  
  720         3964  
93              
94 36         330 my $options = {};
95 36         1140 my $getopt_ret
96             = Getopt::Long::GetOptionsFromArray( \@argv, $options, @args );
97 36 50       73031 return -1
98             if !$getopt_ret;
99              
100             my $merge_options = sub
101             {
102 71     71   220 my $arg = shift;
103 71 100       429 if ( ref $arg eq 'HASH' )
104             {
105 5         30 $options = { %$arg, %$options };
106 5         28 return 0;
107             }
108 66         290 return 1;
109 36         647 };
110              
111 36         164 @argv = grep { $merge_options->($_) } @argv;
  71         264  
112              
113 36         216996 my $orig_dir = cwd;
114 36 100       691 if ( exists $options->{directory} )
115             {
116 3 100       138 if ( !-d $options->{directory} )
117             {
118 1         197 die "Cannot find directory: $options->{directory}\n";
119             }
120 2         48 chdir $options->{directory};
121             }
122              
123 35         3029 local $LOGFH;
124 35   33     625 local $VERBOSE = $options->{verbose} // $VERBOSE;
125 35   33     674 local $QUIET = $options->{quiet} // $QUIET;
126              
127 35         609 my $cmd = ucfirst lc shift @argv;
128 35         371 my $pkg = join( '::', __PACKAGE__, $cmd );
129 35         207 my $action = eval { $pkg->can('go') };
  35         1512  
130 35         457 my $munge = eval { $pkg->can('munge_args') };
  35         401  
131              
132 35 50       279 if ( !defined $action )
133             {
134 0         0 warn "Could not find action to run: $cmd\n";
135 0         0 return -1;
136             }
137              
138 35 100       280 if ( $options->{'diag-run'} )
139             {
140 4         164 warn "Would run '$cmd'\n";
141 4         76 chdir $orig_dir;
142 4         3347 return 0;
143             }
144              
145 31         806 $options->{is_restarted_process} = $is_restarted_process;
146              
147 31 100       205 if ( defined $munge )
148             {
149 5         63 @argv = $pkg->$munge( $options, @argv );
150             }
151              
152 31         274 my $dest_dir = &dest_dir;
153 31 100       1457 if ( !-d $dest_dir )
154             {
155 18         375 mkdir $dest_dir;
156             }
157              
158 31 50       502 unless ( $options->{'no-log'} )
159             {
160 31         255 my $log_dir = "$dest_dir/logs";
161 31 100       748 if ( !-d $log_dir )
162             {
163 18         124 mkdir $log_dir;
164             }
165              
166 31         2103 my $log_path;
167 31         892 ( $LOGFH, $log_path ) = tempfile( "$log_dir/log.$$.XXXX", UNLINK => 0 );
168             }
169              
170 31 50       22778 my $ret = eval { $pkg->$action( $options, @argv ) || 0; };
  31         413  
171 31         368 chdir $orig_dir;
172              
173 31 100       3549 if ( !defined $ret )
174             {
175 9         49 error($@);
176 9         120 return -1;
177             }
178              
179 22         284 return $ret;
180             }
181              
182             sub url_re
183             {
184 34     34 0 1805 state $url_re = qr[
185             ^
186             (?: ftp | http | https | file )
187             : //
188             ]xmsi;
189 34         484 return $url_re;
190             }
191              
192             sub git_re
193             {
194 39     39 0 2111 state $git_re = qr[
195             ^ (?: git | ssh ) :
196             |
197             [.]git (?: @|$ )
198             ]xmsi;
199 39         702 return $git_re;
200             }
201              
202             sub git_extract_re
203             {
204 6     6 0 29 state $re = qr[
205             ^
206             ( # git url capture
207             .* ://
208             (?: \w*@)? # Might have an @ for user@url
209             .*? # Capture the rest
210             )
211             (?: # git commit id capture
212             @
213             ([^@]*) # Evertyhing after @ is a commit_id
214             )?
215             $
216             ]xmsi;
217              
218 6         85 return $re;
219             }
220              
221             sub logmsg
222             {
223 582     582 0 2302 my @lines = @_;
224              
225             return
226 582 100       2258 unless defined $LOGFH;
227              
228 473         1491 foreach my $line (@lines)
229             {
230 473 100       2663 if ( $line !~ m/\n$/xms )
231             {
232 464         1684 $line .= "\n";
233             }
234 473         2787 print $LOGFH $line;
235             }
236              
237 473         1419 return;
238             }
239              
240             sub info
241             {
242 479     479 0 1466 my $key = shift;
243 479         1240 my $line = shift;
244              
245 479 100       1845 if ( !defined $line )
246             {
247 1         6 $line = $key;
248 1         7 undef $key;
249             }
250              
251 479         2215 status( $key, 'YELLOW', $line );
252             }
253              
254             sub success
255             {
256 75     75 0 269 my $key = shift;
257 75         205 my $line = shift;
258              
259 75 100       298 if ( !defined $line )
260             {
261 1         11 $line = $key;
262 1         104 undef $key;
263             }
264              
265 75         356 status( $key, 'GREEN', $line );
266             }
267              
268             sub error
269             {
270 19     19 0 79 my $key = shift;
271 19         57 my $line = shift;
272              
273 19 100       93 if ( !defined $line )
274             {
275 9         24 $line = $key;
276 9         28 undef $key;
277             }
278              
279 19         87 status( $key, 'RED', $line );
280             }
281              
282             my $RESET = Term::ANSIColor::color('RESET');
283             my $BOLD = Term::ANSIColor::color('BOLD');
284              
285             sub _show_line
286             {
287 0     0   0 my $key = shift;
288 0         0 my $color = shift;
289 0         0 my $line = shift;
290              
291             # Clean up the line
292 0         0 $line =~ s/\n/ /xmsg;
293              
294 0         0 state @key_lines;
295              
296 0     0   0 my $idx = first { $key_lines[$_] eq $key } 0 .. $#key_lines;
  0         0  
297              
298 0 0       0 if ( !defined $key )
299             {
300             # Scroll Up 1 line
301 0         0 print STDERR "\n";
302 0         0 $idx = -1;
303             }
304              
305 0 0       0 if ( !defined $idx )
306             {
307 0         0 unshift @key_lines, $key;
308 0         0 $idx = 0;
309              
310             # Scroll Up 1 line
311 0         0 print STDERR "\n";
312             }
313 0         0 $idx++;
314              
315             # Don't bother with fancy line movements if we are verbose
316 0 0       0 if ($VERBOSE)
317             {
318 0         0 print STDERR "$color$line$RESET\n";
319 0         0 return;
320             }
321              
322             # We use some ANSI escape codes, so they are:
323             # \e[.F - Move up from current line, which is always the end of the list
324             # \e[K - Clear the line
325             # $color - Colorize the text
326             # $line - Print the text
327             # $RESET - Reset the colorize
328             # \e[.E - Move down from the current line, back to the end of the list
329 0         0 print STDERR "\e[${idx}F";
330 0         0 print STDERR "\e[K";
331 0         0 print STDERR "$color$line$RESET\n";
332 0 0       0 print STDERR "\e[" . ( $idx - 1 ) . "E"
333             if $idx > 1;
334              
335 0         0 return;
336             }
337              
338             sub status
339             {
340 573     573 0 1429 my $key = shift;
341 573         2132 my $color = shift;
342 573         1308 my $line = shift;
343              
344 573 50       2165 if ( !defined $line )
345             {
346 0         0 $line = $color;
347 0         0 $color = 'RESET';
348             }
349              
350 573         2520 logmsg($line);
351              
352             return
353 573 50       3003 if $QUIET;
354              
355 0   0     0 $color = eval { Term::ANSIColor::color($color) } // $RESET;
  0         0  
356              
357 0         0 state @last_key;
358              
359             # Undo the last line that is bold
360 0 0 0     0 if ( @last_key && !$VERBOSE && $last_key[0] ne $key )
      0        
361             {
362 0         0 _show_line(@last_key);
363             }
364              
365 0         0 _show_line( $key, $color . $BOLD, $line );
366              
367 0         0 @last_key = ( $key, $color, $line );
368             }
369 22 100   22   33257 END { print STDERR "\n" unless $QUIET; }
370             INIT { print STDERR "\n" unless $QUIET; }
371              
372             package MechaCPAN::DestGuard
373             {
374 22     22   206 use Cwd qw/cwd/;
  22         60  
  22         1340  
375 22     22   139 use Scalar::Util qw/refaddr weaken/;
  22         50  
  22         1677  
376 22     22   143 use overload '""' => sub { my $s = shift; return $$s }, fallback => 1;
  22     216   59  
  22         188  
  216         30198  
  216         6153  
377             my $dest_dir;
378              
379             sub get
380             {
381 97     97   670 my $result = $dest_dir;
382 97 100       795 if ( !defined $result )
383             {
384 46         226253 my $pwd = cwd;
385 46         813 $dest_dir = \"$pwd/local";
386 46         435 bless $dest_dir;
387 46         190 $result = $dest_dir;
388 46         484 weaken $dest_dir;
389             }
390 97         1002 return $dest_dir;
391             }
392              
393             sub DESTROY
394             {
395 46     46   4392 undef $dest_dir;
396             }
397             }
398              
399             sub dest_dir
400             {
401 97     97 0 794 my $result = MechaCPAN::DestGuard::get();
402 97         1217 return $result;
403             }
404              
405             sub inflate_archive
406             {
407 44     44 0 165 my $src = shift;
408              
409             # $src can be a file path or a URL.
410 44 50       1068 if ( !-e $src )
411             {
412 0         0 local $File::Fetch::WARN;
413 0         0 my $ff = File::Fetch->new( uri => $src );
414 0 0       0 $ff->scheme('http')
415             if $ff->scheme eq 'https';
416 0         0 my $content = '';
417 0         0 my $where = $ff->fetch( to => \$content );
418 0 0 0     0 die $ff->error || "Could not download $src"
419             if !defined $where;
420 0         0 $src = $where;
421             }
422              
423 44         3034 my $dir = tempdir(
424             TEMPLATE => File::Spec->tmpdir . '/mechacpan_XXXXXXXX',
425             CLEANUP => 1,
426             );
427 44         301997 my $orig = cwd;
428              
429 44         1154 my $error_free = eval {
430 44         936 chdir $dir;
431 44         34710 my $tar = Archive::Tar->new;
432 44         1743 $tar->error(1);
433 44         1520 my $ret = $tar->read( "$src", 1, { extract => 1 } );
434 44 50       17408752 die $tar->error
435             unless $ret;
436 44         2901 1;
437             };
438 44         306 my $err = $@;
439              
440 44         524 chdir $orig;
441              
442 44 50       6181 die $err
443             unless $error_free;
444              
445 44         567 return $dir;
446             }
447              
448             sub _genio
449             {
450 308     308   840 state $iswin32 = $^O eq 'MSWin32';
451 308         775 my $write_hdl;
452             my $read_hdl;
453              
454 308 50       1037 if ($iswin32)
455             {
456 22     22   22502 use Socket;
  22         66559  
  22         32764  
457 0         0 socketpair( $read_hdl, $write_hdl, AF_UNIX, SOCK_STREAM, PF_UNSPEC );
458 0         0 shutdown( $read_hdl, 1 );
459 0         0 shutdown( $write_hdl, 0 );
460             }
461             else
462             {
463 308         1699 $write_hdl = $read_hdl = geniosym;
464             }
465              
466 308         13520 $write_hdl->blocking(0);
467 308         2730 $write_hdl->autoflush(1);
468 308         18686 $read_hdl->blocking(0);
469 308         1274 $read_hdl->autoflush(1);
470              
471 308         11542 return ( $read_hdl, $write_hdl );
472             }
473              
474             sub run
475             {
476 154     154 0 5220 my $cmd = shift;
477 154         782 my @args = @_;
478              
479 154         489 my $out = "";
480 154         460 my $err = "";
481              
482 154         443 my $dest_out_fh = $LOGFH;
483 154         421 my $dest_err_fh = $LOGFH;
484 154         533 my $print_output = $VERBOSE;
485 154         626 my $wantoutput = defined wantarray;
486              
487 154 100       753 if ( ref $cmd eq 'GLOB' )
488             {
489 2         12 $dest_out_fh = $cmd;
490 2         9 $cmd = shift @args;
491             }
492              
493             # If the output is asked for (non-void context), don't show it anywhere
494             #<<<
495 154 100       650 if ($wantoutput)
496             {
497 1         4 undef $dest_out_fh; open $dest_out_fh, ">", \$out;
  1         13  
498 1         4701 undef $dest_err_fh; open $dest_err_fh, ">", \$err;
  1         15  
499 1         84 undef $print_output;
500             }
501             #>>>
502              
503 154         1000 my ( $output, $output_chld ) = _genio;
504 154         679 my ( $error, $error_chld ) = _genio;
505              
506 154 50       690 warn( join( "\t", $cmd, @args ) . "\n" )
507             if $VERBOSE;
508              
509 154 100       1270 print $dest_err_fh ( 'Running: ', join( "\t", $cmd, @args ) . "\n" )
510             if defined $dest_err_fh;
511              
512 154 50       1602 my $pid = open3(
    50          
513             undef,
514             $output_chld->fileno ? '>&' . $output_chld->fileno : $output_chld,
515             $error_chld->fileno ? '>&' . $error_chld->fileno : $error_chld,
516             $cmd, @args
517             );
518 154         1043368 undef $output_chld;
519 154         691 undef $error_chld;
520              
521 154         3346 my $select = IO::Select->new;
522              
523 154         5641 $select->add( $output, $error );
524              
525 154         21226 my $alrm_code = "TIMEOUT\n";
526 154     1   6063 local $SIG{ALRM} = sub { die $alrm_code };
  1         3000137  
527 154         748 local $@;
528              
529 154         749 eval {
530 154         1122 alarm $TIMEOUT;
531 154         1533 while ( my @ready = $select->can_read )
532             {
533 2156         1459304374 alarm $TIMEOUT;
534 2156         7481 foreach my $fh (@ready)
535             {
536 2288         35027 my $line = <$fh>;
537              
538 2288 100       11755 if ( !defined $line )
539             {
540 306         2324 $select->remove($fh);
541 306         21831 next;
542             }
543              
544 1982 50       7112 print STDERR $line if $print_output;
545              
546 1982 100       12407 if ( $fh eq $output )
547             {
548 1268 100       7449 print $dest_out_fh $line
549             if defined $dest_out_fh;
550 1268 100       7327 $out .= $line
551             unless $wantoutput;
552             }
553              
554 1982 100       15976 if ( $fh eq $error )
555             {
556 714 100       2436 print $dest_err_fh $line
557             if defined $dest_err_fh;
558 714 50       4468 $err .= $line
559             unless $wantoutput;
560             }
561              
562             }
563             }
564             };
565              
566 154         2644 my $error = $@;
567 154         625 alarm 0;
568              
569 154 100       996 if ( $error eq $alrm_code )
570             {
571 1         28 info "Idle timeout (${TIMEOUT}s) exceeded, killing";
572 1         15 kill "KILL", $pid;
573             }
574              
575 154         12371 waitpid( $pid, 0 );
576              
577 154 100       2036 if ($?)
578             {
579 7         91 my $code = qq/Exit Code: / . ( $? >> 8 );
580 7 100       93 my $sig = ( $? & 127 ) ? qq/Signal: / . ( $? & 127 ) : '';
581 7 50       53 my $core = $? & 128 ? 'Core Dumped' : '';
582              
583 7         109 croak ""
584             . Term::ANSIColor::color('RED')
585             . qq/\nCould not execute '/
586             . join( ' ', $cmd, @args ) . qq/'/
587             . qq/\nPID: $pid/
588             . qq/\t$code/
589             . qq/\t$sig/
590             . qq/\t$core/
591             . Term::ANSIColor::color('GREEN')
592             . qq/\n$out/
593             . Term::ANSIColor::color('YELLOW')
594             . qq/\n$err/
595             . Term::ANSIColor::color('RESET') . "\n";
596             }
597              
598             return
599 147 100       8745 if !defined wantarray;
600              
601 1 50       9 if (wantarray)
602             {
603 1         61 return split( /\r?\n/, $out );
604             }
605              
606 0         0 return $out;
607             }
608              
609             sub restart_script
610             {
611 1     1 0 8 my $dest_dir = &dest_dir;
612 1         11 my $local_perl = File::Spec->canonpath("$dest_dir/perl/bin/perl");
613 1         12 my $this_perl = File::Spec->canonpath($^X);
614 1 50       13 if ( $^O ne 'VMS' )
615             {
616             $this_perl .= $Config{_exe}
617 1 50       67 unless $this_perl =~ m/$Config{_exe}$/i;
618             $local_perl .= $Config{_exe}
619 1 50       45 unless $local_perl =~ m/$Config{_exe}$/i;
620             }
621              
622 1         6647 state $orig_cwd = cwd;
623 1         19 state $orig_0 = $0;
624              
625 1         5862 my $current_cwd = cwd;
626 1         39 chdir $orig_cwd;
627              
628 1 0 33     204 if (
      33        
      0        
      0        
629             $loaded_at_compile # IF we were loaded during compile-time
630             && -e -x $local_perl # AND the local perl is there
631             && $this_perl ne $local_perl # AND if we're not running it
632             && -e -f -r $0 # AND we are a readable file
633             && !$^P # AND we're not debugging
634             )
635             {
636             # ReExecute using the local perl
637 0         0 my @inc_add;
638 0         0 my @paths = qw/
639             sitearchexp sitelibexp
640             vendorarchexp vendorlibexp
641             archlibexp privlibexp
642             otherlibdirsexp
643             /;
644 0         0 my %site_inc = map { $_ => 1 } @Config{@paths}, '.';
  0         0  
645              
646 0         0 foreach my $lib ( split ':', $ENV{PERL5LIB} )
647             {
648 0         0 $site_inc{$lib} = 1;
649 0         0 $site_inc{"$lib/$Config{archname}"} = 1;
650             }
651              
652 0         0 foreach my $lib (@INC)
653             {
654             push( @inc_add, $lib )
655 0 0       0 unless exists $site_inc{$lib};
656             }
657              
658             # Make sure anything from PERL5LIB and local::lib are removed since it's
659             # most likely the wrong version as well.
660 0         0 @inc_add = grep { $_ !~ m/^$ENV{PERL_LOCAL_LIB_ROOT}/xms } @inc_add;
  0         0  
661 0         0 undef @ENV{qw/PERL_LOCAL_LIB_ROOT PERL5LIB/};
662              
663             # If we've running, inform the new us that they are a restarted process
664 0 0       0 $ENV{$restarted_key} = 1
665             if ${^GLOBAL_PHASE} eq 'RUN';
666              
667             # Cleanup any files opened already. They arn't useful after we exec
668 0         0 File::Temp::cleanup();
669              
670 0         0 exec( $local_perl, map( {"-I$_"} @inc_add ), $0, @ARGV );
  0         0  
671             }
672              
673 1         12 chdir $current_cwd;
674             }
675              
676             1;
677             __END__