File Coverage

blib/lib/Sys/OsRelease.pm
Criterion Covered Total %
statement 153 170 90.0
branch 56 78 71.7
condition 8 12 66.6
subroutine 29 32 90.6
pod 12 16 75.0
total 258 308 83.7


line stmt bran cond sub pod time code
1             # Sys::OsRelease
2             # ABSTRACT: read operating system details from standard /etc/os-release file
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 must be maintained for minimal dependencies so it can be used to build systems and containers.
8              
9             ## no critic (Modules::RequireExplicitPackage)
10             # This resolves conflicting Perl::Critic rules which want package and strictures each before the other
11 4     4   12471 use strict;
  4         26  
  4         105  
12 4     4   16 use warnings;
  4         6  
  4         87  
13 4     4   2245 use utf8;
  4         52  
  4         18  
14             ## use critic (Modules::RequireExplicitPackage)
15              
16             package Sys::OsRelease;
17             $Sys::OsRelease::VERSION = '0.2.2';
18 4     4   183 use Config;
  4         8  
  4         166  
19 4     4   23 use Carp qw(carp croak);
  4         6  
  4         1632  
20              
21             # the instance - use Sys::OsRelease->instance() to get it
22             my %_instances = ();
23              
24             # default search path and file name for os-release file
25             my @std_search_path = qw(/etc /usr/lib /run/host);
26             my $std_file_name = "os-release";
27              
28             # defined attributes from FreeDesktop's os-release standard - this needs to be kept up-to-date with the standard
29             my @std_attrs = qw(NAME ID ID_LIKE PRETTY_NAME CPE_NAME VARIANT VARIANT_ID VERSION VERSION_ID VERSION_CODENAME
30             BUILD_ID IMAGE_ID IMAGE_VERSION HOME_URL DOCUMENTATION_URL SUPPORT_URL BUG_REPORT_URL PRIVACY_POLICY_URL
31             LOGO ANSI_COLOR DEFAULT_HOSTNAME SYSEXT_LEVEL);
32              
33             # OS ID strings which are preferred as common if found in ID_LIKE
34             my %common_id = (
35             alpine => 1,
36             arch => 1,
37             fedora => 1,
38             debian => 1,
39             opensuse => 1,
40             );
41              
42             # call destructor when program ends
43             END {
44 4     4   5077 foreach my $class (keys %_instances) {
45 1         9 $class->clear_instance();
46             }
47 4         20 undef %_instances;
48             }
49              
50             #
51             # singleton management methods
52             # These can be imported by another class by using the import_singleton() method. That was done for Sys::OsPackage,
53             # to avoid copying those methods. But other classes with a similar need to minimize module dependencies which already
54             # use Sys::OsRelease can do this too.
55             #
56              
57             # alternative method to initiate initialization without returning a value
58             sub init
59             {
60 0     0 1 0 my ($class, @params) = @_;
61 0         0 $class->instance(@params);
62 0         0 return;
63             }
64              
65             # new method calls instance
66             sub new
67             {
68 0     0 1 0 my ($class, @params) = @_;
69 0         0 return $class->instance(@params);
70             }
71              
72             # singleton class instance
73             sub instance
74             {
75 27     27 1 61528 my ($class, @params) = @_;
76              
77             # initialize if not already done
78 27 100       62 if (not $class->defined_instance()) {
79 11         39 $_instances{$class} = $class->_new_instance(@params);
80             }
81              
82             # return singleton instance
83 27         75 return $_instances{$class};
84             }
85              
86             # test if instance is defined for testing
87             sub defined_instance
88             {
89 40     40 1 696 my $class = shift;
90 40 100 66     269 return ((exists $_instances{$class}) and $_instances{$class}->isa($class)) ? 1 : 0;
91             }
92              
93             # clear instance for exit-cleanup or for re-use in testing
94             sub clear_instance
95             {
96 11     11 1 4357 my $class = shift;
97 11 50       39 if ($class->defined_instance()) {
98             # clean up anything that the destructor will miss, such as auto-generated methods
99 11 50       70 if ($class->can("_cleanup_instance")) {
100 11         31 $class->_cleanup_instance();
101             }
102              
103             # dereferencing will destroy singleton instance
104 11         27 delete $_instances{$class};
105             }
106 11         83 return;
107             }
108              
109             # allow other classes which cooperate with Sys::OsRelease to import our singleton-management methods
110             # This helps maintain minimal prerequisites among modules working to set up Perl on containers or new systems.
111             sub import_singleton
112             {
113 1     1 1 2042 my $class = shift;
114 1         3 my $caller_class = caller;
115              
116             # export singleton-management methods to caller class
117 1         3 foreach my $method_name (qw(init new instance defined_instance clear_instance)) {
118             ## no critic (TestingAndDebugging::ProhibitNoStrict)
119 4     4   34 no strict 'refs';
  4         8  
  4         5869  
120 5         7 *{$caller_class."::".$method_name} = \&{$class."::".$method_name};
  5         44  
  5         13  
121             }
122 1         4 return;
123             }
124              
125             #
126             # os-release data access methods
127             #
128              
129             # access module constants
130 1     1 0 481 sub std_search_path { return @std_search_path; }
131 1     1 0 72 sub std_attrs { return @std_attrs; }
132              
133             # fold case for case-insensitive matching
134             my $can_fc = CORE->can("fc"); # test fc() once and save result
135             sub fold_case
136             {
137 795     795 0 996 my $str = shift;
138              
139             # use fc if available, otherwise lc to support older Perls
140 795 50       3806 return $can_fc ? $can_fc->($str) : lc($str);
141             }
142              
143             # initialize a new instance
144             sub _new_instance
145             {
146 11     11   51 my ($class, @params) = @_;
147              
148             # enforce class lineage - _new_instance() should be overloaded by other classes that import singleton methods
149 11 50       50 if (not $class->isa(__PACKAGE__)) {
150 0 0       0 croak "_new_instance() should be overloaded by calling class: "
151             .(ref $class ? ref $class : $class)." is not a ".__PACKAGE__;
152             }
153              
154             # obtain parameters from array or hashref
155 11         21 my %obj;
156 11 100       30 if (scalar @params > 0) {
157 9 50       23 if (ref $params[0] eq 'HASH') {
158 0         0 $obj{_config} = $params[0];
159             } else {
160 9         32 $obj{_config} = {@params};
161             }
162             }
163              
164             # locate os-release file in standard places
165 11         23 my $osrelease_path;
166 11 100       29 my @search_path = ((exists $obj{_config}{search_path}) ? @{$obj{_config}{search_path}} : @std_search_path);
  9         24  
167 11 100       34 my $file_name = ((exists $obj{_config}{file_name}) ? $obj{_config}{file_name} : $std_file_name);
168 11         22 foreach my $search_dir (@search_path) {
169 10 50       510 if (-r "$search_dir/$file_name") {
170 10         55 $osrelease_path = $search_dir."/".$file_name;
171 10         21 last;
172             }
173             }
174              
175             # If we found os-release on this system, read it
176             # otherwise leave everything empty and platform() method will use Perl's $Config{osname} as a summary value
177 11 100       22 if (defined $osrelease_path) {
178             # save os-release file path
179 10         29 $obj{_config}{osrelease_path} = $osrelease_path;
180              
181             # read os-release file
182             ## no critic (InputOutput::RequireBriefOpen)
183 10 50       541 if (open my $fh, "<", $osrelease_path) {
184 10         1331 while (my $line = <$fh>) {
185 119         212 chomp $line; # remove trailing nl
186 119 50       209 if (substr($line, -1, 1) eq "\r") {
187 0         0 $line = substr($line, 0, -1); # remove trailing cr
188             }
189              
190             # skip comments and blank lines
191 119 50 33     438 if ($line =~ /^ \s+ #/x or $line =~ /^ \s+ $/x) {
192 0         0 next;
193             }
194              
195             # read attribute assignment lines
196 119 100 66     450 if ($line =~ /^ ([A-Z0-9_]+) = "(.*)" $/x
      100        
197             or $line =~ /^ ([A-Z0-9_]+) = '(.*)' $/x
198             or $line =~ /^ ([A-Z0-9_]+) = (.*) $/x)
199             {
200 117 50       234 next if $1 eq "_config"; # don't overwrite _config
201 117         167 $obj{fold_case($1)} = $2;
202             }
203             }
204 10         176 close $fh;
205             }
206             }
207              
208             # bless instance and generate accessor methods
209 11         53 my $obj_ref = bless \%obj, $class;
210 11         44 $obj_ref->_gen_accessors();
211              
212             # instantiate object
213 11         31 return $obj_ref;
214             }
215              
216             # helper function to allow methods to get the instance ref when called via the class name
217             sub class_or_obj
218             {
219 993     993 0 1106 my $coo = shift;
220              
221             # return the instance
222 993 100       1650 return ((ref $coo) ? $coo : $coo->instance());
223             }
224              
225             # clean up data in an instance before feeding it to the destructor
226             sub _cleanup_instance
227             {
228 11     11   24 my ($class_or_obj) = @_;
229 11         19 my $self = class_or_obj($class_or_obj);
230              
231             # enforce class lineage - _cleanup_instance() should be overloaded by other classes that import singleton methods
232 11 50       48 if (not $self->isa(__PACKAGE__)) {
233 0         0 croak "_new_instance() should be overloaded by calling class: "
234             .(ef $self)." is not a ".__PACKAGE__;
235             }
236              
237             # clear accessor functions
238 11         16 foreach my $acc (keys %{$self->{_config}{accessor}}) {
  11         106  
239 256         361 $self->_clear_accessor($acc);
240             }
241 11         28 return;
242             }
243              
244             # determine platform type
245             sub platform
246             {
247 4     4 1 1223 my ($class_or_obj) = @_;
248 4         7 my $self = class_or_obj($class_or_obj);
249            
250             # if we haven't already saved this result, compute and save it
251 4 100       8 if (not $self->has_config("platform")) {
252 2 100       5 if ($self->has_attr("id")) {
253 1         2 $self->config("platform", $self->id);
254             }
255 2 100       4 if ($self->has_attr("id_like")) {
256             # check if the configuration has additional common IDs which should be recognized if seen in ID_LIKE
257 1 50       6 if ($self->has_config("common_id")) {
258 0         0 my $cids = $self->config("common_id");
259 0 0       0 my @cids = (ref $cids eq "ARRAY") ? (@{$cids}) : (split /\s+/x, $cids);
  0         0  
260 0         0 foreach my $cid (@cids) {
261 0         0 $common_id{$cid} = 1;
262             }
263             }
264              
265             # check ID_LIKE for more common names which should be used instead of ID
266 1         3 foreach my $like (split /\s+/x, $self->id_like) {
267 1 50       3 if (exists $common_id{$like}) {
268 1         3 $self->config("platform", $like);
269 1         2 last;
270             }
271             }
272             }
273              
274             # if platform is still not set, use Perl's osname config as a summary value
275 2 100       4 if (not $self->has_config("platform")) {
276 1         3 $self->config("platform", $Config{osname});
277             }
278             }
279 4         8 return $self->config("platform");
280             }
281              
282             # get location of the os-release file found on this system
283             # return undef if the file was not found
284             sub osrelease_path
285             {
286 1     1 1 5 my ($class_or_obj) = @_;
287 1         3 my $self = class_or_obj($class_or_obj);
288 1 50       3 if (exists $self->{_config}{osrelease_path}) {
289 1         3 return $self->{_config}{osrelease_path};
290             }
291 0         0 return;
292             }
293              
294             # attribute existence checker
295             sub has_attr
296             {
297 341     341 1 43413 my ($class_or_obj, $key) = @_;
298 341         553 my $self = class_or_obj($class_or_obj);
299 341 100       672 return ((exists $self->{fold_case($key)}) ? 1 : 0);
300             }
301              
302             # attribute read-only accessor
303             sub get
304             {
305 95     95 1 205 my ($class_or_obj, $key) = @_;
306 95         161 my $self = class_or_obj($class_or_obj);
307 95         163 return $self->{fold_case($key)};
308             }
309              
310             # attribute existence checker
311             sub has_config
312             {
313 11     11 1 10343 my ($class_or_obj, $key) = @_;
314 11         15 my $self = class_or_obj($class_or_obj);
315 11 100       36 return ((exists $self->{_config}{$key}) ? 1 : 0);
316             }
317              
318             # config read/write accessor
319             sub config
320             {
321 7     7 1 21 my ($class_or_obj, $key, $value) = @_;
322 7         11 my $self = class_or_obj($class_or_obj);
323 7 100       15 if (defined $value) {
324 3         5 $self->{_config}{$key} = $value;
325             }
326 7         25 return $self->{_config}{$key};
327             }
328              
329             # generate accessor methods for all defined and standardized attributes
330             # private internal method
331             sub _gen_accessors
332             {
333 11     11   24 my ($class_or_obj) = @_;
334 11         20 my $self = class_or_obj($class_or_obj);
335              
336             # generate read-only accessors for attributes actually found in os-release
337 11         19 foreach my $key (sort keys %{$self}) {
  11         99  
338 128 100       212 next if $key eq "_config"; # protect special/reserved attribute
339 117         186 $self->_gen_accessor($key);
340             }
341              
342             # generate undef accessors for standardized attributes which were not found in os-release
343 11         29 foreach my $std_attr (@std_attrs) {
344 242 50       352 next if $std_attr eq "_config"; # protect special/reserved attribute
345 242         316 my $fc_attr = fold_case($std_attr);
346 242 100       362 next if $self->has_attr($fc_attr);
347 139         206 $self->_gen_accessor($fc_attr);
348             }
349 11         14 return;
350             }
351              
352             # generate accessor
353             # private internal method
354             sub _gen_accessor
355             {
356 256     256   351 my ($class_or_obj, $name) = @_;
357 256         306 my $self = class_or_obj($class_or_obj);
358 256 50       372 my $class = (ref $self) ? (ref $self) : $self;
359 256         349 my $method_name = $class."::".$name;
360              
361             # mark accessor flag in configuration so it can be deleted for cleanup (mainly for testing)
362 256 100       586 if (not exists $self->{_config}{accessor}) {
363 11         31 $self->{_config}{accessor} = {};
364             }
365              
366             # generate accessor as read-only or undef depending whether it exists in the running system
367 256 100       356 if (exists $self->{$name}) {
368             # generate read-only accessor for attribute which was found in os-release
369 117     3   355 $self->{_config}{accessor}{$name} = sub { return $self->{$name} };
  3         11  
370             } else {
371             # generate undef accessor for standard attribute which was not found in os-release
372 139     0   440 $self->{_config}{accessor}{$name} = sub { return; };
  0         0  
373             }
374              
375             ## no critic (TestingAndDebugging::ProhibitNoStrict)
376 4     4   35 no strict 'refs';
  4         7  
  4         516  
377 256         369 *{$method_name} = $self->{_config}{accessor}{$name};
  256         610  
378 256         441 return;
379             }
380              
381             # clean up accessor
382             # private internal method
383             sub _clear_accessor
384             {
385 256     256   343 my ($class_or_obj, $name) = @_;
386 256         305 my $self = class_or_obj($class_or_obj);
387 256 50       362 my $class = (ref $self) ? (ref $self) : $self;
388 256 50       422 if (exists $self->{_config}{accessor}{$name}) {
389 256         364 my $method_name = $class."::".$name;
390             ## no critic (TestingAndDebugging::ProhibitNoStrict)
391 4     4   26 no strict 'refs';
  4         7  
  4         491  
392 256         261 undef *{$method_name};
  256         572  
393 256         893 delete $self->{_config}{accessor}{$name};
394             }
395 256         427 return;
396             }
397              
398             1;
399              
400             =pod
401              
402             =encoding UTF-8
403              
404             =head1 NAME
405              
406             Sys::OsRelease - read operating system details from standard /etc/os-release file
407              
408             =head1 VERSION
409              
410             version 0.2.2
411              
412             =head1 SYNOPSIS
413              
414             non-object-oriented:
415              
416             Sys::OsRelease->init();
417             my $id = Sys::OsRelease->id();
418             my $id_like = Sys::OsRelease->id_like();
419              
420             object-oriented:
421              
422             my $osrelease = Sys::OsRelease->instance();
423             my $id = $osrelease->id();
424             my $id_like = $osrelease->id_like();
425              
426             =head1 DESCRIPTION
427              
428             Sys::OsRelease is a helper library to read the /etc/os-release file, as defined by FreeDesktop.Org.
429             The os-release file is used to define an operating system environment.
430             It has been in widespread use among Linux distributions since 2017 and BSD variants since 2020.
431             It was started on Linux systems which use the systemd software, but then spread to other Linux, BSD and
432             Unix-based systems.
433             Its purpose is to identify the system to any software which needs to know.
434             It differentiates between Unix-based operating systems and even between Linux distributions.
435              
436             Sys::OsRelease is implemented with a singleton model, meaning there is only one instance of the class.
437             Instead of instantiating an object with new(), the instance() class method returns the one and only instance.
438             The first time it's called, it instantiates it.
439             On following calls, it returns a reference to the singleton instance.
440              
441             This module maintains minimal prerequisites, and only those which are usually included with Perl.
442             (Suggestions of new features and code will have to follow this rule.)
443             That is intended to be acceptable for establishing system or container environments which contain Perl programs.
444             It can also be used for installing or configuring software that needs to know about the system environment.
445              
446             =head2 The os-release Standard
447              
448             FreeDesktop.Org's os-release standard is at L.
449              
450             Current attributes recognized by Sys::OsRelease are:
451             NAME ID ID_LIKE PRETTY_NAME CPE_NAME VARIANT VARIANT_ID VERSION VERSION_ID VERSION_CODENAME BUILD_ID IMAGE_ID
452             IMAGE_VERSION HOME_URL DOCUMENTATION_URL SUPPORT_URL BUG_REPORT_URL PRIVACY_POLICY_URL LOGO ANSI_COLOR
453             DEFAULT_HOSTNAME SYSEXT_LEVEL
454              
455             If other attributes are found in the os-release file, they will be accepted.
456             Folded to lower case, the attribute names are used as keys in an internal hash structure.
457              
458             =head1 NAME
459              
460             Sys::OsRelease - read operating system details from standard /etc/os-release file
461              
462             =head1 METHODS
463              
464             =over 1
465              
466             =item init([key => value, ...])
467              
468             initializes the singleton instance without returning a value.
469             Parameters are passed to the instance() method.
470             This method is for cases where method calls will be via the class name, and the program
471             doesn't need a reference to the instance.
472              
473             Under normal circumstances no parameters are needed. See instance() for possible parameters.
474              
475             =item new([key => value, ...])
476              
477             initializes the singleton instance and returns a reference to it.
478             Parameters are passed to the instance() method.
479             This is equivalent to using the instance() method, made available if new() sounds more comfortable.
480              
481             Under normal circumstances no parameters are needed. See instance() for possible parameters.
482              
483             =item instance([key => value, ...])
484              
485             initializes the singleton instance and returns a reference to it.
486              
487             Under normal circumstances no parameters are needed. Possible optional parameters are as follows:
488              
489             =over 1
490              
491             =item common_id
492              
493             supplies an arrayref to use as a list of additional common strings which should be recognized by the platform()
494             method, if they occur in the ID_LIKE attribute in the os-release file. By default, "debian" and "fedora" are
495             regonized by platform() as common names and it will return them instead of the system's ID attribute.
496              
497             =item search_path
498              
499             supplies an arrayref of strings with directories to use as the search path for the os-release file.
500              
501             =item file_name
502              
503             supplies a string with the basename of the file to look for the os-release file.
504             Obviously the default file name is "os-release".
505             Under normal circumstances there is no need to set this.
506             Currently this is only used for testing, where suffixes are added for copies of various different systems'
507             os-release files, to indicate which system they came from.
508              
509             =back
510              
511             =item platform()
512              
513             returns a string with the platform type. On systems with /etc/os-release (or os-release in any location
514             from the standard) this is usually from the ID field.
515             On systems that use the ID_LIKE field, systems that claim to be like "debian" or "fedora" (always in lower case)
516             will return those names for the platform.
517              
518             The list of recognized common platforms can be modified by passing a "common_id" parameter to instance()/new()
519             with an arrayref containing additional names to recognize as common. For example, "centos" is another possibility.
520             It was not included in the default because CentOS is discontinued. Both Rocky Linux and Alma Linux have
521             ID_LIKE fields of "rhel centos fedora", which will match "fedora" with the default setting, but could be configured
522             via "common_id" to recognize "centos" since it's listed first in ID_LIKE.
523              
524             On systems where an os-release file doesn't exist or isn't found, the platform string will fall back to Perl's
525             $Config{osname} setting for the system.
526              
527             =item osrelease_path()
528              
529             returns the path where os-release was found.
530              
531             The default search path is /etc, /usr/lib and /run/host as defined by the standard.
532             The search path can be replaced by providing a "search_path" parameter to instance()/new() with an arrayref
533             containing the directories to search. This feature is currently only used for testing purposes.
534              
535             =item defined_instance()
536              
537             returns true if the singleton instance is defined, false if it is not yet defined or has been cleared.
538              
539             =item has_attr(name)
540              
541             returns a boolean which is true if the attribute named by the string parameter exists in the os-release data for the
542             current system.
543             The attribute name is case insensitive.
544              
545             =item get(name)
546              
547             is a read-only accessor which returns the value of the os-release attribute named by the string parameter,
548             or undef if it doesn't exist.
549              
550             =item has_config(name)
551              
552             returns a boolean which is true if Sys::OsRelease contains a configuration setting named by the string parameter.
553              
554             =item config(name, [value])
555              
556             is a read/write accessor for the configuration setting named by the string parameter "name".
557             If no value parameter is provided, it returns the value of the parameter, or undef if it doesn't exist.
558             If a value parameter is provided, it assigns that to the configuration setting and returns the same value.
559              
560             =item clear_instance()
561              
562             removes the singleton instance of the class if it was defined.
563             Under normal circumstances it is not necessary to call this since the class destructor will call it automatically.
564             It is currently only used for testing, where it is necessary to clear the instance before loading a new one with
565             different parameters.
566              
567             Since this class is based on the singleton model, there is only one instance.
568             The instance(), new() and init() methods will only initialize the instance if it is not already initialized.
569              
570             =item import_singleton
571              
572             The singleton-management methods I, I, I, I and I
573             can be imported by another class by using the import_singleton() method.
574             That was done for L, to allow it to avoid copying those methods.
575             But other classes with a similar need to minimize module dependencies which already
576             use I can do this too.
577             This helps maintain minimal prerequisites among modules working to set up Perl on containers or new systems.
578              
579             =back
580              
581             =head1 SEE ALSO
582              
583             FreeDesktop.Org's os-release standard: L
584              
585             GitHub repository for Sys::OsRelease: L
586              
587             Related modules:
588              
589             =over 1
590              
591             =item L
592              
593             installs Perl modules, for example as dependencies of a script, via OS packages if available or otherwise via CPAN -
594             uses Sys::OsRelease to determine OS type
595              
596             =item L
597              
598             system information collected from multiple sources including system architecture, hardware, OS release data
599              
600             =back
601              
602             =head1 BUGS AND LIMITATIONS
603              
604             Please report bugs via GitHub at L
605              
606             Patches and enhancements may be submitted via a pull request at L
607              
608             =head1 LICENSE INFORMATION
609              
610             Copyright (c) 2022 by Ian Kluft
611              
612             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.
613              
614             =head1 AUTHOR
615              
616             Ian Kluft
617              
618             =head1 COPYRIGHT AND LICENSE
619              
620             This software is Copyright (c) 2022 by Ian Kluft.
621              
622             This is free software, licensed under:
623              
624             The Artistic License 2.0 (GPL Compatible)
625              
626             =cut
627              
628             __END__