File Coverage

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