File Coverage

blib/lib/Sys/OsPackage.pm
Criterion Covered Total %
statement 168 464 36.2
branch 55 248 22.1
condition 5 42 11.9
subroutine 30 51 58.8
pod 0 34 0.0
total 258 839 30.7


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