File Coverage

blib/lib/CPAN/Plugin/Sysdeps.pm
Criterion Covered Total %
statement 254 409 62.1
branch 151 286 52.8
condition 40 66 60.6
subroutine 31 38 81.5
pod 2 2 100.0
total 478 801 59.6


line stmt bran cond sub pod time code
1             package CPAN::Plugin::Sysdeps;
2              
3 7     7   440034 use strict;
  7         71  
  7         222  
4 7     7   39 use warnings;
  7         13  
  7         339  
5              
6             our $VERSION = '0.71';
7              
8 7     7   48 use List::Util 'first';
  7         12  
  7         950  
9              
10             our $TRAVERSE_ONLY; # only for testing
11              
12 7     7   49 use constant SUPPORTED_NUMERICAL_OPS => ['<','<=','==','>','>='];
  7         23  
  7         1007  
13 7         36 use constant SUPPORTED_NUMERICAL_OPS_RX => do {
14 7         18 my $rx = '^(' . join('|', map { quotemeta } @{SUPPORTED_NUMERICAL_OPS()}) . ')$';
  31         89  
  7         43  
15 7         14074 qr{$rx};
16 7     7   66 };
  7         13  
17              
18             sub new {
19 29     29 1 192079 my($class, @args) = @_;
20              
21 29         132 my $installer;
22 29         61 my $batch = 0;
23 29         42 my $dryrun = 0;
24 29         86 my $debug = 0;
25 29         106 my @additional_mappings;
26             my @args_errors;
27 29         3 my $options;
28 29         101 for my $arg (@args) {
29 81 100       462 if (ref $arg eq 'HASH') {
    100          
    100          
    100          
    100          
    50          
    0          
30 15 50       38 if ($options) {
31 1         42 die "Cannot handle multiple option hashes";
32             } else {
33 15         39 $options = $arg;
34             }
35             } elsif ($arg =~ m{^(apt-get|aptitude|pkg|pkg_add|yum|dnf|chocolatey|homebrew)$}) { # XXX are there more package installers?
36 12         43 $installer = $1;
37             } elsif ($arg eq 'batch') {
38 20         64 $batch = 1;
39             } elsif ($arg eq 'interactive') {
40 2         15 $batch = 0;
41             } elsif ($arg eq 'dryrun') {
42 20         37 $dryrun = 1;
43             } elsif ($arg =~ m{^mapping=(.*)$}) {
44 17         92 push @additional_mappings, $1;
45             } elsif ($arg =~ m{^debug(?:=(\d+))?$}) {
46 1 0       7 $debug = defined $1 ? $1 : 1;
47             } else {
48 1         2 push @args_errors, $arg;
49             }
50             }
51 29 50       142 if (@args_errors) {
52 1 0       7 die 'Unrecognized ' . __PACKAGE__ . ' argument' . (@args_errors != 1 ? 's' : '') . ": @args_errors\n";
53             }
54              
55 29 50       94 if (exists $ENV{CPAN_PLUGIN_SYSDEPS_DEBUG}) {
56 1         36 $debug = $ENV{CPAN_PLUGIN_SYSDEPS_DEBUG};
57             }
58 29 50       83 if ($debug) {
59 1         3 require Data::Dumper; # we'll need it
60             }
61              
62 29   66     168 my $os = $options->{os} || $^O;
63 29         60 my $osvers = '';
64 29         46 my $linuxdistro = '';
65 29         81 my $linuxdistroversion = 0;
66 29         50 my $linuxdistrocodename = '';
67 29 100 33     80 if ($os eq 'linux') {
    50 33        
68 20         56 my $linux_info;
69             my $get_linux_info = sub {
70 42 100   43   148 return $linux_info if $linux_info;
71 14         40 return $linux_info = _detect_linux_distribution();
72 19         89 };
73 19 100       56 if (defined $options->{linuxdistro}) {
74 5         10 $linuxdistro = $options->{linuxdistro};
75             } else {
76 14         30 $linuxdistro = lc $get_linux_info->()->{linuxdistro};
77             }
78              
79 19 100       72 if (defined $options->{linuxdistroversion}) {
80 5         22 $linuxdistroversion = $options->{linuxdistroversion};
81             } else {
82 14         30 $linuxdistroversion = $get_linux_info->()->{linuxdistroversion}; # XXX make it a version object? or make sure it's just X.Y?
83             }
84              
85 19 100       52 if (defined $options->{linuxdistrocodename}) {
86 5         21 $linuxdistrocodename = $options->{linuxdistrocodename};
87             } else {
88 14         39 $linuxdistrocodename = $get_linux_info->()->{linuxdistrocodename};
89             }
90             } elsif (($os eq 'freebsd') || ($os eq 'openbsd') || ($os eq 'dragonfly')) {
91             # Note: don't use $Config{osvers}, as this is just the OS
92             # version of the system which built the current perl, not the
93             # actually running OS version.
94 9 50       19 if (defined $options->{osvers}) {
95 9         16 $osvers = $options->{osvers};
96             } else {
97 0         0 chomp($osvers = `/sbin/sysctl -n kern.osrelease`);
98             }
99             }
100              
101 28 100       94 if (!$installer) {
102 17 100 66     100 if ($os eq 'freebsd' || $os eq 'dragonfly') {
    50          
    50          
    50          
    0          
    0          
103 9         14 $installer = 'pkg';
104             } elsif ($os eq 'gnukfreebsd') {
105 0         0 $installer = 'apt-get';
106             } elsif ($os eq 'openbsd') {
107 0         0 $installer = 'pkg_add';
108             } elsif ($os eq 'linux') {
109 8 50       39 if (__PACKAGE__->_is_linux_debian_like($linuxdistro)) {
    0          
110 8         17 $installer = 'apt-get';
111             } elsif (__PACKAGE__->_is_linux_fedora_like($linuxdistro)) {
112 0 0       0 if (_detect_dnf()) {
113 0         0 $installer = 'dnf';
114             } else {
115 0         0 $installer = 'yum';
116             }
117             } else {
118 0         0 die __PACKAGE__ . " has no support for linux distribution $linuxdistro $linuxdistroversion\n";
119             }
120             } elsif( $os eq 'MSWin32' ) {
121 0         0 $installer = 'chocolatey';
122             } elsif ($os eq 'darwin') {
123 0         0 $installer = 'homebrew';
124             } else {
125 0         0 die __PACKAGE__ . " has no support for operating system $os\n";
126             }
127             }
128              
129 28         59 my @mapping;
130 28         64 for my $mapping (@additional_mappings, 'CPAN::Plugin::Sysdeps::Mapping') {
131 44 100       714 if (-r $mapping) {
132 16 50       664 open my $fh, '<', $mapping
133             or die "Can't load $mapping: $!";
134 16         102 local $/;
135 16         617 my $buf = <$fh>;
136 16         3047 push @mapping, eval $buf;
137 16 50       348 die "Error while loading $mapping: $@" if $@;
138             } else {
139 28 50       1731 eval "require $mapping"; die "Can't load $mapping: $@" if $@;
  28         137  
140 28         122 push @mapping, $mapping->mapping;
141             }
142             }
143              
144 28         357 my %config =
145             (
146             installer => $installer,
147             batch => $batch,
148             dryrun => $dryrun,
149             debug => $debug,
150             os => $os,
151             osvers => $osvers,
152             linuxdistro => $linuxdistro,
153             linuxdistroversion => $linuxdistroversion,
154             linuxdistrocodename => $linuxdistrocodename,
155             mapping => \@mapping,
156             );
157 28         136 my $self = bless \%config, $class;
158 28 50       58 if (eval { require Hash::Util; 1 }) {
  28         3920  
  28         19019  
159 28         112 Hash::Util::lock_keys($self);
160             }
161 28         595 $self;
162             }
163              
164             # CPAN.pm plugin hook method
165             sub post_get {
166 4     5 1 1452 my($self, $dist) = @_;
167              
168 4         24 my @packages = $self->_map_cpandist($dist);
169 3 100       15 if (@packages) {
170 2         13 my @uninstalled_packages = $self->_filter_uninstalled_packages(@packages);
171 2 50       34 if (@uninstalled_packages) {
172 2         51 my @cmds = $self->_install_packages_commands(@uninstalled_packages);
173 2         27 for my $cmd (@cmds) {
174 2 50       19 if ($self->{dryrun}) {
175 2         597 warn "DRYRUN: @$cmd\n";
176             } else {
177 0         0 warn "INFO: run @$cmd...\n";
178              
179 0         0 system @$cmd;
180 0 0       0 if ($? != 0) {
181 0         0 die "@$cmd failed, stop installation";
182             }
183             }
184             }
185             }
186             }
187             }
188              
189             # Helpers/Internal functions/methods
190             sub _detect_linux_distribution {
191 14 50   15   253 if (-x '/usr/bin/lsb_release') {
192 0         0 _detect_linux_distribution_lsb_release();
193             } else {
194 14         51 _detect_linux_distribution_fallback();
195             }
196             }
197              
198             sub _detect_linux_distribution_lsb_release {
199 0     1   0 my %info;
200 0         0 my @cmd = ('lsb_release', '-irc');
201 0 0       0 open my $fh, '-|', @cmd
202             or die "Error while running '@cmd': $!";
203 0         0 while(<$fh>) {
204 0         0 chomp;
205 0 0       0 if (m{^Distributor ID:\s+(.*)}) {
    0          
    0          
206 0         0 $info{linuxdistro} = $1;
207             } elsif (m{^Release:\s+(.*)}) {
208 0         0 $info{linuxdistroversion} = $1;
209             } elsif (m{^Codename:\s+(.*)}) {
210 0         0 $info{linuxdistrocodename} = $1;
211             } else {
212 0         0 warn "WARNING: unexpected '@cmd' output '$_'";
213             }
214             }
215 0 0       0 close $fh
216             or die "Error while running '@cmd': $!";
217 0         0 \%info;
218             }
219              
220             sub _detect_linux_distribution_fallback {
221 14 50   15   357 if (open my $fh, '<', '/etc/redhat-release') {
222 0         0 my $contents = <$fh>;
223 0 0       0 if ($contents =~ m{^(CentOS|Rocky|RedHat|Fedora) (?:Linux )?release (\d+)\S*( \((.*?)\))?}) {
224 0 0       0 return {linuxdistro => $1, linuxdistroversion => $2, linuxdistrocodename => defined $3 ? $3 : ''};
225             }
226             }
227 14 50       531 if (open my $fh, '<', '/etc/issue') {
228 14         1985 chomp(my $line = <$fh>);
229 14 50       163 if ($line =~ m{^Linux Mint (\d+) (\S+)}) {
    50          
    50          
230 0         0 return {linuxdistro => 'LinuxMint', linuxdistroversion => $1, linuxdistrocodename => $2};
231             } elsif ($line =~ m{^(Debian) GNU/Linux (\d+)}) {
232 0         0 my %info = (linuxdistro => $1, linuxdistroversion => $2);
233             $info{linuxdistrocodename} =
234             {
235             6 => 'squeeze',
236             7 => 'wheezy',
237             8 => 'jessie',
238             9 => 'stretch',
239             10 => 'buster',
240             11 => 'bullseye',
241             12 => 'bookworm',
242 0         0 }->{$info{linuxdistroversion}};
243 0         0 return \%info;
244             } elsif ($line =~ m{^(Ubuntu) (\d+\.\d+)}) {
245 14         114 my %info = (linuxdistro => $1, linuxdistroversion => $2);
246             $info{linuxdistrocodename} =
247             {
248             '12.04' => 'precise',
249             '14.04' => 'trusty',
250             '16.04' => 'xenial',
251             '18.04' => 'bionic',
252             '20.04' => 'focal',
253             '22.04' => 'jammy',
254 14         104 }->{$info{linuxdistroversion}};
255 14         401 return \%info;
256             } else {
257 0         0 warn "WARNING: don't know how to handle '$line'";
258             }
259             } else {
260 0         0 warn "WARNING: no /etc/issue available";
261             }
262 0         0 return {};
263             }
264              
265             sub _is_linux_debian_like {
266 356     357   675 my(undef, $linuxdistro) = @_;
267 356         1721 $linuxdistro =~ m{^(debian|ubuntu|linuxmint)$};
268             }
269              
270             sub _is_linux_fedora_like {
271 211     212   368 my(undef, $linuxdistro) = @_;
272 211         1194 $linuxdistro =~ m{^(fedora|redhat|centos|rocky)$};
273             }
274              
275 15     16   198 sub _is_apt_installer { shift->{installer} =~m{^(apt-get|aptitude)$} }
276              
277             # Run a process in an elevated window, wait for its exit
278             sub _win32_run_elevated {
279 0     1   0 my($exe, @args) = @_;
280            
281 0 0       0 my $args = join " ", map { if(/[ "]/) { s!"!\\"!g; qq{"$_"} } else { $_ }} @args;
  0         0  
  0         0  
  0         0  
  0         0  
282              
283 0         0 my $ps1 = sprintf q{powershell -NonInteractive -NoProfile -Command "$process = Start-Process '%s' -PassThru -ErrorAction Stop -ArgumentList '%s' -Verb RunAs -Wait; Exit $process.ExitCode"},
284             $exe, $args;
285              
286 0         0 $ps1;
287             }
288              
289             sub _debug {
290 12124     12125   16021 my $self = shift;
291 12124 50       23028 if ($self->{debug}) {
292 0         0 print STDERR 'DEBUG: ';
293             print STDERR join('', map {
294 0 0       0 if (ref $_) {
  0         0  
295 0         0 Data::Dumper->new([$_])->Terse(1)->Indent(0)->Dump;
296             } else {
297 0         0 $_;
298             }
299             } @_);
300 0         0 print STDERR "\n";
301             }
302             }
303              
304             sub _map_cpandist {
305 18     19   44 my($self, $dist) = @_;
306              
307             # compat for older CPAN.pm (1.76)
308 18 50       119 if (!$dist->can('base_id')) {
309 7     7   57 no warnings 'once';
  7         38  
  7         17982  
310             *CPAN::Distribution::base_id = sub {
311 0     0   0 my $self = shift;
312 0         0 my $id = $self->id();
313 0         0 my $base_id = File::Basename::basename($id);
314 0         0 $base_id =~ s{\.(?:tar\.(bz2|gz|Z)|t(?:gz|bz)|zip)$}{}i;
315 0         0 return $base_id;
316 0         0 };
317             }
318              
319             # smartmatch for regexp/string/array without ~~, 5.8.x compat!
320             # also add support for numerical comparisons
321             my $smartmatch = sub ($$) {
322 3537     3537   6686 my($left, $right) = @_;
323 3537 100       7696 if (ref $right eq 'Regexp') {
    100          
    100          
324 31 100       354 return 1 if $left =~ $right;
325             } elsif (ref $right eq 'ARRAY') {
326 431 100       1694 return 1 if first { $_ eq $left } @$right;
  1033         2795  
327             } elsif (ref $right eq 'HASH') {
328 30         123 for my $op (keys %$right) {
329 34 50       196 if ($op !~ SUPPORTED_NUMERICAL_OPS_RX) {
330 0         0 die "Unsupported operator '$op', only supported: @{SUPPORTED_NUMERICAL_OPS()}";
  0         0  
331             }
332 34         72 my $val = $right->{$op};
333 34         74 my $code = 'no warnings q(numeric); $left '.$op.' $val';
334 34     2   2869 my $res = eval $code;
  2     2   25  
  2     2   6  
  2     2   92  
  2     2   14  
  2     2   5  
  2     2   77  
  2     2   13  
  2         8  
  2         74  
  2         15  
  2         8  
  2         64  
  2         17  
  2         8  
  2         54  
  2         19  
  2         8  
  2         69  
  2         14  
  2         6  
  2         50  
  2         14  
  2         4  
  2         52  
335 34 50       110 die "Evaluation of '$code' failed: $@" if $@;
336 34 100       231 return 0 if !$res;
337             }
338 6         36 return 1;
339             } else {
340 3045 100       10424 return 1 if $left eq $right;
341             }
342 18         113 };
343              
344 18         29 my $handle_mapping_entry; $handle_mapping_entry = sub {
345 5191     5191   8294 my($entry, $level) = @_;
346 5191         10487 for(my $map_i=0; $map_i <= $#$entry; $map_i++) {
347 7538         11914 my $key_or_subentry = $entry->[$map_i];
348 7538 100       14079 if (ref $key_or_subentry eq 'ARRAY') {
    100          
349 2294         6263 $self->_debug(' ' x $level . ' traverse another tree level');
350 2294         4274 my $res = $handle_mapping_entry->($key_or_subentry, $level+1);
351 2293 100 100     11104 return $res if $res && !$TRAVERSE_ONLY;
352             } elsif (ref $key_or_subentry eq 'CODE') {
353 1         39 my $res = $key_or_subentry->($self, $dist);
354 1 50 33     12 return $res if $res && !$TRAVERSE_ONLY;
355             } else {
356 5243         6772 my $key = $key_or_subentry;
357 5243         7528 my $match = $entry->[++$map_i];
358 5243         15165 $self->_debug(' ' x $level . " match '$key' against '", $match, "'");
359 5243 100       13346 if ($key eq 'cpandist') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
360 15 100 100     51 return 0 if !$smartmatch->($dist->base_id, $match) && !$TRAVERSE_ONLY;
361             } elsif ($key eq 'cpanmod') {
362 2882         3795 my $found = 0;
363 2882         5473 for my $mod ($dist->containsmods) {
364 2882         22729 $self->_debug(' ' x $level . " found module '$mod' in dist, check now against '", $match, "'");
365 2882 100       4726 if ($smartmatch->($mod, $match)) {
366 15         25 $found = 1;
367 15         25 last;
368             }
369             }
370 2882 100 100     10846 return 0 if !$found && !$TRAVERSE_ONLY;
371             } elsif ($key eq 'os') {
372 442 100 100     746 return 0 if !$smartmatch->($self->{os}, $match) && !$TRAVERSE_ONLY;
373             } elsif ($key eq 'osvers') {
374 17 100 100     46 return 0 if !$smartmatch->($self->{osvers}, $match) && !$TRAVERSE_ONLY;
375             } elsif ($key eq 'linuxdistro') {
376 608 100       2505 if ($match =~ m{^~(debian|fedora)$}) {
    100          
377 559         1310 my $method = "_is_linux_$1_like";
378 559         1677 $self->_debug(' ' x $level . " translate $match to $method");
379 559 50 66     1326 return 0 if !$self->$method($self->{linuxdistro}) && !$TRAVERSE_ONLY;
380             } elsif ($match =~ m{^~}) {
381 1         15 die "'like' matches only for debian and fedora";
382             } else {
383 48 50 33     89 return 0 if !$smartmatch->($self->{linuxdistro}, $match) && !$TRAVERSE_ONLY;
384             }
385             } elsif ($key eq 'linuxdistroversion') {
386 30 50 66     48 return 0 if !$smartmatch->($self->{linuxdistroversion}, $match) && !$TRAVERSE_ONLY;
387             } elsif ($key eq 'linuxdistrocodename') {
388 103 50 66     178 return 0 if !$smartmatch->($self->{linuxdistrocodename}, $match) && !$TRAVERSE_ONLY; # XXX should also do a smart codename comparison additionally!
389             } elsif ($key eq 'package') {
390 1146         3552 $self->_debug(' ' x $level . " found $match"); # XXX array?
391 1146         3310 return { package => $match };
392             } else {
393 0         0 die "Invalid key '$key'"; # XXX context/position?
394             }
395             }
396             }
397 18         121 };
398              
399 18 50       32 for my $entry (@{ $self->{mapping} || [] }) {
  18         82  
400 2897         4867 my $res = $handle_mapping_entry->($entry, 0);
401 2896 100 66     6515 if ($res && !$TRAVERSE_ONLY) {
402 10 100       96 return ref $res->{package} eq 'ARRAY' ? @{ $res->{package} } : $res->{package};
  2         57  
403             }
404             }
405              
406 7         89 ();
407             }
408              
409             sub _detect_dnf {
410 0     0   0 my @cmd = ('dnf', '--help');
411 0         0 require IPC::Open3;
412 0         0 require Symbol;
413 0         0 my $err = Symbol::gensym();
414 0         0 my $fh;
415 0         0 return eval {
416 0 0       0 if (my $pid = IPC::Open3::open3(undef, $fh, $err, @cmd)) {
417 0         0 waitpid $pid, 0;
418 0         0 return $? == 0;
419             }
420             };
421             }
422              
423             sub _find_missing_deb_packages {
424 5     5   23 my($self, @packages) = @_;
425 5 100       18 return () if !@packages;
426              
427             # taken from ~/devel/deb-install.pl
428 4         21 my %seen_packages;
429             my @missing_packages;
430              
431 4         16 my @cmd = ('dpkg-query', '-W', '-f=${Package} ${Status}\n', @packages);
432 4         1540 require IPC::Open3;
433 4         9992 require Symbol;
434 4         32 my $err = Symbol::gensym();
435 4         83 my $fh;
436 4 50       23 my $pid = IPC::Open3::open3(undef, $fh, $err, @cmd)
437             or die "Error running '@cmd': $!";
438 4         59609 while(<$fh>) {
439 0         0 chomp;
440 0 0       0 if (m{^(\S+) (.*)}) {
441 0 0       0 if ($2 ne 'install ok installed') {
442 0         0 push @missing_packages, $1;
443             }
444 0         0 $seen_packages{$1} = 1;
445             } else {
446 0         0 warn "ERROR: cannot parse $_, ignore line...\n";
447             }
448             }
449 4         170 waitpid $pid, 0;
450 4         71 for my $package (@packages) {
451 6 50       66 if (!$seen_packages{$package}) {
452 6         101 push @missing_packages, $package;
453             }
454             }
455 4         383 @missing_packages;
456             }
457              
458             sub _find_missing_rpm_packages {
459 0     0   0 my($self, @packages) = @_;
460 0 0       0 return () if !@packages;
461              
462 0         0 my @missing_packages;
463              
464             {
465 0         0 my %packages = map{($_,1)} @packages;
  0         0  
  0         0  
466              
467 0         0 local $ENV{LC_ALL} = 'C';
468 0         0 my @cmd = ('rpm', '-q', @packages);
469 0 0       0 open my $fh, '-|', @cmd
470             or die "Error running '@cmd': $!";
471 0         0 while(<$fh>) {
472 0 0       0 if (m{^package (\S+) is not installed}) {
473 0         0 my $package = $1;
474 0 0       0 if (!exists $packages{$package}) {
475 0         0 die "Unexpected: package $package listed as non-installed, but not queries in '@cmd'?!";
476             }
477 0         0 push @missing_packages, $package;
478             }
479             }
480             }
481              
482 0         0 @missing_packages;
483             }
484              
485             sub _find_missing_freebsd_pkg_packages {
486 0     0   0 my($self, @packages) = @_;
487 0 0       0 return () if !@packages;
488              
489 0         0 my @missing_packages;
490 0         0 for my $package (@packages) {
491 0         0 my @cmd = ('pkg', 'info', '--exists', $package);
492 0         0 system @cmd;
493 0 0       0 if ($? != 0) {
494 0         0 push @missing_packages, $package;
495             }
496             }
497              
498 0         0 @missing_packages;
499             }
500              
501             sub _find_missing_openbsd_pkg_packages {
502 0     0   0 my($self, @packages) = @_;
503 0 0       0 return () if !@packages;
504              
505 0         0 require IPC::Open3;
506 0         0 require Symbol;
507              
508 0         0 my @missing_packages;
509 0         0 for my $package (@packages) {
510 0         0 my $err = Symbol::gensym();
511 0         0 my $fh;
512             my $package_in_repository;
513 0         0 eval {
514 0 0       0 if (my $pid = IPC::Open3::open3(undef, $fh, $err, 'pkg_info', $package)) {
515 0         0 waitpid $pid, 0;
516 0 0       0 if ($? == 0) {
517 0         0 $package_in_repository = 1;
518             }
519             }
520             };
521 0 0       0 if ($package_in_repository) {
522 0         0 my @cmd = ('pkg_info', '-q', '-e', "${package}->=0");
523 0         0 system @cmd;
524 0 0       0 if ($? != 0) {
525 0         0 push @missing_packages, $package;
526             }
527             }
528             }
529              
530 0         0 @missing_packages;
531             }
532              
533             sub _find_missing_homebrew_packages {
534 0     0   0 my($self, @packages) = @_;
535 0 0       0 return () if !@packages;
536              
537 0         0 my @missing_packages;
538 0         0 for my $package (@packages) {
539 0         0 my @cmd = ('brew', 'ls', '--versions', $package);
540 0 0       0 open my $fh, '-|', @cmd
541             or die "Error running @cmd: $!";
542 0         0 my $has_package;
543 0         0 while(<$fh>) {
544 0         0 $has_package = 1;
545 0         0 last;
546             }
547 0         0 close $fh; # earlier homebrew versions returned always 0,
548             # newer (since Oct 2016) return 1 if the package is
549             # missing
550 0 0       0 if (!$has_package) {
551 0         0 push @missing_packages, $package;
552             }
553             }
554 0         0 @missing_packages;
555             }
556              
557             sub _find_missing_chocolatey_packages {
558 0     0   0 my($self, @packages) = @_;
559 0 0       0 return () if !@packages;
560              
561             my %installed_packages = map {
562 0 0       0 /^(.*)\|(.*)$/
563             or next;
564 0         0 $1 => $2
565             } grep {
566 0         0 /^(.*)\|(.*)$/
  0         0  
567             } `$self->{installer} list --localonly --limit-output`;
568 0         0 my @missing_packages = grep { ! $installed_packages{ $_ }} @packages;
  0         0  
569 0         0 @missing_packages;
570             }
571              
572             sub _filter_uninstalled_packages {
573 4     4   2335 my($self, @packages) = @_;
574 4         15 my $find_missing_packages;
575 4 50 0     29 if ($self->_is_apt_installer) {
    0 0        
    0          
    0          
    0          
    0          
576 4         16 $find_missing_packages = '_find_missing_deb_packages';
577             } elsif (($self->{installer} eq 'yum') || ($self->{installer} eq 'dnf')) {
578 0         0 $find_missing_packages = '_find_missing_rpm_packages';
579             } elsif ($self->{os} eq 'freebsd' || $self->{os} eq 'dragonfly') {
580 0         0 $find_missing_packages = '_find_missing_freebsd_pkg_packages';
581             } elsif ($self->{os} eq 'openbsd') {
582 0         0 $find_missing_packages = '_find_missing_openbsd_pkg_packages';
583             } elsif ($self->{os} eq 'MSWin32') {
584 0         0 $find_missing_packages = '_find_missing_chocolatey_packages';
585             } elsif ($self->{installer} eq 'homebrew') {
586 0         0 $find_missing_packages = '_find_missing_homebrew_packages';
587             } else {
588 0         0 warn "check for installed packages is NYI for $self->{os}/$self->{linuxdistro}";
589             }
590 4 50       34 if ($find_missing_packages) {
591 4         11 my @plain_packages;
592             my @missing_packages;
593 4         15 for my $package_spec (@packages) {
594 5 100       23 if ($package_spec =~ m{\|}) { # has alternatives
595 1         26 my @single_packages = split /\s*\|\s*/, $package_spec;
596 1         17 my @missing_in_packages_spec = $self->$find_missing_packages(@single_packages);
597 1 50       41 if (@missing_in_packages_spec == @single_packages) {
598 1         168 push @missing_packages, $single_packages[0];
599             }
600             } else {
601 4         12 push @plain_packages, $package_spec;
602             }
603             }
604 4         35 push @missing_packages, $self->$find_missing_packages(@plain_packages);
605 4         83 @packages = @missing_packages;
606             }
607 4         75 @packages;
608             }
609              
610             sub _install_packages_commands {
611 11     11   80 my($self, @packages) = @_;
612 11         35 my @pre_cmd;
613             my @install_cmd;
614              
615             # sudo or not?
616 11 100       84 if ($self->{installer} eq 'homebrew') {
    50          
617             # may run as unprivileged user
618             } elsif ($self->{installer} eq 'chocolatey') {
619             # no sudo on Windows systems?
620             } else {
621 10 50       129 if ($< != 0) {
622 0         0 push @install_cmd, 'sudo';
623             }
624             }
625              
626             # the installer executable
627 11 100       51 if ($self->{installer} eq 'homebrew') {
628 1         6 push @install_cmd, 'brew';
629             } else {
630 10         58 push @install_cmd, $self->{installer};
631             }
632              
633             # batch, default or interactive
634 11 100       51 if ($self->{batch}) {
635 3 50 0     43 if ($self->_is_apt_installer) {
    0          
    0          
    0          
636 3         29 push @install_cmd, '-y';
637             } elsif (($self->{installer} eq 'yum') || ($self->{installer} eq 'dnf')) {
638 0         0 push @install_cmd, '-y';
639             } elsif ($self->{installer} eq 'pkg') { # FreeBSD's pkg
640             # see below
641             } elsif ($self->{installer} eq 'homebrew') {
642             # batch by default
643             } else {
644 0         0 warn "batch=1 NYI for $self->{installer}";
645             }
646             } else {
647 8 100 100     20 if ($self->_is_apt_installer) {
    100          
    100          
    50          
    100          
648 3         16 @pre_cmd = ('sh', '-c', 'echo -n "Install package(s) '."@packages".'? (y/N) "; read yn; [ "$yn" = "y" ]');
649             } elsif (($self->{installer} eq 'yum') || ($self->{installer} eq 'dnf')) {
650             # interactive by default
651             } elsif ($self->{installer} eq 'pkg') { # FreeBSD's pkg
652             # see below
653             } elsif ($self->{installer} =~ m{^(chocolatey)$}) {
654             # Nothing to do here
655             } elsif ($self->{installer} eq 'homebrew') {
656             # the sh builtin echo does not understand -n -> use printf
657 1         9 @pre_cmd = ('sh', '-c', 'printf %s "Install package(s) '."@packages".'? (y/N) "; read yn; [ "$yn" = "y" ]');
658             } else {
659 1         17 warn "batch=0 NYI for $self->{installer}";
660             }
661             }
662              
663             # special options
664 11 100       60 if ($self->{installer} eq 'pkg') { # FreeBSD's pkg
665 1         2 push @install_cmd, '--option', 'LOCK_RETRIES=86400'; # wait quite long in case there are concurrent pkg runs
666             }
667              
668             # the installer subcommand
669 11 100       61 if ($self->{installer} ne 'pkg_add') {
670 10         35 push @install_cmd, 'install';
671             }
672              
673             # post options
674 11 50 66     101 if ($self->{batch} && $self->{installer} eq 'pkg') {
675 0         0 push @install_cmd, '-y';
676             }
677 11 50 66     71 if ($self->{batch} && $self->{installer} eq 'chocolatey') {
678 0         0 push @install_cmd, '-y';
679             }
680 11 50 66     47 if ($self->{batch} && $self->{installer} eq 'pkg_add') {
681 0         0 push @install_cmd, '-I';
682             }
683              
684 11         48 push @install_cmd, @packages;
685            
686 11 50       62 if ($self->{os} eq 'MSWin32') {
687             # Wrap the thing in our small powershell program
688 0         0 @install_cmd = _win32_run_elevated(@install_cmd);
689             };
690              
691 11 100       103 ((@pre_cmd ? \@pre_cmd : ()), \@install_cmd);
692             }
693              
694             1;
695              
696             __END__