File Coverage

blib/lib/Sys/OsPackage.pm
Criterion Covered Total %
statement 168 465 36.1
branch 55 252 21.8
condition 5 42 11.9
subroutine 30 51 58.8
pod 0 34 0.0
total 258 844 30.5


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   2633 use strict;
  3         18  
  3         84  
12 3     3   15 use warnings;
  3         4  
  3         70  
13 3     3   1802 use utf8;
  3         42  
  3         15  
14             ## use critic (Modules::RequireExplicitPackage)
15              
16             package Sys::OsPackage;
17             $Sys::OsPackage::VERSION = '0.1.7';
18 3     3   159 use Config;
  3         8  
  3         146  
19 3     3   16 use Carp qw(carp croak confess);
  3         6  
  3         235  
20 3     3   1542 use Sys::OsRelease;
  3         8073  
  3         96  
21 3     3   1403 use autodie;
  3         49007  
  3         13  
22             BEGIN {
23             # import methods from Sys::OsRelease to manage singleton instance
24 3     3   23020 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 575 my $coo = shift;
117 421 100       972 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       50 if (not defined $coo) {
121 0         0 confess "coo got undef from:".(join "|", caller 1);
122             }
123              
124             # return the instance
125 22         98 my $inst_method = $coo->can("instance");
126 22 50       47 if (not $inst_method) {
127 0         0 confess "incompatible class $coo from:".(join "|", caller 1);
128             }
129 22         54 return &$inst_method($coo);
130             }
131              
132             # system configuration
133             sub sysconf
134             {
135 10     10 0 2394 my $key = shift;
136 10 100       40 return if not exists $_sysconf{$key};
137 9         154 return $_sysconf{$key};
138             }
139              
140             # Perl configuration
141             sub perlconf
142             {
143 5     5 0 2887 my $key = shift;
144 5 100       21 return if not exists $_perlconf{$key};
145 4         23 return $_perlconf{$key};
146             }
147              
148             # platform configuration
149             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
150 1     1   106 sub _platconf { return \%_platconf; } # for testing
151             ## use critic (Subroutines::ProhibitUnusedPrivateSubroutines)
152             sub platconf
153             {
154 22     22 0 1148 my ($class_or_obj, $key) = @_;
155 22         51 my $self = class_or_obj($class_or_obj);
156              
157 22 50       240 return if not defined $self->platform();
158 22 100       60 return if not exists $_platconf{$key}{$self->platform()};
159 21         41 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   2448 my ($class, @params) = @_;
172              
173             # enforce class lineage
174 2 50       18 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         5 my %obj;
180 2 50       8 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         8 $obj{_config} = {@params};
185             }
186             }
187              
188             # bless instance
189 2         5 my $obj_ref = bless \%obj, $class;
190              
191             # initialization
192 2 50       16 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         7 $obj_ref->collect_sysenv();
203              
204             # instantiate object
205 2         39 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 56 my $value = shift;
213 12 100 66     202 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 34 my ($class_or_obj, $value) = @_;
224 14         24 my $self = class_or_obj($class_or_obj);
225              
226 14 50       34 if (defined $value) {
227 0         0 $self->{debug} = $value;
228             }
229 14         62 return $self->{debug};
230             }
231              
232             # read-only accessor for quiet flag
233             sub quiet
234             {
235 3     3 0 2296 my ($class_or_obj) = @_;
236 3         9 my $self = class_or_obj($class_or_obj);
237              
238 3         11 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 27696 my ($class_or_obj, $key, $value) = @_;
246 241         392 my $self = class_or_obj($class_or_obj);
247              
248 241 100       463 if (defined $value) {
249 40         169 $self->{sysenv}{$key} = $value;
250             }
251 241         1425 return $self->{sysenv}{$key};
252             }
253              
254             # return system platform type
255             sub platform
256             {
257 71     71 0 152 my ($class_or_obj) = @_;
258 71         127 my $self = class_or_obj($class_or_obj);
259              
260 71         172 return $self->sysenv("platform");
261             }
262              
263             # return system packager type, or undef if not determined
264             sub packager
265             {
266 22     22 0 79 my ($class_or_obj) = @_;
267 22         64 my $self = class_or_obj($class_or_obj);
268              
269 22         56 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 665 my ($class_or_obj, @cmd) = @_;
345 7         16 my $self = class_or_obj($class_or_obj);
346 7 50       40 $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         11 my %args;
350 7 50       31 if (ref $cmd[0] eq "HASH") {
351 0         0 %args = %{shift @cmd};
  0         0  
352             }
353              
354             # capture output
355 7         13 my @output;
356 7         26 my $cmd = join( " ", @cmd);
357              
358             # @cmd is concatenated into $cmd - any args which need quotes should have them included
359             {
360 3     3   5876 no autodie;
  3         8  
  3         13  
  7         10  
361 7 50       14660 open my $fh, "-|", $cmd
362             or croak "failed to run pipe command '$cmd': $!";
363 7         3640 while (<$fh>) {
364 7         95 chomp;
365 7         194 push @output, $_;
366             }
367 7 50       463 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       112 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       272 if (deftrue($args{list})) {
385             # return an array if list option set
386 0         0 return @output;
387             }
388 7 100       458 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 69 my ($class_or_obj, $name) = @_;
406 32         54 my $self = class_or_obj($class_or_obj);
407              
408             # collect and cache path info
409 32 100 66     70 if (not defined $self->sysenv("path_list") or not defined $self->sysenv("path_flag")) {
410 2         30 $self->sysenv("path_list", [split /:/x, $ENV{PATH}]);
411 2         7 $self->sysenv("path_flag", {map { ($_ => 1) } @{$self->sysenv("path_list")}});
  18         49  
  2         7  
412 2         8 my $path_flag = $self->sysenv("path_flag");
413 2         7 foreach my $dir (@{sysconf("search_path")}) {
  2         7  
414 12 100       188 -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         55 foreach my $element (@{$self->sysenv("path_list")}) {
  32         61  
424 246         585 my $filepath = "$element/$name";
425 246 100       2885 if (-x $filepath) {
426 14         73 return $filepath;
427             }
428             }
429 18         71 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   13230 no autodie;
  3         7  
  3         11  
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 11 my ($class_or_obj) = @_;
654 2         5 my $self = class_or_obj($class_or_obj);
655              
656             # collect uname info
657 2         18 my $uname = $self->sysenv("uname");
658 2 50       10 if (defined $uname) {
659             # Unix-like systems all have uname
660 2         24 $self->sysenv("os", $self->capture_cmd($uname, "-s"));
661 2         29 $self->sysenv("kernel", $self->capture_cmd($uname, "-r"));
662 2         35 $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         40 my $osrelease = Sys::OsRelease->instance(common_id => sysconf("common_id"));
671 2         3855 $self->sysenv("platform", $osrelease->platform());
672              
673             # determine system's packager if possible
674 2         28 my $plat_packager = $self->platconf("packager");
675 2 50       13 if (defined $plat_packager) {
676 2         13 $self->sysenv("packager", $plat_packager);
677             }
678              
679             # display system info
680 2         16 my $detected;
681 2 50       17 if (defined $osrelease->osrelease_path()) {
682 2 50       58 if ($self->platform() eq $osrelease->id()) {
683 0         0 $detected = $self->platform();
684             } else {
685 2         24 $detected = $osrelease->id()." -> ".$self->platform();
686             }
687 2 50       26 if (defined $self->sysenv("packager")) {
688 2         8 $detected .= " handled by ".$self->sysenv("packager");
689             }
690              
691             } else {
692 0         0 $detected = $self->platform()." (no os-release data)";
693             }
694 2 50       22 if (not $self->quiet()) {
695 0         0 print $self->text_green()."system detected: $detected".$self->text_color_reset()."\n";
696             }
697 2         32 return;
698             }
699              
700             # collect system environment info
701             sub collect_sysenv
702             {
703 2     2 0 5 my ($class_or_obj) = @_;
704 2         7 my $self = class_or_obj($class_or_obj);
705 2         5 my $sysenv = $self->{sysenv};
706              
707             # find command locations
708 2         4 foreach my $cmd (@{sysconf("search_cmds")}) {
  2         5  
709 32 100       75 if (my $filepath = $self->cmd_path($cmd)) {
710 14         55 $sysenv->{$cmd} = $filepath;
711             }
712             }
713 2         185 $sysenv->{perl} = $Config{perlpath};
714              
715             # collect info and deduce platform type
716 2         19 $self->resolve_platform();
717              
718             # check if user is root
719 2 50       36 if ($> == 0) {
720             # set the flag to indicate they are root
721 2         52 $sysenv->{root} = 1;
722              
723             # on Alpine, refresh the package data
724 2 50       15 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       14 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         12 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   14926 no autodie;
  3         8  
  3         16  
  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       0 return (defined $self->sysenv("root")) ? ($self->sysenv("root") != 0) : 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 41 my ($class_or_obj, %args) = @_;
798 5         13 my $self = class_or_obj($class_or_obj);
799              
800 5 50       15 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       14 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     23 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       22 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       12 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         35 my $funcname = $self->packager()."::".$args{op};
839 5 50       14 $self->debug() and print STDERR "debug: $funcname(".join(" ", map {$_."=".$args{$_}} sort keys %args).")\n";
  0         0  
840 5         13 my $funcref = $self->packager()->can($args{op});
841 5 50       16 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         12 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 0 0         $self->debug() and print STDERR "debug: install_module($name)\n";
916              
917             # check if module is installed
918 0 0         if (not $self->module_installed($name)) {
919             # print header for module installation
920 0 0         if (not $self->quiet()) {
921 0           print $self->text_green().('-' x 75)."\n";
922 0           print "install $name".$self->text_color_reset()."\n";
923             }
924              
925             # try first to install it with an OS package (root required)
926 0           my $done=0;
927 0 0         if ($self->is_root()) {
928 0 0         if ($self->module_package($name)) {
929 0           $self->module_installed($name, 1);
930 0           $done=1;
931             }
932             }
933              
934             # try again with CPAN or CPANMinus if it wasn't installed by a package
935 0 0         if (not $done) {
936 0 0         my $cmd = (defined $self->sysenv("cpan") ? $self->sysenv("cpan") : $self->sysenv("cpanm"));
937 0 0         $self->run_cmd($cmd, $name)
938             or croak "failed to install $name module";
939 0           $self->module_installed($name, 1);
940             }
941             }
942 0           return;
943             }
944              
945             # bootstrap CPAN-Minus in a subdirectory of the current directory
946             sub bootstrap_cpanm
947             {
948 0     0 0   my ($class_or_obj) = @_;
949 0           my $self = class_or_obj($class_or_obj);
950              
951             # save current directory
952 0           my $old_pwd = $self->pwd();
953              
954             # make build directory and change into it
955 0 0         if (not -d "build") {
956 3     3   13396 no autodie;
  3         15  
  3         14  
957 0 0         mkdir "build", 0755
958             or croak "can't make build directory in current directory: $!";
959             }
960 0           chdir "build";
961              
962             # verify required commands are present
963 0           my @missing;
964 0           foreach my $cmd (@{perlconf("cpan_deps")}) {
  0            
965 0 0         if (not defined $self->sysenv("$cmd")) {
966 0           push @missing, $cmd;
967             }
968             }
969 0 0         if (scalar @missing > 0) {
970 0           croak "missing ".(join ", ", @missing)." command - can't bootstrap cpanm";
971             }
972              
973             # download cpanm
974 0           my $perl_sources = perlconf("sources");
975             $self->run_cmd($self->sysenv("curl"), "-L", "--output", "app-cpanminus.tar.gz",
976 0 0         $perl_sources->{"App::cpanminus"}
977             )
978             or croak "download failed for App::cpanminus";
979 0           my @cpanm_path = grep {qr(/bin/cpanm$)x} ($self->capture_cmd({list=>1}, $self->sysenv("tar"),
  0            
980             qw(-tf app-cpanminus.tar.gz)));
981 0           my $cpanm_path = pop @cpanm_path;
982 0           $self->run_cmd($self->sysenv("tar"), "-xf", "app-cpanminus.tar.gz", $cpanm_path);
983             {
984 3     3   9315 no autodie;
  3         10  
  3         13  
  0            
985 0 0         chmod 0755, $cpanm_path
986             or croak "failed to chmod $cpanm_path:$!";
987             }
988 0           $self->sysenv("cpanm", $self->pwd()."/".$cpanm_path);
989              
990             # change back up to previous directory
991 0           chdir $old_pwd;
992 0           return;
993             }
994              
995             # establish CPAN if not already present
996             sub establish_cpan
997             {
998 0     0 0   my ($class_or_obj) = @_;
999 0           my $self = class_or_obj($class_or_obj);
1000              
1001             # first get package dependencies for CPAN (and CPAN too if available via OS package)
1002 0 0         if ($self->is_root()) {
1003             # package dependencies for CPAN (i.e. make, or oddly-named OS package that contains CPAN)
1004 0           my @deps = $self->cpan_prereqs();
1005 0           $self->call_pkg_driver(op => "install", pkg => \@deps);
1006              
1007             # check for commands which were installed by their package name, and specifically look for cpan by any package
1008 0           foreach my $dep (@deps, "cpan") {
1009 0 0         if (my $filepath = $self->cmd_path($dep)) {
1010 0           $self->sysenv($dep, $filepath);
1011             }
1012             }
1013             }
1014              
1015             # install CPAN-Minus if neither CPAN nor CPAN-Minus exist
1016 0 0 0       if (not defined $self->sysenv("cpan") and not defined $self->sysenv("cpanm")) {
1017             # try to install CPAN-Minus as an OS package
1018 0 0         if ($self->is_root()) {
1019 0 0         if ($self->module_package("App::cpanminus")) {
1020 0           $self->sysenv("cpanm", $self->cmd_path("cpanm"));
1021             }
1022             }
1023              
1024             # try again if it wasn't installed by a package
1025 0 0         if (not defined $self->sysenv("cpanm")) {
1026 0           $self->bootstrap_cpanm();
1027             }
1028             }
1029              
1030             # install CPAN if it doesn't exist
1031 0 0         if (not defined $self->sysenv("cpan")) {
1032             # try to install CPAN as an OS package
1033 0 0         if ($self->is_root()) {
1034 0 0         if ($self->module_package("CPAN")) {
1035 0           $self->sysenv("cpan", $self->cmd_path("cpan"));
1036             }
1037             }
1038              
1039             # try again with cpanminus if it wasn't installed by a package
1040 0 0         if (not defined $self->sysenv("cpan")) {
1041 0 0         if ($self->run_cmd($self->sysenv("perl"), $self->sysenv("cpanm"), "CPAN")) {
1042 0           $self->sysenv("cpan", $self->cmd_path("cpan"));
1043             }
1044             }
1045             }
1046              
1047             # install dependencies for this tool
1048 0           foreach my $dep (@{perlconf("module_deps")}) {
  0            
1049 0           $self->install_module($dep);
1050             }
1051 0           return;
1052             }
1053              
1054             1;
1055              
1056             =pod
1057              
1058             =encoding UTF-8
1059              
1060             =head1 NAME
1061              
1062             Sys::OsPackage - install OS packages and determine if CPAN modules are packaged for the OS
1063              
1064             =head1 VERSION
1065              
1066             version 0.1.7
1067              
1068             =head1 SYNOPSIS
1069              
1070             use Sys::OsPackage;
1071             my $ospackage = Sys::OsPackage->instance();
1072             foreach my $module ( qw(module-name ...)) {
1073             $ospackage->install_module($module);
1074             }
1075              
1076             =head1 DESCRIPTION
1077              
1078             I is used for installing Perl module dependencies.
1079             It can look up whether a Perl module is available under some operating systems' packages.
1080             If the module is available as an OS package, it installs it via the packaging system of the OS.
1081             Otherwise it runs CPAN to install the module.
1082              
1083             The use cases of I include setting up systems or containers with Perl modules using OS packages
1084             as often as possible. It can also be used fvor installing dependencies for a Perl script on an existing system.
1085              
1086             OS packaging systems currently supported by I are the Linux distributions Alpine, Arch, Debian,
1087             Fedora and OpenSuse.
1088             Using L it's able to detect operating systems derived from a supported platform use the correct driver.
1089              
1090             RHEL and CentOS are supported by the Fedora driver.
1091             CentOS-derived systems Rocky and Alma are supported by recognizing them as derivatives.
1092             Ubuntu is supported by the Debian driver.
1093              
1094             Other packaging systems for Unix-like operating systems should be feasible to add by writing a driver module.
1095              
1096             =head1 SEE ALSO
1097              
1098             L comes with I to provide a command-line interface.
1099              
1100             L
1101              
1102             "pacman/Rosetta" at Arch Linux Wiki compares commands of 5 Linux packaging systems L
1103              
1104             GitHub repository for Sys::OsPackage: L
1105              
1106             =head1 BUGS AND LIMITATIONS
1107              
1108             Please report bugs via GitHub at L
1109              
1110             Patches and enhancements may be submitted via a pull request at L
1111              
1112             =head1 LICENSE INFORMATION
1113              
1114             Copyright (c) 2022 by Ian Kluft
1115              
1116             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.
1117              
1118             =head1 AUTHOR
1119              
1120             Ian Kluft
1121              
1122             =head1 COPYRIGHT AND LICENSE
1123              
1124             This software is Copyright (c) 2022 by Ian Kluft.
1125              
1126             This is free software, licensed under:
1127              
1128             The Artistic License 2.0 (GPL Compatible)
1129              
1130             =cut
1131              
1132             __END__