File Coverage

blib/lib/App/MechaCPAN/Install.pm
Criterion Covered Total %
statement 332 336 98.8
branch 101 120 84.1
condition 28 38 73.6
subroutine 32 32 100.0
pod 1 1 100.0
total 494 527 93.7


line stmt bran cond sub pod time code
1             package App::MechaCPAN::Install;
2              
3 20     20   217 use v5.14;
  20         66  
4              
5 20     20   100 use Config;
  20         43  
  20         719  
6 20     20   98 use Cwd qw/cwd/;
  20         45  
  20         765  
7 20     20   100 use JSON::PP qw//;
  20         45  
  20         249  
8 20     20   87 use File::Spec qw//;
  20         38  
  20         213  
9 20     20   76 use File::Path qw//;
  20         40  
  20         271  
10 20     20   85 use File::Temp qw/tempdir tempfile/;
  20         46  
  20         768  
11 20     20   8324 use CPAN::Meta qw//;
  20         397763  
  20         719  
12 20     20   173 use CPAN::Meta::Prereqs qw//;
  20         49  
  20         281  
13 20     20   96 use File::Fetch qw//;
  20         47  
  20         269  
14 20     20   43794 use Module::CoreList;
  20         861218  
  20         260  
15 20     20   27523 use ExtUtils::MakeMaker qw//;
  20         1605986  
  20         682  
16 20     20   166 use App::MechaCPAN qw/:go/;
  20         47  
  20         55929  
17              
18             our @args = (
19             'skip-tests!',
20             'skip-tests-for:s@',
21             'install-man!',
22             'source=s%',
23             'only-sources!',
24             'update!',
25             );
26              
27             our $dest_lib;
28              
29             # Constants
30             my $COMPLETE = 'COMPLETE';
31              
32             sub go
33             {
34 29     29 1 21797 my $class = shift;
35 29         91 my $opts = shift;
36 29   50     214 my $src = shift // '.';
37 29         122 my @srcs = @_;
38              
39 29         76758 my $orig_dir = cwd;
40 29         524 my $dest_dir = &dest_dir;
41              
42 29         360 local $dest_lib = "$dest_dir/lib/perl5";
43              
44 29         201 my @targets = ( $src, @srcs );
45 29         118 my %src_names;
46             my @deps;
47              
48 29 100 66     438 if ( ref $opts->{source} ne 'HASH' && ref $opts->{source} ne 'CODE' )
49             {
50 25         192 $opts->{source} = {};
51             }
52              
53 29 100       189 if ( ref $opts->{'skip-tests-for'} ne 'ARRAY' )
54             {
55 28         170 $opts->{'skip-tests-for'} = [];
56             }
57             $opts->{'skip-tests-for'}
58 29         95 = { map { $_ => 1 } @{ $opts->{'skip-tests-for'} } };
  1         19  
  29         188  
59              
60             my $unsafe_inc
61 29 100       195 = exists $ENV{PERL_USE_UNSAFE_INC} ? $ENV{PERL_USE_UNSAFE_INC} : 1;
62              
63             # trick AutoInstall
64 29         352 local $ENV{PERL5_CPAN_IS_RUNNING} = $$;
65 29         269 local $ENV{PERL5_CPANPLUS_IS_RUNNING} = $$;
66              
67 29         275 local $ENV{PERL_MM_USE_DEFAULT} = 1;
68 29         245 local $ENV{PERL_USE_UNSAFE_INC} = $unsafe_inc;
69              
70 29         216 local $ENV{PERL_MM_OPT} = "INSTALL_BASE=$dest_dir";
71 29         161 local $ENV{PERL_MB_OPT} = "--install_base $dest_dir";
72              
73 29         241 local $ENV{PERL5LIB} = "$dest_lib";
74              
75             # skip man page generation
76 29 50       219 if ( !$opts->{'install-man'} )
77             {
78             $ENV{PERL_MM_OPT}
79 29         176 .= " " . join( " ", "INSTALLMAN1DIR=none", "INSTALLMAN3DIR=none" );
80 29         197 $ENV{PERL_MB_OPT} .= " " . join(
81             " ", "--config installman1dir=",
82             "--config installsiteman1dir=", "--config installman3dir=",
83             "--config installsiteman3dir="
84             );
85             }
86              
87             #if ( $self->{pure_perl} )
88             #{
89             # $ENV{PERL_MM_OPT} .= " PUREPERL_ONLY=1";
90             # $ENV{PERL_MB_OPT} .= " --pureperl-only";
91             #}
92              
93 29         161 my $cache = { opts => $opts };
94 29         496 my @full_states = (
95             'Resolving' => \&_resolve,
96             'Configuring' => \&_meta,
97             'Configuring' => \&_config_prereq,
98             'Configuring' => \&_configure,
99             'Configuring' => \&_mymeta,
100             'Prerequisites' => \&_prereq,
101             'Installing' => \&_install,
102             'Installed' => \&_write_meta,
103             );
104              
105 29         138 my @states = grep { ref $_ eq 'CODE' } @full_states;
  464         1146  
106 29         104 my @state_desc = grep { ref $_ ne 'CODE' } @full_states;
  464         1068  
107              
108 29         164 foreach my $target (@targets)
109             {
110 33         308 $target = _source_translate( $target, $opts );
111 33         188 $target = _create_target( $target, $cache );
112 33   100     308 $target->{update} = $opts->{update} // 1;
113             }
114              
115 29         177 while ( my $target = shift @targets )
116             {
117 274         1638 $target = _source_translate( $target, $opts );
118 274         1434 $target = _create_target( $target, $cache );
119              
120 274 100       1166 if ( $target->{state} eq $COMPLETE )
121             {
122 1         6 next;
123             }
124              
125 273         4902 chdir $orig_dir;
126             chdir $target->{dir}
127 273 100       2620 if exists $target->{dir};
128              
129             my $line = sprintf(
130             '%-13s %s', $state_desc[ $target->{state} ],
131             $target->{src_name}
132 273         2518 );
133 273         2241 info( $target->{src_name}, $line );
134 273         874 my $method = $states[ $target->{state} ];
135 273         1285 unshift @targets, $method->( $target, $cache );
136             $target->{state}++
137 267 100       2333 if $target->{state} ne $COMPLETE;
138              
139 267 100       2890 if ( $target->{state} eq scalar @states )
140             {
141 29         201 _complete($target);
142 29         103 $target->{was_installed} = 1;
143 29         272 success( $target->{src_name}, $line );
144             }
145             }
146              
147 23         284 chdir $orig_dir;
148              
149 23         5962 return 0;
150             }
151              
152             sub _resolve
153             {
154 40     40   122 my $target = shift;
155 40         97 my $cache = shift;
156              
157 40         112 my $src_name = $target->{src_name};
158              
159             # fetch
160 40         204 my $src_tgz = _get_targz($target);
161              
162             # Verify we need to install it
163 40 100       236 if ( defined $target->{module} )
164             {
165 19         105 my $module = $target->{module};
166 19         113 my $ver = _get_mod_ver($module);
167              
168 19         118 my $msg = 'Up to date';
169              
170             $msg = 'Installed'
171 19 100       111 if $target->{was_installed};
172              
173 19 100 100     398 if ( defined $ver && $target->{version} eq $ver )
174             {
175             success(
176             $target->{src_name},
177             sprintf( '%-13s %s', "$msg-", $target->{src_name} )
178 1         22 );
179 1         9 _complete($target);
180 1         8 return;
181             }
182              
183 18 100 100     153 if ( defined $ver && !$target->{update} )
184             {
185 4         28 my $constraint = $target->{constraint};
186 4   50     120 my $prereq = CPAN::Meta::Prereqs->new(
187             { runtime => { requires => { $module => $constraint // 0 } } } );
188 4         1207 my $req = $prereq->requirements_for( 'runtime', 'requires' );
189              
190 4 50       238 if ( $req->accepts_module( $module, $ver ) )
191             {
192             success(
193             $target->{src_name},
194             sprintf( '%-13s %s', "$msg=", $target->{src_name} )
195 4         434 );
196 4         29 _complete($target);
197 4         74 return;
198             }
199             }
200             }
201              
202 35         268 my $src_dir = inflate_archive($src_tgz);
203              
204 35         2950 my @files = glob( $src_dir . '/*' );
205 35 50       267 if ( @files == 1 )
206             {
207 35         130 $src_dir = $files[0];
208             }
209              
210 35         129 @{$target}{qw/src_tgz dir was_installed/} = ( $src_tgz, $src_dir, 0 );
  35         322  
211 35         332 return $target;
212             }
213              
214             sub _meta
215             {
216 35     35   127 my $target = shift;
217 35         111 my $cache = shift;
218              
219 35         211 $target->{meta} = _load_meta( $target, $cache, 0 );
220 35         191 return $target;
221             }
222              
223             sub _config_prereq
224             {
225 35     35   100 my $target = shift;
226 35         105 my $cache = shift;
227              
228 35         112 my $meta = $target->{meta};
229              
230 35 100       167 return $target
231             if !defined $meta;
232              
233             #printf "testing requirements for %s version %s\n", $meta->name,
234             # $meta->version;
235              
236 34         177 my @config_deps = _phase_prereq( $target, $cache, 'configure' );
237              
238 34         166 $target->{configure_prereq} = [@config_deps];
239              
240 34         135 return @config_deps, $target;
241             }
242              
243             sub _configure
244             {
245 33     33   101 my $target = shift;
246 33         84 my $cache = shift;
247 33         109 my $meta = $target->{meta};
248              
249 33         101 state $mb_deps = { map { $_ => 1 }
  48         278  
250             qw/version ExtUtils-ParseXS ExtUtils-Install ExtUtilsManifest/ };
251              
252             # meta may not be defined, so wrap it in an eval
253 33         95 my $is_mb_dep = eval { exists $mb_deps->{ $meta->name } };
  33         232  
254 33         387 my $maker;
255              
256 33 100 66     964 if ( -e 'Build.PL' && !$is_mb_dep )
257             {
258 2         21 run( $^X, 'Build.PL' );
259 2         48 my $configured = -e -f 'Build';
260             die 'Unable to configure Buid.PL for ' . $target->{module}
261 2 50       17 unless $configured;
262 2         8 $maker = 'mb';
263             }
264              
265 33 100 66     731 if ( !defined $maker && -e 'Makefile.PL' )
266             {
267 31         276 run( $^X, 'Makefile.PL' );
268 31         817 my $configured = -e 'Makefile';
269             die 'Unable to configure Makefile.PL for ' . $target->{module}
270 31 50       279 unless $configured;
271 31         251 $maker = 'mm';
272             }
273              
274             die 'Unable to configure ' . $target->{module}
275 33 50       188 if !defined $maker;
276              
277 33         217 $target->{maker} = $maker;
278 33         499 return $target;
279             }
280              
281             sub _mymeta
282             {
283 33     33   101 my $target = shift;
284 33         106 my $cache = shift;
285              
286 33         229 $target->{meta} = _load_meta( $target, $cache, 1 );
287 33         2526 $target->{name} = $target->{meta}->name;
288 33         375 $target->{name} =~ s[-][::]xmsg;
289              
290 33         152 return $target;
291             }
292              
293             sub _prereq
294             {
295 33     33   102 my $target = shift;
296 33         85 my $cache = shift;
297              
298 33         112 my $meta = $target->{meta};
299              
300             #printf "testing requirements for %s version %s\n", $meta->name,
301             # $meta->version;
302              
303             my @deps
304 33         100 = map { _phase_prereq( $target, $cache, $_ ) } qw/runtime build test/;
  99         395  
305              
306 33         216 $target->{prereq} = [@deps];
307              
308 33         146 return @deps, $target;
309             }
310              
311             sub _install
312             {
313 33     33   100 my $target = shift;
314 33         106 my $cache = shift;
315              
316 33         460 local $ENV{PERL_MM_USE_DEFAULT} = 0;
317 33         199 local $ENV{NONINTERACTIVE_TESTING} = 0;
318              
319 33         515 my $make = $Config{make};
320 33         130 my $opts = $cache->{opts};
321              
322 33         117 my $skip_tests = $cache->{opts}->{'skip-tests'};
323 33 100       144 if ( !$skip_tests )
324             {
325 32         104 my $skips = $opts->{'skip-tests-for'};
326 32         122 $skip_tests = exists $skips->{ $target->{src_name} };
327              
328 32 100 100     301 if ( !$skip_tests && defined $target->{module} )
329             {
330 12         48 $skip_tests = $skips->{ $target->{module} };
331             }
332             }
333              
334 33 100       161 if ( $target->{maker} eq 'mb' )
335             {
336 2         17 run( $^X, './Build' );
337 2 50       38 run( $^X, './Build', 'test' )
338             unless $skip_tests;
339 2         21 run( $^X, './Build', 'install' );
340 2         65 return $target;
341             }
342              
343 31 50       128 if ( $target->{maker} eq 'mm' )
344             {
345 31         195 run($make);
346 31 100       506 run( $make, 'test' )
347             unless $skip_tests;
348 27         312 run( $make, 'install' );
349 27         975 return $target;
350             }
351              
352 0         0 die 'Unable to determine how to install ' . $target->{meta}->name;
353             }
354              
355             sub _write_meta
356             {
357 29     29   157 my $target = shift;
358 29         127 my $cache = shift;
359              
360 29         417 state $arch_dir = "$Config{archname}/.meta/";
361              
362 29 100       234 if ( $target->{is_cpan} )
363             {
364 14         144 my $dir = "$dest_lib/$arch_dir/" . $target->{distvname};
365 14         4513 File::Path::mkpath( $dir, 0, 0777 );
366 14         356 $target->{meta}->save("$dir/MYMETA.json");
367              
368             my $install = {
369             name => $target->{name},
370             target => $target->{src_name},
371             version => $target->{meta}->version,
372             dist => $target->{distvname},
373             pathname => $target->{pathname},
374             provides => $target->{meta}->provides,
375 14         3384398 };
376              
377 14         30688 open my $fh, ">", "$dir/install.json";
378 14         130 print $fh JSON::PP::encode_json($install);
379             }
380 29         7690 return;
381             }
382              
383             my $git_re = qr[
384             ^ (?: git | ssh ) :
385             |
386             [.]git (?: @|$ )
387             ]xmsi;
388              
389             my $url_re = qr[
390             ^
391             (?: ftp | http | https | file )
392             : //
393             ]xmsi;
394              
395             my $full_pause_re = qr[
396             (?: authors/id/ )
397             ( \w / \w\w )
398             /
399             ( \w{2,} )
400             /
401             ( [^/]+ )
402             ]xms;
403             my $pause_re = qr[
404             ^
405              
406             (?: authors/id/ )?
407             (?: \w / \w\w /)?
408              
409             ( \w{2,} )
410             /
411             ( [^/]+ )
412              
413             $
414             ]xms;
415              
416             sub _escape
417             {
418 34     34   82 my $str = shift;
419 34         226 $str =~ s/ ([^A-Za-z0-9\-\._~]) / sprintf("%%%02X", ord($1)) /xmsge;
  52         333  
420 34         148 return $str;
421             }
422              
423             sub _create_target
424             {
425 321     321   16154 my $target = shift;
426 321         687 my $cache = shift;
427              
428 321 100       1410 return $target
429             if ref $target eq 'HASH';
430              
431 55 100       237 if ( ref $target eq '' )
432             {
433 41 100       291 if ( $target =~ m{^ ([^/]+) @ (.*) $}xms )
434             {
435 1         16 $target = [ $1, "==$2" ];
436             }
437             else
438             {
439 40         254 $target = [ split /[~]/xms, $target, 2 ];
440             }
441             }
442              
443 55 50       330 if ( ref $target eq 'ARRAY' )
444             {
445 55         454 $target = {
446             state => 0,
447             src_name => $target->[0],
448             constraint => $target->[1],
449             };
450             }
451              
452 55 100       369 if ( exists $cache->{targets}->{ $target->{src_name} } )
453             {
454 2         15 my $cached_target = $cache->{targets}->{ $target->{src_name} };
455 2 100 66     59 if ( $cached_target->{state} eq $COMPLETE
456             && $target->{constraint} ne $cached_target->{constraint} )
457             {
458 1         4 $cached_target->{constraint} = $target->{constraint};
459 1         5 $cached_target->{state} = 0;
460             }
461 2         12 $target = $cached_target;
462             }
463              
464 55         263 $cache->{targets}->{ $target->{src_name} } = $target;
465              
466 55         175 return $target;
467             }
468              
469             sub _search_metacpan
470             {
471 24     24   76 my $src = shift;
472 24         62 my $constraint = shift;
473              
474             # TODO mirrors
475 24         124 my $dnld = 'https://api-v1.metacpan.org/download_url/' . _escape($src);
476 24 100       111 if ( defined $constraint )
477             {
478 10         33 $dnld .= '?version=' . _escape($constraint);
479             }
480              
481 24         102 local $File::Fetch::WARN;
482 24         351 my $ff = File::Fetch->new( uri => $dnld );
483 24 50       85852 $ff->scheme('http')
484             if $ff->scheme eq 'https';
485 24         445 my $json_info = '';
486 24         167 my $where = $ff->fetch( to => \$json_info );
487              
488 24 50       12401391 die "Could not find module $src on metacpan"
489             if !defined $where;
490              
491 24         280 return JSON::PP::decode_json($json_info);
492             }
493              
494             sub _get_targz
495             {
496 54     54   213 my $target = shift;
497              
498 54         166 my $src = $target->{src_name};
499              
500 54 100       1163 if ( -e -f $src )
501             {
502 19         83 return $src;
503             }
504              
505 35         108 my $url;
506              
507             # git
508 35 100       598 if ( $src =~ $git_re )
509             {
510 2         27 my ( $git_url, $commit ) = $src =~ m/^ (.*?) (?: @ ([^@]*) )? $/xms;
511              
512 2         30 my $dir
513             = tempdir( TEMPLATE => File::Spec->tmpdir . '/mechacpan_XXXXXXXX' );
514 2         490 my ( $fh, $file ) = tempfile(
515             TEMPLATE => File::Spec->tmpdir . '/mechacpan_tar.gz_XXXXXXXX',
516             CLEANUP => 1
517             );
518              
519 2         606 run( 'git', 'clone', '--bare', $git_url, $dir );
520 2   100     56 run(
521             $fh, 'git', 'archive', '--format=tar.gz', "--remote=$dir",
522             $commit || 'master'
523             );
524 2         64 close $fh;
525 2         51 return $file;
526             }
527              
528             # URL
529 33 100       363 if ( $src =~ $url_re )
530             {
531 1         6 $url = $src;
532             }
533              
534             # PAUSE
535 33 100       278 if ( $src =~ $pause_re )
536             {
537 6         38 my $author = $1;
538 6         29 my $package = $2;
539 6         43 $url = join(
540             '/',
541             'https://cpan.metacpan.org/authors/id',
542             substr( $author, 0, 1 ),
543             substr( $author, 0, 2 ),
544             $author,
545             $package,
546             );
547              
548 6         29 $target->{is_cpan} = 1;
549             }
550              
551             # Module Name
552 33 100       142 if ( !defined $url )
553             {
554 26         134 my $json_data = _search_metacpan( $src, $target->{constraint} );
555              
556 26         42360 $url = $json_data->{download_url};
557              
558 26         108 $target->{is_cpan} = 1;
559 26         128 $target->{module} = "$src";
560 26         442 $target->{version} = version->parse( $json_data->{version} );
561             }
562              
563 33 50       174 if ( defined $url )
564             {
565             # if it's pause like, parse out the distibution's version name
566 33 100       575 if ( $url =~ $full_pause_re )
567             {
568 30         128 my $package = $3;
569 30         251 $target->{pathname} = "$1/$2/$3";
570 30         341 $package =~ s/ (.*) [.] ( tar[.](gz|z|bz2) | zip | tgz) $/$1/xmsi;
571 30         134 $target->{distvname} = $package;
572             }
573              
574 33         105 local $File::Fetch::WARN;
575 33         379 my $ff = File::Fetch->new( uri => $url );
576 33         139066 my $dest_dir = dest_dir() . "/pkgs";
577              
578 33 100       204 $ff->scheme('http')
579             if $ff->scheme eq 'https';
580 33         802 my $where = $ff->fetch( to => $dest_dir );
581 33 50 0     3156421 die $ff->error || "Could not download $url"
582             if !defined $where;
583              
584 33         887 return $where;
585             }
586              
587 0         0 die "Cannot find $src\n";
588             }
589              
590             sub _get_mod_ver
591             {
592 19     19   68 my $module = shift;
593 19 50       177 return $]
594             if $module eq 'perl';
595 19         61 local $@;
596 19         67 my $ver = eval {
597 19         112 my $file = _installed_file_for_module($module);
598 19         570 MM->parse_version($file);
599             };
600              
601 19 100       2093 if ( !defined $ver )
602             {
603 17         256 $ver = $Module::CoreList::version{$]}{$module};
604             }
605              
606 19         12595 return $ver;
607             }
608              
609             sub _load_meta
610             {
611 68     68   194 my $target = shift;
612 68         179 my $cache = shift;
613 68         174 my $my = shift;
614              
615 68 100       365 my $prefix = $my ? 'MYMETA' : 'META';
616              
617 68         199 my $meta;
618              
619 68         406 foreach my $file ( "$prefix.json", "$prefix.yml" )
620             {
621 88         246 $meta = eval { CPAN::Meta->load_file($file) };
  88         1087  
622             last
623 88 100       13632834 if defined $meta;
624             }
625              
626 68         425 return $meta;
627             }
628              
629             sub _phase_prereq
630             {
631 133     133   344 my $target = shift;
632 133         297 my $cache = shift;
633 133         448 my $phase = shift;
634              
635 133         784 my $prereqs = $target->{meta}->effective_prereqs;
636 133         2552701 my @result;
637              
638 133         796 my $requirements = $prereqs->requirements_for( $phase, "requires" );
639 133         5966 my $reqs = $requirements->as_string_hash;
640 133         11888 for my $module ( sort keys %$reqs )
641             {
642 263         503 my $is_core;
643              
644 263         2264 my $version = $Module::CoreList::version{$]}{$module};
645 263 100       65156 if ( defined $version )
646             {
647 214         813 $is_core = $requirements->accepts_module( $module, $version );
648             }
649              
650 263 100 100     13422 push @result, [ $module, $reqs->{$module} ]
651             if $module ne 'perl' && !$is_core;
652             }
653              
654 133         3816 return @result;
655             }
656              
657             sub _installed_file_for_module
658             {
659 19     19   59 my $prereq = shift;
660 19         87 my $file = "$prereq.pm";
661 19         153 $file =~ s{::}{/}g;
662              
663 19         322 my $archname = $Config{archname};
664 19         146 my $perlver = $Config{version};
665              
666 19         145 for my $dir (
667             "$dest_lib/$archname",
668             "$dest_lib",
669             )
670             {
671 38         447 my $tmp = File::Spec->catfile( $dir, $file );
672 38 100       651 return $tmp
673             if -r $tmp;
674             }
675             }
676              
677             sub _source_translate
678             {
679 307     307   855 my $target = shift;
680 307         739 my $opts = shift;
681              
682 307         918 my $sources = $opts->{source};
683              
684 307 50 66     2272 if ( ref $target eq 'HASH' && exists $target->{state} )
685             {
686 266         869 return $target;
687             }
688              
689 41         105 my $src_name = $target;
690 41 100       183 if ( ref $target eq 'ARRAY' )
691             {
692 15         57 $src_name = $target->[0];
693             }
694              
695 41 50       186 if ( ref $target eq 'HASH' )
696             {
697 0         0 $src_name = $target->{src_name};
698             }
699              
700 41         88 my $new_src;
701              
702 41 50       194 if ( ref $sources eq 'HASH' )
703             {
704 41         137 $new_src = $sources->{$src_name};
705             }
706              
707 41 50       171 if ( ref $sources eq 'CODE' )
708             {
709 0         0 $new_src = $sources->($src_name);
710             }
711              
712 41 100       224 if ( $opts->{'only-sources'} )
713             {
714 2 50       18 die "Unable to locate $src_name from the sources list\n"
715             if !$new_src;
716 2         16 return $new_src;
717             }
718              
719 39 100       183 return defined $new_src ? $new_src : $target;
720             }
721              
722             sub _complete
723             {
724 34     34   104 my $target = shift;
725 34         121 $target->{state} = $COMPLETE;
726 34         194 return;
727             }
728              
729             1;
730             __END__