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   10823 use strict;
  4         21  
  4         95  
12 4     4   17 use warnings;
  4         4  
  4         95  
13 4     4   2000 use utf8;
  4         48  
  4         19  
14             ## use critic (Modules::RequireExplicitPackage)
15              
16             package Sys::OsRelease;
17             $Sys::OsRelease::VERSION = '0.2.3';
18 4     4   166 use Config;
  4         4  
  4         146  
19 4     4   18 use Carp qw(carp croak);
  4         14  
  4         1505  
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   4719 foreach my $class (keys %_instances) {
45 1         10 $class->clear_instance();
46             }
47 4         21 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 56735 my ($class, @params) = @_;
76              
77             # initialize if not already done
78 27 100       50 if (not $class->defined_instance()) {
79 11         24 $_instances{$class} = $class->_new_instance(@params);
80             }
81              
82             # return singleton instance
83 27         61 return $_instances{$class};
84             }
85              
86             # test if instance is defined for testing
87             sub defined_instance
88             {
89 40     40 1 641 my $class = shift;
90 40 100 66     188 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 4198 my $class = shift;
97 11 50       22 if ($class->defined_instance()) {
98             # clean up anything that the destructor will miss, such as auto-generated methods
99 11 50       42 if ($class->can("_cleanup_instance")) {
100 11         23 $class->_cleanup_instance();
101             }
102              
103             # dereferencing will destroy singleton instance
104 11         24 delete $_instances{$class};
105             }
106 11         52 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 1810 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   31 no strict 'refs';
  4         8  
  4         5511  
120 5         6 *{$caller_class."::".$method_name} = \&{$class."::".$method_name};
  5         36  
  5         13  
121             }
122 1         3 return;
123             }
124              
125             #
126             # os-release data access methods
127             #
128              
129             # access module constants
130 1     1 0 466 sub std_search_path { return @std_search_path; }
131 1     1 0 68 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 919 my $str = shift;
138              
139             # use fc if available, otherwise lc to support older Perls
140 795 50       3397 return $can_fc ? $can_fc->($str) : lc($str);
141             }
142              
143             # initialize a new instance
144             sub _new_instance
145             {
146 11     11   41 my ($class, @params) = @_;
147              
148             # enforce class lineage - _new_instance() should be overloaded by other classes that import singleton methods
149 11 50       37 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         14 my %obj;
156 11 100       22 if (scalar @params > 0) {
157 9 50       16 if (ref $params[0] eq 'HASH') {
158 0         0 $obj{_config} = $params[0];
159             } else {
160 9         24 $obj{_config} = {@params};
161             }
162             }
163              
164             # locate os-release file in standard places
165 11         17 my $osrelease_path;
166 11 100       21 my @search_path = ((exists $obj{_config}{search_path}) ? @{$obj{_config}{search_path}} : @std_search_path);
  9         18  
167 11 100       24 my $file_name = ((exists $obj{_config}{file_name}) ? $obj{_config}{file_name} : $std_file_name);
168 11         15 foreach my $search_dir (@search_path) {
169 10 50       3892 if (-r "$search_dir/$file_name") {
170 10         36 $osrelease_path = $search_dir."/".$file_name;
171 10         15 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       20 if (defined $osrelease_path) {
178             # save os-release file path
179 10         20 $obj{_config}{osrelease_path} = $osrelease_path;
180              
181             # read os-release file
182             ## no critic (InputOutput::RequireBriefOpen)
183 10 50       340 if (open my $fh, "<", $osrelease_path) {
184 10         745 while (my $line = <$fh>) {
185 119         177 chomp $line; # remove trailing nl
186 119 50       211 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     387 if ($line =~ /^ \s+ #/x or $line =~ /^ \s+ $/x) {
192 0         0 next;
193             }
194              
195             # read attribute assignment lines
196 119 100 66     419 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       207 next if $1 eq "_config"; # don't overwrite _config
201 117         155 $obj{fold_case($1)} = $2;
202             }
203             }
204 10         112 close $fh;
205             }
206             }
207              
208             # bless instance and generate accessor methods
209 11         43 my $obj_ref = bless \%obj, $class;
210 11         32 $obj_ref->_gen_accessors();
211              
212             # instantiate object
213 11         29 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 1067 my $coo = shift;
220              
221             # return the instance
222 993 100       1664 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   20 my ($class_or_obj) = @_;
229 11         15 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       26 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         12 foreach my $acc (keys %{$self->{_config}{accessor}}) {
  11         60  
239 256         326 $self->_clear_accessor($acc);
240             }
241 11         22 return;
242             }
243              
244             # determine platform type
245             sub platform
246             {
247 4     4 1 1212 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       6 if (not $self->has_config("platform")) {
252 2 100       4 if ($self->has_attr("id")) {
253 1         3 $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       5 if (not $self->has_config("platform")) {
276 1         4 $self->config("platform", $Config{osname});
277             }
278             }
279 4         9 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         2 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 40213 my ($class_or_obj, $key) = @_;
298 341         443 my $self = class_or_obj($class_or_obj);
299 341 100       645 return ((exists $self->{fold_case($key)}) ? 1 : 0);
300             }
301              
302             # attribute read-only accessor
303             sub get
304             {
305 95     95 1 152 my ($class_or_obj, $key) = @_;
306 95         165 my $self = class_or_obj($class_or_obj);
307 95         159 return $self->{fold_case($key)};
308             }
309              
310             # attribute existence checker
311             sub has_config
312             {
313 11     11 1 10257 my ($class_or_obj, $key) = @_;
314 11         16 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 19 my ($class_or_obj, $key, $value) = @_;
322 7         8 my $self = class_or_obj($class_or_obj);
323 7 100       13 if (defined $value) {
324 3         5 $self->{_config}{$key} = $value;
325             }
326 7         27 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   18 my ($class_or_obj) = @_;
334 11         17 my $self = class_or_obj($class_or_obj);
335              
336             # generate read-only accessors for attributes actually found in os-release
337 11         12 foreach my $key (sort keys %{$self}) {
  11         83  
338 128 100       192 next if $key eq "_config"; # protect special/reserved attribute
339 117         162 $self->_gen_accessor($key);
340             }
341              
342             # generate undef accessors for standardized attributes which were not found in os-release
343 11         26 foreach my $std_attr (@std_attrs) {
344 242 50       374 next if $std_attr eq "_config"; # protect special/reserved attribute
345 242         277 my $fc_attr = fold_case($std_attr);
346 242 100       383 next if $self->has_attr($fc_attr);
347 139         206 $self->_gen_accessor($fc_attr);
348             }
349 11         17 return;
350             }
351              
352             # generate accessor
353             # private internal method
354             sub _gen_accessor
355             {
356 256     256   330 my ($class_or_obj, $name) = @_;
357 256         287 my $self = class_or_obj($class_or_obj);
358 256 50       358 my $class = (ref $self) ? (ref $self) : $self;
359 256         348 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       505 if (not exists $self->{_config}{accessor}) {
363 11         23 $self->{_config}{accessor} = {};
364             }
365              
366             # generate accessor as read-only or undef depending whether it exists in the running system
367 256 100       354 if (exists $self->{$name}) {
368             # generate read-only accessor for attribute which was found in os-release
369 117     3   295 $self->{_config}{accessor}{$name} = sub { return $self->{$name} };
  3         10  
370             } else {
371             # generate undef accessor for standard attribute which was not found in os-release
372 139     0   384 $self->{_config}{accessor}{$name} = sub { return; };
  0         0  
373             }
374              
375             ## no critic (TestingAndDebugging::ProhibitNoStrict)
376 4     4   26 no strict 'refs';
  4         7  
  4         511  
377 256         353 *{$method_name} = $self->{_config}{accessor}{$name};
  256         566  
378 256         415 return;
379             }
380              
381             # clean up accessor
382             # private internal method
383             sub _clear_accessor
384             {
385 256     256   345 my ($class_or_obj, $name) = @_;
386 256         281 my $self = class_or_obj($class_or_obj);
387 256 50       361 my $class = (ref $self) ? (ref $self) : $self;
388 256 50       368 if (exists $self->{_config}{accessor}{$name}) {
389 256         356 my $method_name = $class."::".$name;
390             ## no critic (TestingAndDebugging::ProhibitNoStrict)
391 4     4   25 no strict 'refs';
  4         7  
  4         455  
392 256         253 undef *{$method_name};
  256         491  
393 256         727 delete $self->{_config}{accessor}{$name};
394             }
395 256         378 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.3
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 METHODS
459              
460             =over 1
461              
462             =item init([key => value, ...])
463              
464             initializes the singleton instance without returning a value.
465             Parameters are passed to the instance() method.
466             This method is for cases where method calls will be via the class name, and the program
467             doesn't need a reference to the instance.
468              
469             Under normal circumstances no parameters are needed. See instance() for possible parameters.
470              
471             =item new([key => value, ...])
472              
473             initializes the singleton instance and returns a reference to it.
474             Parameters are passed to the instance() method.
475             This is equivalent to using the instance() method, made available if new() sounds more comfortable.
476              
477             Under normal circumstances no parameters are needed. See instance() for possible parameters.
478              
479             =item instance([key => value, ...])
480              
481             initializes the singleton instance and returns a reference to it.
482              
483             Under normal circumstances no parameters are needed. Possible optional parameters are as follows:
484              
485             =over 1
486              
487             =item common_id
488              
489             supplies an arrayref to use as a list of additional common strings which should be recognized by the platform()
490             method, if they occur in the ID_LIKE attribute in the os-release file. By default, "debian" and "fedora" are
491             regonized by platform() as common names and it will return them instead of the system's ID attribute.
492              
493             =item search_path
494              
495             supplies an arrayref of strings with directories to use as the search path for the os-release file.
496              
497             =item file_name
498              
499             supplies a string with the basename of the file to look for the os-release file.
500             Obviously the default file name is "os-release".
501             Under normal circumstances there is no need to set this.
502             Currently this is only used for testing, where suffixes are added for copies of various different systems'
503             os-release files, to indicate which system they came from.
504              
505             =back
506              
507             =item platform()
508              
509             returns a string with the platform type. On systems with /etc/os-release (or os-release in any location
510             from the standard) this is usually from the ID field.
511             On systems that use the ID_LIKE field, systems that claim to be like "debian" or "fedora" (always in lower case)
512             will return those names for the platform.
513              
514             The list of recognized common platforms can be modified by passing a "common_id" parameter to instance()/new()
515             with an arrayref containing additional names to recognize as common. For example, "centos" is another possibility.
516             It was not included in the default because CentOS is discontinued. Both Rocky Linux and Alma Linux have
517             ID_LIKE fields of "rhel centos fedora", which will match "fedora" with the default setting, but could be configured
518             via "common_id" to recognize "centos" since it's listed first in ID_LIKE.
519              
520             On systems where an os-release file doesn't exist or isn't found, the platform string will fall back to Perl's
521             $Config{osname} setting for the system.
522              
523             =item osrelease_path()
524              
525             returns the path where os-release was found.
526              
527             The default search path is /etc, /usr/lib and /run/host as defined by the standard.
528             The search path can be replaced by providing a "search_path" parameter to instance()/new() with an arrayref
529             containing the directories to search. This feature is currently only used for testing purposes.
530              
531             =item defined_instance()
532              
533             returns true if the singleton instance is defined, false if it is not yet defined or has been cleared.
534              
535             =item has_attr(name)
536              
537             returns a boolean which is true if the attribute named by the string parameter exists in the os-release data for the
538             current system.
539             The attribute name is case insensitive.
540              
541             =item get(name)
542              
543             is a read-only accessor which returns the value of the os-release attribute named by the string parameter,
544             or undef if it doesn't exist.
545              
546             =item has_config(name)
547              
548             returns a boolean which is true if Sys::OsRelease contains a configuration setting named by the string parameter.
549              
550             =item config(name, [value])
551              
552             is a read/write accessor for the configuration setting named by the string parameter "name".
553             If no value parameter is provided, it returns the value of the parameter, or undef if it doesn't exist.
554             If a value parameter is provided, it assigns that to the configuration setting and returns the same value.
555              
556             =item clear_instance()
557              
558             removes the singleton instance of the class if it was defined.
559             Under normal circumstances it is not necessary to call this since the class destructor will call it automatically.
560             It is currently only used for testing, where it is necessary to clear the instance before loading a new one with
561             different parameters.
562              
563             Since this class is based on the singleton model, there is only one instance.
564             The instance(), new() and init() methods will only initialize the instance if it is not already initialized.
565              
566             =item import_singleton
567              
568             The singleton-management methods I, I, I, I and I
569             can be imported by another class by using the import_singleton() method.
570             That was done for L, to allow it to avoid copying those methods.
571             But other classes with a similar need to minimize module dependencies which already
572             use I can do this too.
573             This helps maintain minimal prerequisites among modules working to set up Perl on containers or new systems.
574              
575             =back
576              
577             =head1 SEE ALSO
578              
579             FreeDesktop.Org's os-release standard: L
580              
581             GitHub repository for Sys::OsRelease: L
582              
583             Related modules:
584              
585             =over 1
586              
587             =item L
588              
589             installs Perl modules, for example as dependencies of a script, via OS packages if available or otherwise via CPAN -
590             uses Sys::OsRelease to determine OS type
591              
592             =item L
593              
594             system information collected from multiple sources including system architecture, hardware, OS release data
595              
596             =back
597              
598             =head1 BUGS AND LIMITATIONS
599              
600             Please report bugs via GitHub at L
601              
602             Patches and enhancements may be submitted via a pull request at L
603              
604             =head1 LICENSE INFORMATION
605              
606             Copyright (c) 2022 by Ian Kluft
607              
608             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.
609              
610             =head1 AUTHOR
611              
612             Ian Kluft
613              
614             =head1 COPYRIGHT AND LICENSE
615              
616             This software is Copyright (c) 2022 by Ian Kluft.
617              
618             This is free software, licensed under:
619              
620             The Artistic License 2.0 (GPL Compatible)
621              
622             =cut
623              
624             __END__