File Coverage

blib/lib/Sys/OsPackage.pm
Criterion Covered Total %
statement 180 483 37.2
branch 57 258 22.0
condition 8 54 14.8
subroutine 34 55 61.8
pod 0 38 0.0
total 279 888 31.4


line stmt bran cond sub pod time code
1             # Sys::OsPackage
2             # ABSTRACT: install OS packages and determine if CPAN modules are packaged for the OS
3             # Copyright (c) 2022 by Ian Kluft
4             # Open Source license Perl's Artistic License 2.0:
5             # SPDX-License-Identifier: Artistic-2.0
6              
7             # This module is maintained for minimal dependencies so it can build systems/containers from scratch.
8              
9             ## no critic (Modules::RequireExplicitPackage)
10             # This resolves conflicting Perl::Critic rules which want package and strictures each before the other
11 3     3   300425 use strict;
  3         7  
  3         168  
12 3     3   35 use warnings;
  3         8  
  3         231  
13 3     3   2001 use utf8;
  3         1086  
  3         19  
14             ## use critic (Modules::RequireExplicitPackage)
15              
16             package Sys::OsPackage;
17             $Sys::OsPackage::VERSION = '0.4.0';
18 3     3   205 use Config;
  3         11  
  3         180  
19 3     3   41 use Carp qw(carp croak confess);
  3         5  
  3         258  
20 3     3   3341 use Sys::OsRelease;
  3         12460  
  3         116  
21 3     3   2258 use autodie;
  3         71059  
  3         17  
22              
23             BEGIN {
24             # import methods from Sys::OsRelease to manage singleton instance
25 3     3   26714 Sys::OsRelease->import_singleton();
26             }
27              
28             # system configuration
29             my %_sysconf = (
30              
31             # additional common IDs to provide to Sys::OsRelease to recognize as common platforms in ID_LIKE attributes
32             # this adds to recognized common platforms:
33             # RHEL, SuSE, Ubuntu - common commercial platforms
34             # CentOS - because we use it to recognize Rocky and Alma as needing EPEL
35             common_id => [qw(centos rhel suse ubuntu)],
36              
37             # command search list & path
38             search_cmds => [
39             qw(uname curl tar cpan cpanm rpm yum repoquery dnf apt apt-cache dpkg-query apk pacman brew
40             zypper)
41             ],
42             search_path => [qw(/bin /usr/bin /sbin /usr/sbin /opt/bin /usr/local/bin)],
43             );
44              
45             # platform/package configuration
46             # all entries in here have a second-level hash keyed on the platform
47             # TODO: refactor to delegate this to packaging driver classes
48             my %_platconf = (
49              
50             # platform packaging handler class name
51             packager => {
52             alpine => "Sys::OsPackage::Driver::Alpine",
53             arch => "Sys::OsPackage::Driver::Arch",
54             centos => "Sys::OsPackage::Driver::RPM", # CentOS no longer exists; CentOS derivatives supported via ID_LIKE
55             debian => "Sys::OsPackage::Driver::Debian",
56             fedora => "Sys::OsPackage::Driver::RPM",
57             opensuse => "Sys::OsPackage::Driver::Suse",
58             rhel => "Sys::OsPackage::Driver::RPM",
59             suse => "Sys::OsPackage::Driver::Suse",
60             ubuntu => "Sys::OsPackage::Driver::Debian",
61             },
62              
63             # package name override where computed name is not correct
64             override => {
65             debian => {
66             "libapp-cpanminus-perl" => "cpanminus",
67             },
68             ubuntu => {
69             "libapp-cpanminus-perl" => "cpanminus",
70             },
71             arch => {
72             "perl-app-cpanminus" => "cpanminus",
73             "tar" => "core/tar",
74             "curl" => "core/curl",
75             },
76             },
77              
78             # prerequisite OS packages for CPAN
79             prereq => {
80             alpine => [qw(perl-utils)],
81             fedora => [qw(perl-CPAN)],
82             centos => [qw(epel-release perl-CPAN)], # CentOS no longer exists, still used for CentOS-derived systems
83             debian => [qw(perl-modules)],
84             opensuse => [qw()],
85             suse => [qw()],
86             ubuntu => [qw(perl-modules)],
87             },
88             );
89              
90             # Perl-related configuration (read only)
91             my %_perlconf = (
92             sources => {
93             "App::cpanminus" => 'https://cpan.metacpan.org/authors/id/M/MI/MIYAGAWA/App-cpanminus-1.7046.tar.gz',
94             },
95              
96             # Perl module dependencies
97             # Sys::OsPackage doesn't have to declare these as dependencies because it will load them by package or CPAN before use
98             # That maintains a light footprint for bootstrapping a container or system.
99             module_deps => [qw(Term::ANSIColor Perl::PrereqScanner::NotQuiteLite HTTP::Tiny)],
100              
101             # OS package dependencies for CPAN
102             cpan_deps => [qw(curl tar make)],
103              
104             # built-in modules/pragmas to skip processing as dependencies
105             skip => {
106             "strict" => 1,
107             "warnings" => 1,
108             "utf8" => 1,
109             "feature" => 1,
110             "autodie" => 1,
111             },
112             );
113              
114             #
115             # class data access functions
116             #
117              
118             # helper function to allow methods to get the instance ref when called via the class name
119             sub class_or_obj
120             {
121 556     556 0 596 my $coo = shift;
122 556 100       928 return $coo if ref $coo; # return it if it's an object
123              
124             # safety net: all-stop if we received an undef
125 22 50       34 if ( not defined $coo ) {
126 0         0 confess "class_or_obj() got undef from: " . ( join "|", caller 1 );
127             }
128              
129             # return the instance
130 22         77 my $inst_method = $coo->can("instance");
131 22 50       40 if ( not $inst_method ) {
132 0         0 confess "incompatible class $coo from:" . ( join "|", caller 1 );
133             }
134 22         45 return &$inst_method($coo);
135             }
136              
137             # system configuration
138             sub sysconf
139             {
140 10     10 0 1205 my $key = shift;
141 10 100       28 return if not exists $_sysconf{$key};
142 9         92 return $_sysconf{$key};
143             }
144              
145             # Perl configuration
146             sub perlconf
147             {
148 5     5 0 2094 my $key = shift;
149 5 100       14 return if not exists $_perlconf{$key};
150 4         14 return $_perlconf{$key};
151             }
152              
153             # platform configuration
154             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
155 1     1   343274 sub _platconf { return \%_platconf; } # for testing
156             ## use critic (Subroutines::ProhibitUnusedPrivateSubroutines)
157             sub platconf
158             {
159 22     22 0 1228 my ( $class_or_obj, $key ) = @_;
160 22         29 my $self = class_or_obj($class_or_obj);
161              
162 22 50       211 return if not defined $self->platform();
163 22 100       55 return if not exists $_platconf{$key}{ $self->platform() };
164 21         33 return $_platconf{$key}{ $self->platform() };
165             }
166              
167             #
168             # initialization of the singleton instance
169             # imported methods from Sys::OsRelease: init new instance defined_instance clear_instance
170             #
171              
172             # initialize a new instance
173             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines) # called by imported instance() - perlcritic can't see it
174             sub _new_instance
175             {
176 2     2   291091 my ( $class, @params ) = @_;
177              
178             # enforce class lineage
179 2 50       22 if ( not $class->isa(__PACKAGE__) ) {
180 0 0       0 croak "cannot find instance: " . ( ref $class ? ref $class : $class ) . " is not a " . __PACKAGE__;
181             }
182              
183             # obtain parameters from array or hashref
184 2         4 my %obj;
185 2 50       5 if ( scalar @params > 0 ) {
186 2 50       28 if ( ref $params[0] eq 'HASH' ) {
187 0         0 $obj{_config} = $params[0];
188             } else {
189 2         7 $obj{_config} = {@params};
190             }
191             }
192              
193             # bless instance
194 2         10 my $obj_ref = bless \%obj, $class;
195              
196             # initialization
197 2 50       13 if ( exists $obj_ref->{_config}{debug} ) {
    50          
198 0         0 $obj_ref->{debug} = $obj_ref->{_config}{debug};
199             } elsif ( exists $ENV{SYS_OSPACKAGE_DEBUG} ) {
200 0         0 $obj_ref->{debug} = deftrue( $ENV{SYS_OSPACKAGE_DEBUG} );
201             }
202 2 50       6 if ( deftrue( $obj_ref->{debug} ) ) {
203 0         0 print STDERR "_new_instance($class, " . join( ", ", @params ) . ")\n";
204             }
205 2         6 $obj_ref->{sysenv} = {};
206 2         3 $obj_ref->{module_installed} = {};
207 2         10 $obj_ref->collect_sysenv();
208              
209             # instantiate object
210 2         16 return $obj_ref;
211             }
212             ## use critic (Subroutines::ProhibitUnusedPrivateSubroutines)
213              
214             # utility: test if a value is defined and is true
215             sub deftrue
216             {
217 27     27 0 55 my $value = shift;
218 27 100 100     163 return ( ( defined $value ) and $value ) ? 1 : 0;
219             }
220              
221             #
222             # functions that query instance data
223             #
224              
225             # read/write accessor for debug flag
226             sub debug
227             {
228 24     24 0 32 my ( $class_or_obj, $value ) = @_;
229 24         39 my $self = class_or_obj($class_or_obj);
230              
231 24 50       42 if ( defined $value ) {
232 0         0 $self->{debug} = $value;
233             }
234 24         51 return $self->{debug};
235             }
236              
237             # read-only accessor for boolean flags
238             sub ro_flag_accessor
239             {
240 13     13 0 28 my ( $class_or_obj, $name ) = @_;
241 13         21 my $self = class_or_obj($class_or_obj);
242              
243 13         30 return deftrue( $self->{_config}{$name} );
244             }
245              
246             # read-only accessor for quiet flag
247             sub quiet
248             {
249 3     3 0 1458 my ($class_or_obj) = @_;
250 3         10 return ro_flag_accessor( $class_or_obj, "quiet" );
251             }
252              
253             # read-only accessor for notest flag
254             sub notest
255             {
256 0     0 0 0 my ($class_or_obj) = @_;
257 0         0 return ro_flag_accessor( $class_or_obj, "notest" );
258             }
259              
260             # read-only accessor for sudo flag
261             sub sudo
262             {
263 10     10 0 16 my ($class_or_obj) = @_;
264 10         17 return ro_flag_accessor( $class_or_obj, "sudo" );
265             }
266              
267             # for generation of commands with sudo: return sudo or empty list depending on --sudo flag
268             # The sudo command is not generated if the user already has root privileges.
269             sub sudo_cmd
270             {
271 10     10 0 16 my ($class_or_obj) = @_;
272 10         15 my $self = class_or_obj($class_or_obj);
273 10 100 66     25 if ( $self->sudo() and not $self->is_root() ) {
274 5         14 return "sudo";
275             }
276 5         13 return ();
277             }
278              
279             # read/write accessor for system environment data
280             # sysenv is the data collected about the system and commands
281             sub sysenv
282             {
283 291     291 0 30425 my ( $class_or_obj, $key, $value ) = @_;
284 291         378 my $self = class_or_obj($class_or_obj);
285              
286 291 100       433 if ( defined $value ) {
287 45         143 $self->{sysenv}{$key} = $value;
288             }
289 291         2168 return $self->{sysenv}{$key};
290             }
291              
292             # return system platform type
293             sub platform
294             {
295 71     71 0 134 my ($class_or_obj) = @_;
296 71         82 my $self = class_or_obj($class_or_obj);
297              
298 71         108 return $self->sysenv("platform");
299             }
300              
301             # return system packager type, or undef if not determined
302             sub packager
303             {
304 62     62 0 93 my ($class_or_obj) = @_;
305 62         85 my $self = class_or_obj($class_or_obj);
306              
307 62         110 return $self->sysenv("packager"); # undef intentionally returned if it doesn't exist
308             }
309              
310             # look up known exceptions for the platform's package naming pattern
311             sub pkg_override
312             {
313 0     0 0 0 my ( $class_or_obj, $pkg ) = @_;
314 0         0 my $self = class_or_obj($class_or_obj);
315              
316 0         0 my $override = $self->platconf("override");
317 0 0 0     0 return if ( ( not defined $override ) or ( ref $override ne "HASH" ) );
318 0         0 return $override->{$pkg};
319             }
320              
321             # check if a package name is actually a pragma and may as well be skipped because it's built in to Perl
322             sub mod_is_pragma
323             {
324 0     0 0 0 my ( $class_or_obj, $module ) = @_;
325 0         0 my $self = class_or_obj($class_or_obj);
326              
327 0         0 my $perl_skip = perlconf("skip");
328 0 0 0     0 return if ( ( not defined $perl_skip ) or ( ref $perl_skip ne "HASH" ) );
329 0 0       0 return ( deftrue( $perl_skip->{$module} ) ? 1 : 0 );
330             }
331              
332             # find platform-specific prerequisite packages for installation of CPAN
333             sub cpan_prereqs
334             {
335 0     0 0 0 my ($class_or_obj) = @_;
336 0         0 my $self = class_or_obj($class_or_obj);
337              
338 0         0 my @prereqs = @{ perlconf("cpan_deps") };
  0         0  
339 0         0 my $plat_prereq = $self->platconf("prereq");
340 0 0 0     0 if ( ( defined $plat_prereq )
341             and ( ref $plat_prereq eq "ARRAY" ) )
342             {
343 0         0 push @prereqs, @{$plat_prereq};
  0         0  
344             }
345 0         0 return @prereqs;
346             }
347              
348             # determine if a Perl module is installed, or if a value is provided act as a write accessor for the module's flag
349             sub module_installed
350             {
351 0     0 0 0 my ( $class_or_obj, $name, $value ) = @_;
352 0         0 my $self = class_or_obj($class_or_obj);
353 0         0 my $found = 0;
354              
355             # check each path element for the module
356 0         0 my $modfile = join( "/", split( /::/x, $name ) );
357 0         0 foreach my $element (@INC) {
358 0         0 my $filepath = "$element/$modfile.pm";
359 0 0       0 if ( -f $filepath ) {
360 0         0 $found = 1;
361 0         0 last;
362             }
363             }
364              
365             # if a value is provided, act as a write accessor to the module_installed flag for the module
366             # Set it to true if a true value was provided and the module was found in the @INC path.
367 0 0       0 if ( defined $value ) {
368 0 0 0     0 if ( $found and $value ) {
369 0         0 $self->{module_installed}{$name} = $found;
370             }
371             }
372              
373 0         0 return $found;
374             }
375              
376             # run an external command and capture its standard output
377             # optional \%args in first parameter
378             # carp_errors - carp full details in case of errors
379             # list - return an array of result lines
380             sub capture_cmd
381             {
382 7     7 0 515 my ( $class_or_obj, @cmd ) = @_;
383 7         10 my $self = class_or_obj($class_or_obj);
384 7 50       21 $self->debug() and print STDERR "debug(capture_cmd): " . join( " ", @cmd ) . "\n";
385              
386             # get optional arguments if first element of @cmd is a hashref
387 7         10 my %args;
388 7 50       14 if ( ref $cmd[0] eq "HASH" ) {
389 0         0 %args = %{ shift @cmd };
  0         0  
390             }
391              
392             # capture output
393 7         10 my @output;
394 7         19 my $cmd = join( " ", @cmd );
395              
396             # @cmd is concatenated into $cmd - any args which need quotes should have them included
397             {
398 3     3   8972 no autodie;
  3         8  
  3         19  
  7         24  
399 7 50       23898 open my $fh, "-|", $cmd
400             or croak "failed to run pipe command '$cmd': $!";
401 7         5331 while (<$fh>) {
402 7         56 chomp;
403 7         204 push @output, $_;
404             }
405 7 50       874 if ( not close $fh ) {
406 0 0       0 if ( deftrue( $args{carp_errors} ) ) {
407 0         0 carp "failed to close pipe for command '$cmd': $!";
408             }
409             }
410             }
411              
412             # detect and handle errors
413 7 50       81 if ( $? != 0 ) {
414              
415             # for some commands displaying errors are unnecessary - carp errors if requested
416 0 0       0 if ( deftrue( $args{carp_errors} ) ) {
417 0         0 carp "exit status $? from command '$cmd'";
418             }
419 0         0 return;
420             }
421              
422             # return results
423 7 50       147 if ( deftrue( $args{list} ) ) {
424              
425             # return an array if list option set
426 0         0 return @output;
427             }
428 7 100       177 return wantarray ? @output : join( "\n", @output );
429             }
430              
431             # get working directory (with minimal library prerequisites)
432             sub pwd
433             {
434 0     0 0 0 my ($class_or_obj) = @_;
435 0         0 my $self = class_or_obj($class_or_obj);
436              
437 0         0 my $pwd = $self->capture_cmd('pwd');
438 0 0       0 $self->debug() and print STDERR "debug: pwd = $pwd\n";
439 0         0 return $pwd;
440             }
441              
442             # find executable files in the $PATH and standard places
443             sub cmd_path
444             {
445 32     32 0 52 my ( $class_or_obj, $name ) = @_;
446 32         46 my $self = class_or_obj($class_or_obj);
447              
448             # collect and cache path info
449 32 100 66     47 if ( not defined $self->sysenv("path_list") or not defined $self->sysenv("path_flag") ) {
450 2         14 $self->sysenv( "path_list", [ split /:/x, $ENV{PATH} ] );
451 2         5 $self->sysenv( "path_flag", { map { ( $_ => 1 ) } @{ $self->sysenv("path_list") } } );
  18         30  
  2         4  
452 2         4 my $path_flag = $self->sysenv("path_flag");
453 2         2 foreach my $dir ( @{ sysconf("search_path") } ) {
  2         13  
454 12 100       452 -d $dir or next;
455 10 50       21 if ( not exists $path_flag->{$dir} ) {
456 0         0 push @{ $self->sysenv("path_list") }, $dir;
  0         0  
457 0         0 $path_flag->{$dir} = 1;
458             }
459             }
460             }
461              
462             # check each path element for the file
463 32         35 foreach my $element ( @{ $self->sysenv("path_list") } ) {
  32         37  
464 252         331 my $filepath = "$element/$name";
465 252 100       3245 if ( -x $filepath ) {
466 14         53 return $filepath;
467             }
468             }
469 18         53 return;
470             }
471              
472             # de-duplicate a colon-delimited path
473             # utility function
474             sub _dedup_path
475             {
476 0     0   0 my ( $class_or_obj, @in_paths ) = @_;
477 0         0 my $self = class_or_obj($class_or_obj);
478              
479             # construct path lists and deduplicate
480 0         0 my @out_path;
481             my %path_seen;
482 0         0 foreach my $dir ( map { split /:/x, $_ } @in_paths ) {
  0         0  
483 0 0       0 $self->debug() and print STDERR "debug: found $dir\n";
484 0 0       0 if ( $dir eq "." ) {
485              
486             # omit "." for good security practice
487 0         0 next;
488             }
489              
490             # add the path if it hasn't already been seen, and it exists
491 0 0 0     0 if ( not exists $path_seen{$dir} and -d $dir ) {
492 0         0 push @out_path, $dir;
493 0 0       0 $self->debug() and print STDERR "debug: pushed $dir\n";
494             }
495 0         0 $path_seen{$dir} = 1;
496             }
497 0         0 return join ":", @out_path;
498             }
499              
500             # save library hints where user's local Perl modules go, observed in search/cleanup of paths
501             sub _save_hint
502             {
503 0     0   0 my ( $item, $lib_hints_ref, $hints_seen_ref ) = @_;
504 0 0       0 if ( not exists $hints_seen_ref->{$item} ) {
505 0         0 push @{$lib_hints_ref}, $item;
  0         0  
506 0         0 $hints_seen_ref->{$item} = 1;
507             }
508 0         0 return;
509             }
510              
511             # more exhaustive search for user's local perl library directory
512             sub user_perldir_search_loop
513             {
514 0     0 0 0 my ($class_or_obj) = @_;
515 0         0 my $self = class_or_obj($class_or_obj);
516              
517 0 0       0 if ( not defined $self->sysenv("perlbase") ) {
518             DIRLOOP:
519 0         0 foreach my $dirpath ( $self->sysenv("home"), $self->sysenv("home") . "/lib", $self->sysenv("home") . "/.local" )
520             {
521 0         0 foreach my $perlname (qw(perl perl5)) {
522 0 0 0     0 if ( -d "$dirpath/$perlname" and -w "$dirpath/$perlname" ) {
523 0         0 $self->sysenv( "perlbase", $dirpath . "/" . $perlname );
524 0         0 last DIRLOOP;
525             }
526             }
527             }
528             }
529 0         0 return;
530             }
531              
532             # make sure directory path exists
533             sub build_path
534             {
535 0     0 0 0 my @path_parts = @_;
536 0         0 my $need_path;
537 0         0 foreach my $need_dir (@path_parts) {
538 0 0       0 $need_path = ( defined $need_path ) ? "$need_path/$need_dir" : $need_dir;
539 0 0       0 if ( not -d $need_path ) {
540 3     3   17874 no autodie;
  3         13  
  3         15  
541 0 0       0 mkdir $need_path, 0755
542             or return 0; # give up if we can't create the directory
543             }
544             }
545 0         0 return 1;
546             }
547              
548             # if the user's local perl library doesn't exist, see if we can create it
549             sub user_perldir_create
550             {
551 0     0 0 0 my ($class_or_obj) = @_;
552 0         0 my $self = class_or_obj($class_or_obj);
553              
554             # bail out on Win32 because XDG directory standard only applies to Unix-like systems
555 0 0 0     0 if ( $self->sysenv("os") eq "MSWin32" or $self->sysenv("os") eq "Win32" ) {
556 0         0 return 0;
557             }
558              
559             # try to create an XDG-compatible perl library directory under .local
560 0 0       0 if ( not defined $self->sysenv("perlbase") ) {
561              
562             # use a default that complies with XDG directory structure
563 0 0       0 if ( build_path( $self->sysenv("home"), ".local", "perl" ) ) {
564 0         0 $self->sysenv( "perlbase", $self->sysenv("home") . "/.local/perl" );
565             }
566             }
567 0         0 build_path( $self->sysenv("perlbase"), "lib", "perl5" );
568 0         0 return;
569             }
570              
571             # find or create user's local Perl directory
572             sub user_perldir_search
573             {
574 0     0 0 0 my ($class_or_obj) = @_;
575 0         0 my $self = class_or_obj($class_or_obj);
576              
577             # use environment variables to look for user's Perl library
578 0         0 my @lib_hints;
579             my %hints_seen;
580 0         0 my $home = $self->sysenv("home");
581 0 0       0 if ( exists $ENV{PERL_LOCAL_LIB_ROOT} ) {
582 0         0 foreach my $item ( split /:/x, $ENV{PERL_LOCAL_LIB_ROOT} ) {
583 0 0       0 if ( $item =~ qr(^$home/)x ) {
584 0         0 $item =~ s=/$==x; # remove trailing slash if present
585 0         0 _save_hint( $item, \@lib_hints, \%hints_seen );
586             }
587             }
588             }
589 0 0       0 if ( exists $ENV{PERL5LIB} ) {
590 0         0 foreach my $item ( split /:/x, $ENV{PERL5LIB} ) {
591 0 0       0 if ( $item =~ qr(^$home/)x ) {
592 0         0 $item =~ s=/$==x; # remove trailing slash if present
593 0         0 $item =~ s=/[^/]+$==x; # remove last directory from path
594 0         0 _save_hint( $item, \@lib_hints, \%hints_seen );
595             }
596             }
597             }
598 0 0       0 if ( exists $ENV{PATH} ) {
599 0         0 foreach my $item ( split /:/x, $ENV{PATH} ) {
600 0 0 0     0 if ( $item =~ qr(^$home/)x and $item =~ qr(/perl[5]?/)x ) {
601 0         0 $item =~ s=/$==x; # remove trailing slash if present
602 0         0 $item =~ s=/[^/]+$==x; # remove last directory from path
603 0         0 _save_hint( $item, \@lib_hints, \%hints_seen );
604             }
605             }
606             }
607 0         0 foreach my $dirpath (@lib_hints) {
608 0 0 0     0 if ( -d $dirpath and -w $dirpath ) {
609 0         0 $self->sysenv( "perlbase", $dirpath );
610 0         0 last;
611             }
612             }
613              
614             # more exhaustive search for user's local perl library directory
615 0         0 $self->user_perldir_search_loop();
616              
617             # if the user's local perl library doesn't exist, create it
618 0         0 $self->user_perldir_create();
619 0         0 return;
620             }
621              
622             # set up user library and environment variables
623             # this is called for non-root users
624             sub set_user_env
625             {
626 0     0 0 0 my ($class_or_obj) = @_;
627 0         0 my $self = class_or_obj($class_or_obj);
628              
629             # find or create library under home directory
630 0 0       0 if ( exists $ENV{HOME} ) {
631 0         0 $self->sysenv( "home", $ENV{HOME} );
632             }
633 0         0 $self->user_perldir_search();
634              
635             #
636             # set user environment variables similar to local::lib
637             #
638             {
639             # allow environment variables to be set without "local" in this block - this updates them for child processes
640             ## no critic (Variables::RequireLocalizedPunctuationVars)
641              
642             # update PATH
643 0 0       0 if ( exists $ENV{PATH} ) {
  0         0  
644 0         0 $ENV{PATH} = $self->_dedup_path( $ENV{PATH}, $self->sysenv("perlbase") . "/bin" );
645             } else {
646 0         0 $ENV{PATH} = $self->_dedup_path( "/usr/bin:/bin", $self->sysenv("perlbase") . "/bin", "/usr/local/bin" );
647             }
648              
649             # because we modified PATH: remove path cache/flags and force them to be regenerated
650 0         0 delete $self->{sysenv}{path_list};
651 0         0 delete $self->{sysenv}{path_flag};
652              
653             # update PERL5LIB
654 0 0       0 if ( exists $ENV{PERL5LIB} ) {
655 0         0 $ENV{PERL5LIB} = $self->_dedup_path( $ENV{PERL5LIB}, $self->sysenv("perlbase") . "/lib/perl5" );
656             } else {
657 0         0 $ENV{PERL5LIB} = $self->_dedup_path( @INC, $self->sysenv("perlbase") . "/lib/perl5" );
658             }
659              
660             # update PERL_LOCAL_LIB_ROOT/PERL_MB_OPT/PERL_MM_OPT for local::lib
661 0 0       0 if ( exists $ENV{PERL_LOCAL_LIB_ROOT} ) {
662 0         0 $ENV{PERL_LOCAL_LIB_ROOT} = $self->_dedup_path( $ENV{PERL_LOCAL_LIB_ROOT}, $self->sysenv("perlbase") );
663             } else {
664 0         0 $ENV{PERL_LOCAL_LIB_ROOT} = $self->sysenv("perlbase");
665             }
666             {
667             ## no critic (Variables::RequireLocalizedPunctuationVars)
668 0         0 $ENV{PERL_MB_OPT} = '--install_base "' . $self->sysenv("perlbase") . '"';
  0         0  
669 0         0 $ENV{PERL_MM_OPT} = 'INSTALL_BASE=' . $self->sysenv("perlbase");
670             }
671              
672             # update MANPATH
673 0 0       0 if ( exists $ENV{MANPATH} ) {
674 0         0 $ENV{MANPATH} = $self->_dedup_path( $ENV{MANPATH}, $self->sysenv("perlbase") . "/man" );
675             } else {
676             $ENV{MANPATH} =
677 0         0 $self->_dedup_path( "usr/share/man", $self->sysenv("perlbase") . "/man", "/usr/local/share/man" );
678             }
679             }
680              
681             # display updated environment variables
682 0 0       0 if ( not $self->quiet() ) {
683 0         0 print "using environment settings: (add these to login shell rc script if needed)\n";
684 0         0 print "" . ( '-' x 75 ) . "\n";
685 0         0 foreach my $varname (qw(PATH PERL5LIB PERL_LOCAL_LIB_ROOT PERL_MB_OPT PERL_MM_OPT MANPATH)) {
686 0         0 print "export $varname=$ENV{$varname}\n";
687             }
688 0         0 print "" . ( '-' x 75 ) . "\n";
689 0         0 print "\n";
690             }
691 0         0 return;
692             }
693              
694             # collect info and deduce platform type
695             sub resolve_platform
696             {
697 2     2 0 5 my ($class_or_obj) = @_;
698 2         6 my $self = class_or_obj($class_or_obj);
699              
700             # collect uname info
701 2         6 my $uname = $self->sysenv("uname");
702 2 50       6 if ( defined $uname ) {
703              
704             # Unix-like systems all have uname
705 2         9 $self->sysenv( "os", $self->capture_cmd( $uname, "-s" ) );
706 2         16 $self->sysenv( "kernel", $self->capture_cmd( $uname, "-r" ) );
707 2         26 $self->sysenv( "machine", $self->capture_cmd( $uname, "-m" ) );
708             } else {
709              
710             # if the platform doesn't have uname (mainly Windows), get what we can from the Perl configuration
711 0         0 $self->sysenv( "os", $Config{osname} );
712 0         0 $self->sysenv( "machine", $Config{archname} );
713             }
714              
715             # initialize Sys::OsRelease and set platform type
716 2         49 my $osrelease = Sys::OsRelease->instance( common_id => sysconf("common_id") );
717 2         2203 $self->sysenv( "platform", $osrelease->platform() );
718              
719             # determine system's packager if possible
720 2         40 my $plat_packager = $self->platconf("packager");
721 2 50       11 if ( defined $plat_packager ) {
722 2         44 $self->sysenv( "packager", $plat_packager );
723             }
724              
725             # display system info
726 2         27 my $detected;
727 2 50       8 if ( defined $osrelease->osrelease_path() ) {
728 2 50       25 if ( $self->platform() eq $osrelease->id() ) {
729 0         0 $detected = $self->platform();
730             } else {
731 2         33 $detected = $osrelease->id() . " -> " . $self->platform();
732             }
733 2 50       7 if ( defined $self->sysenv("packager") ) {
734 2         4 $detected .= " handled by " . $self->sysenv("packager");
735             }
736              
737             } else {
738 0         0 $detected = $self->platform() . " (no os-release data)";
739             }
740 2 50       10 if ( not $self->quiet() ) {
741 0         0 print $self->text_green() . "system detected: $detected" . $self->text_color_reset() . "\n";
742             }
743 2         6 return;
744             }
745              
746             # collect system environment info
747             sub collect_sysenv
748             {
749 2     2 0 4 my ($class_or_obj) = @_;
750 2         7 my $self = class_or_obj($class_or_obj);
751 2         4 my $sysenv = $self->{sysenv};
752              
753             # find command locations
754 2         2 foreach my $cmd ( @{ sysconf("search_cmds") } ) {
  2         6  
755 32 100       61 if ( my $filepath = $self->cmd_path($cmd) ) {
756 14         37 $sysenv->{$cmd} = $filepath;
757             }
758             }
759 2         207 $sysenv->{perl} = $Config{perlpath};
760              
761             # collect info and deduce platform type
762 2         13 $self->resolve_platform();
763              
764             # check if user is root
765 2 50       20 if ( $> == 0 ) {
766              
767             # set the flag to indicate they are root
768 2         9 $sysenv->{root} = 1;
769              
770             # on Alpine, refresh the package data
771 2 50       7 if ( exists $sysenv->{apk} ) {
772 0         0 $self->run_cmd( $sysenv->{apk}, "update" );
773             }
774             } else {
775              
776             # set user environment variables as necessary (similar to local::lib but without that as a dependency)
777 0         0 $self->set_user_env();
778             }
779              
780             # debug dump
781 2 50       21 if ( $self->debug() ) {
782 0         0 print STDERR "debug: sysenv:\n";
783 0         0 foreach my $key ( sort keys %$sysenv ) {
784 0 0       0 if ( ref $sysenv->{$key} eq "ARRAY" ) {
785 0         0 print STDERR " $key => [" . join( " ", @{ $sysenv->{$key} } ) . "]\n";
  0         0  
786             } else {
787 0 0       0 print STDERR " $key => " . ( exists $sysenv->{$key} ? $sysenv->{$key} : "(undef)" ) . "\n";
788             }
789             }
790             }
791 2         9 return;
792             }
793              
794             # run an external command
795             sub run_cmd
796             {
797 0     0 0 0 my ( $class_or_obj, @cmd ) = @_;
798 0         0 my $self = class_or_obj($class_or_obj);
799              
800 0 0       0 $self->debug() and print STDERR "debug(run_cmd): " . join( " ", @cmd ) . "\n";
801             {
802 3     3   21631 no autodie;
  3         8  
  3         21  
  0         0  
803 0         0 system @cmd;
804             }
805 0 0       0 if ( $? == -1 ) {
    0          
806 0         0 print STDERR "failed to execute '" . ( join " ", @cmd ) . "': $!\n";
807 0         0 exit 1;
808             } elsif ( $? & 127 ) {
809 0 0       0 printf STDERR "child '" . ( join " ", @cmd ) . "' died with signal %d, %s coredump\n",
810             ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without';
811 0         0 exit 1;
812             } else {
813 0         0 my $retval = $? >> 8;
814 0 0       0 if ( $retval != 0 ) {
815 0         0 printf STDERR "child '" . ( join " ", @cmd ) . "' exited with value %d\n", $? >> 8;
816 0         0 return 0;
817             }
818             }
819              
820             # it gets here if it succeeded
821 0         0 return 1;
822             }
823              
824             # check if the user is root - if so, return true
825             sub is_root
826             {
827 5     5 0 8 my ($class_or_obj) = @_;
828 5         6 my $self = class_or_obj($class_or_obj);
829              
830 5         10 return deftrue( $self->sysenv("root") );
831             }
832              
833             # handle various systems' packagers
834             # op parameter is a string:
835             # implemented: 1 if packager implemented for this system, otherwise undef
836             # pkgcmd: 1 if packager command found, 0 if not found
837             # modpkg(module): find name of package for Perl module
838             # find(pkg): 1 if named package exists, 0 if not
839             # install(pkg): 0 = failure, 1 = success
840             # returns undef if not implemented
841             # for ops which return a numeric status: 0 = failure, 1 = success
842             # some ops return a value such as query results
843             sub call_pkg_driver
844             {
845 15     15 0 2559 my ( $class_or_obj, %args ) = @_;
846 15         26 my $self = class_or_obj($class_or_obj);
847              
848 15 50       26 if ( not exists $args{op} ) {
849 0         0 croak "call_pkg_driver() requires op parameter";
850             }
851              
852             # check if packager is implemented for currently-running system
853 15 50       28 if ( $args{op} eq "implemented" ) {
854 0 0       0 if ( $self->sysenv("os") eq "Linux" ) {
855 0 0       0 if ( not defined $self->platform() ) {
856              
857             # for Linux packagers, we need ID to tell distros apart - all modern distros should provide one
858 0         0 return;
859             }
860 0 0       0 if ( not defined $self->packager() ) {
861              
862             # it gets here on Linux distros which we don't have a packager implementation
863 0         0 return;
864             }
865             } else {
866              
867             # add handlers for more packagers as they are implemented
868 0         0 return;
869             }
870 0         0 return 1;
871             }
872              
873             # if a pkg parameter is present, apply package name override if one is configured
874 15 50 33     23 if ( exists $args{pkg} and $self->pkg_override( $args{pkg} ) ) {
875 0         0 $args{pkg} = $self->pkg_override( $args{pkg} );
876             }
877              
878             # if a module parameter is present, add mod_parts parameter
879 15 50       26 if ( exists $args{module} ) {
880 0         0 $args{mod_parts} = [ split /::/x, $args{module} ];
881             }
882              
883             # look up function which implements op for package type
884             ## no critic (BuiltinFunctions::ProhibitStringyEval) # need stringy eval to load a class from a string
885 15 50       51 if ( not eval "require " . $self->packager() ) {
886 0         0 croak "failed to load driver class " . $self->packager();
887             }
888             ## use critic (BuiltinFunctions::ProhibitStringyEval)
889 15         111 my $funcname = $self->packager() . "::" . $args{op};
890             $self->debug()
891 15 50       36 and print STDERR "debug: $funcname(" . join( " ", map { $_ . "=" . $args{$_} } sort keys %args ) . ")\n";
  0         0  
892 15         33 my $funcref = $self->packager()->can( $args{op} );
893 15 50       39 if ( not defined $funcref ) {
894              
895             # not implemented - subroutine name not found in driver class
896 0 0       0 $self->debug() and print STDERR "debug: $funcname not implemented\n";
897 0         0 return;
898             }
899              
900             # call the function with parameters: driver class (class method call), Sys::OsPackage instance, arguments
901 15         27 return $funcref->( $self->packager(), $self, \%args );
902             }
903              
904             # return string to turn text green
905             sub text_green
906             {
907 0     0 0   my ($class_or_obj) = @_;
908 0           my $self = class_or_obj($class_or_obj);
909              
910 0 0         $self->module_installed('Term::ANSIColor') or return "";
911 0           require Term::ANSIColor;
912 0           return Term::ANSIColor::color('green');
913             }
914              
915             # return string to turn text back to normal
916             sub text_color_reset
917             {
918 0     0 0   my ($class_or_obj) = @_;
919 0           my $self = class_or_obj($class_or_obj);
920              
921 0 0         $self->module_installed('Term::ANSIColor') or return "";
922 0           require Term::ANSIColor;
923 0           return Term::ANSIColor::color('reset');
924             }
925              
926             # install a Perl module as an OS package
927             sub module_package
928             {
929 0     0 0   my ( $class_or_obj, $module ) = @_;
930 0           my $self = class_or_obj($class_or_obj);
931              
932             # check if we can install a package
933 0 0 0       if ( not $self->is_root() and not $self->sudo() ) {
934              
935             # must be root or set sudo flag in order to install an OS package
936 0           return 0;
937             }
938 0 0         if ( not $self->call_pkg_driver( op => "implemented" ) ) {
939 0           return 0;
940             }
941              
942             # handle various package managers
943 0           my $pkgname = $self->call_pkg_driver( op => "modpkg", module => $module );
944 0 0 0       return 0 if ( not defined $pkgname ) or length($pkgname) == 0;
945 0 0         if ( not $self->quiet() ) {
946 0           print "\n";
947 0           print $self->text_green()
948             . "install $pkgname for $module using "
949             . $self->sysenv("packager")
950             . $self->text_color_reset() . "\n";
951             }
952              
953 0           return $self->call_pkg_driver( op => "install", pkg => $pkgname );
954             }
955              
956             # check if OS package is installed
957             sub pkg_installed
958             {
959 0     0 0   my ( $class_or_obj, $pkgname ) = @_;
960 0           my $self = class_or_obj($class_or_obj);
961              
962 0 0 0       return 0 if ( not defined $pkgname ) or length($pkgname) == 0;
963 0           return $self->call_pkg_driver( op => "is_installed", pkg => $pkgname );
964             }
965              
966             # check if module is installed, and install it if not present
967             # throws exception on failure
968             sub install_module
969             {
970 0     0 0   my ( $class_or_obj, $name ) = @_;
971 0           my $self = class_or_obj($class_or_obj);
972 0 0         $self->debug() and print STDERR "debug: install_module($name) begin\n";
973 0           my $result = $self->module_installed($name);
974              
975             # check if module is installed
976 0 0         if ($result) {
977 0 0         $self->debug() and print STDERR "debug: install_module($name) skip - already installed\n";
978             } else {
979              
980             # print header for module installation
981 0 0         if ( not $self->quiet() ) {
982 0           print $self->text_green() . ( '-' x 75 ) . "\n";
983 0           print "install $name" . $self->text_color_reset() . "\n";
984             }
985              
986             # try first to install it with an OS package (root required)
987 0 0 0       if ( $self->is_root() or $self->sudo() ) {
988 0 0         if ( $self->module_package($name) ) {
989 0           $result = $self->module_installed( $name, 1 );
990             }
991             }
992              
993             # try again with CPAN or CPANMinus if it wasn't installed by a package
994 0 0         if ( not $result ) {
995 0           my ( $cmd, @test_param );
996 0 0         if ( defined $self->sysenv("cpan") ) {
997 0           $cmd = $self->sysenv("cpan");
998 0 0         $self->notest() and push @test_param, "-T";
999             } else {
1000 0           $cmd = $self->sysenv("cpanm");
1001 0 0         $self->notest() and push @test_param, "--notest";
1002 0           push @test_param, "--without-recommends";
1003 0           push @test_param, "--without-suggests";
1004             }
1005 0 0         $self->run_cmd( $cmd, @test_param, $name )
1006             or croak "failed to install $name module";
1007 0           $result = $self->module_installed( $name, 1 );
1008             }
1009             }
1010 0 0         $self->debug() and print STDERR "debug: install_module($name) result=$result\n";
1011 0           return $result;
1012             }
1013              
1014             # bootstrap CPAN-Minus in a subdirectory of the current directory
1015             sub bootstrap_cpanm
1016             {
1017 0     0 0   my ($class_or_obj) = @_;
1018 0           my $self = class_or_obj($class_or_obj);
1019              
1020             # save current directory
1021 0           my $old_pwd = $self->pwd();
1022              
1023             # make build directory and change into it
1024 0 0         if ( not -d "build" ) {
1025 3     3   18873 no autodie;
  3         8  
  3         16  
1026 0 0         mkdir "build", 0755
1027             or croak "can't make build directory in current directory: $!";
1028             }
1029 0           chdir "build";
1030              
1031             # verify required commands are present
1032 0           my @missing;
1033 0           foreach my $cmd ( @{ perlconf("cpan_deps") } ) {
  0            
1034 0 0         if ( not defined $self->sysenv("$cmd") ) {
1035 0           push @missing, $cmd;
1036             }
1037             }
1038 0 0         if ( scalar @missing > 0 ) {
1039 0           croak "missing " . ( join ", ", @missing ) . " command - can't bootstrap cpanm";
1040             }
1041              
1042             # download cpanm
1043 0           my $perl_sources = perlconf("sources");
1044 0 0         $self->run_cmd( $self->sysenv("curl"), "-L", "--output", "app-cpanminus.tar.gz", $perl_sources->{"App::cpanminus"} )
1045             or croak "download failed for App::cpanminus";
1046 0           my @cpanm_path = grep { qr(/bin/cpanm$)x }
  0            
1047             ( $self->capture_cmd( { list => 1 }, $self->sysenv("tar"), qw(-tf app-cpanminus.tar.gz) ) );
1048 0           my $cpanm_path = pop @cpanm_path;
1049 0           $self->run_cmd( $self->sysenv("tar"), "-xf", "app-cpanminus.tar.gz", $cpanm_path );
1050             {
1051 3     3   12899 no autodie;
  3         8  
  3         15  
  0            
1052 0 0         chmod 0755, $cpanm_path
1053             or croak "failed to chmod $cpanm_path:$!";
1054             }
1055 0           $self->sysenv( "cpanm", $self->pwd() . "/" . $cpanm_path );
1056              
1057             # change back up to previous directory
1058 0           chdir $old_pwd;
1059 0           return;
1060             }
1061              
1062             # establish CPAN if not already present
1063             sub establish_cpan
1064             {
1065 0     0 0   my ($class_or_obj) = @_;
1066 0           my $self = class_or_obj($class_or_obj);
1067              
1068             # first get package dependencies for CPAN (and CPAN too if available via OS package)
1069 0 0         if ( $self->is_root() ) {
1070              
1071             # package dependencies for CPAN (i.e. make, or oddly-named OS package that contains CPAN)
1072 0           my @deps = $self->cpan_prereqs();
1073 0           $self->call_pkg_driver( op => "install", pkg => \@deps );
1074              
1075             # check for commands which were installed by their package name, and specifically look for cpan by any package
1076 0           foreach my $dep ( @deps, "cpan" ) {
1077 0 0         if ( my $filepath = $self->cmd_path($dep) ) {
1078 0           $self->sysenv( $dep, $filepath );
1079             }
1080             }
1081             }
1082              
1083             # install CPAN-Minus if neither CPAN nor CPAN-Minus exist
1084 0 0 0       if ( not defined $self->sysenv("cpan") and not defined $self->sysenv("cpanm") ) {
1085              
1086             # try to install CPAN-Minus as an OS package
1087 0 0         if ( $self->is_root() ) {
1088 0 0         if ( $self->module_package("App::cpanminus") ) {
1089 0           $self->sysenv( "cpanm", $self->cmd_path("cpanm") );
1090             }
1091             }
1092              
1093             # try again if it wasn't installed by a package
1094 0 0         if ( not defined $self->sysenv("cpanm") ) {
1095 0           $self->bootstrap_cpanm();
1096             }
1097             }
1098              
1099             # install CPAN if it doesn't exist
1100 0 0         if ( not defined $self->sysenv("cpan") ) {
1101              
1102             # try to install CPAN as an OS package
1103 0 0         if ( $self->is_root() ) {
1104 0 0         if ( $self->module_package("CPAN") ) {
1105 0           $self->sysenv( "cpan", $self->cmd_path("cpan") );
1106             }
1107             }
1108              
1109             # try again with cpanminus if it wasn't installed by a package
1110 0 0         if ( not defined $self->sysenv("cpan") ) {
1111 0 0         if ( $self->run_cmd( $self->sysenv("perl"), $self->sysenv("cpanm"), "CPAN" ) ) {
1112 0           $self->sysenv( "cpan", $self->cmd_path("cpan") );
1113             }
1114             }
1115             }
1116              
1117             # install modules used by Sys::OsPackage or CPAN
1118 0           foreach my $dep ( @{ perlconf("module_deps") } ) {
  0            
1119 0           $self->install_module($dep);
1120             }
1121 0           return 1;
1122             }
1123              
1124             1;
1125              
1126             =pod
1127              
1128             =encoding UTF-8
1129              
1130             =head1 NAME
1131              
1132             Sys::OsPackage - install OS packages and determine if CPAN modules are packaged for the OS
1133              
1134             =head1 VERSION
1135              
1136             version 0.4.0
1137              
1138             =head1 SYNOPSIS
1139              
1140             use Sys::OsPackage;
1141             my $ospackage = Sys::OsPackage->instance();
1142             foreach my $module ( qw(module-name ...)) {
1143             $ospackage->install_module($module);
1144             }
1145              
1146             =head1 DESCRIPTION
1147              
1148             I is used for installing Perl module dependencies.
1149             It can look up whether a Perl module is available under some operating systems' packages.
1150             If the module is available as an OS package, it installs it via the packaging system of the OS.
1151             Otherwise it runs CPAN to install the module.
1152              
1153             The use cases of I include setting up systems or containers with Perl modules using OS packages
1154             as often as possible. It can also be used for installing dependencies for a Perl script on an existing system.
1155              
1156             OS packaging systems currently supported by I are the Linux distributions Alpine, Arch, Debian,
1157             Fedora and OpenSuse.
1158             Using L it's able to detect operating systems derived from a supported platform use the correct driver.
1159              
1160             RHEL and CentOS are supported by the Fedora driver.
1161             CentOS-derived systems Rocky and Alma are supported by recognizing them as derivatives.
1162             Ubuntu is supported by the Debian driver.
1163              
1164             Other packaging systems for Unix-like operating systems should be feasible to add by writing a driver module.
1165              
1166             =head1 SEE ALSO
1167              
1168             L comes with I to provide a command-line interface.
1169              
1170             L
1171              
1172             "pacman/Rosetta" at Arch Linux Wiki compares commands of 5 Linux packaging systems L
1173              
1174             GitHub repository for Sys::OsPackage: L
1175              
1176             =head1 BUGS AND LIMITATIONS
1177              
1178             Please report bugs via GitHub at L
1179              
1180             Patches and enhancements may be submitted via a pull request at L
1181              
1182             =head1 LICENSE INFORMATION
1183              
1184             Copyright (c) 2022 by Ian Kluft
1185              
1186             This module is distributed in the hope that it will be useful, but it is provided “as is” and without any express or implied warranties. For details, see the full text of the license in the file LICENSE or at L.
1187              
1188             =head1 AUTHOR
1189              
1190             Ian Kluft
1191              
1192             =head1 COPYRIGHT AND LICENSE
1193              
1194             This software is Copyright (c) 2022 by Ian Kluft.
1195              
1196             This is free software, licensed under:
1197              
1198             The Artistic License 2.0 (GPL Compatible)
1199              
1200             =cut
1201              
1202             __END__