Source code for rospkg.os_detect

# Copyright (c) 2009, Willow Garage, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the Willow Garage, Inc. nor the names of its
#       contributors may be used to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# Author Tully Foote/tfoote@willowgarage.com, Ken Conley/kwc@willowgarage.com

"""
Library for detecting the current OS, including detecting specific
Linux distributions.
"""

from __future__ import absolute_import
from __future__ import print_function

import codecs
# to be removed after Ubuntu Xenial is out of support
import sys
if sys.version_info >= (3, 8):
    import distro
else:
    import platform as distro
import locale
import os
import platform
import subprocess


def _read_stdout(cmd):
    try:
        pop = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (std_out, std_err) = pop.communicate()
        # Python 2.6 compatibility
        if isinstance(std_out, str):
            return std_out.strip()
        return std_out.decode(encoding='UTF-8').strip()
    except:
        return None


[docs]def uname_get_machine(): """ Linux: wrapper around uname to determine if OS is 64-bit """ return _read_stdout(['uname', '-m'])
def read_issue(filename="/etc/issue"): """ :returns: list of strings in issue file, or None if issue file cannot be read/split """ if os.path.exists(filename): with codecs.open(filename, 'r', encoding=locale.getpreferredencoding()) as f: return f.read().split() return None def read_os_release(filename=None): """ :returns: Dictionary of key value pairs from /etc/os-release or fallback to /usr/lib/os-release, with quotes stripped from values """ if filename is None: filename = '/etc/os-release' if not os.path.exists(filename): filename = '/usr/lib/os-release' if not os.path.exists(filename): return None release_info = {} with codecs.open(filename, 'r', encoding=locale.getpreferredencoding()) as f: for line in f: key, val = line.rstrip('\n').partition('=')[::2] release_info[key] = val.strip('"') return release_info
[docs]class OsNotDetected(Exception): """ Exception to indicate failure to detect operating system. """ pass
[docs]class OsDetector(object): """ Generic API for detecting a specific OS. """
[docs] def is_os(self): """ :returns: if the specific OS which this class is designed to detect is present. Only one version of this class should return for any version. """ raise NotImplementedError("is_os unimplemented")
[docs] def get_version(self): """ :returns: standardized version for this OS. (aka Ubuntu Hardy Heron = "8.04") :raises: :exc:`OsNotDetected` if called on incorrect OS. """ raise NotImplementedError("get_version unimplemented")
[docs] def get_codename(self): """ :returns: codename for this OS. (aka Ubuntu Hardy Heron = "hardy"). If codenames are not available for this OS, return empty string. :raises: :exc:`OsNotDetected` if called on incorrect OS. """ raise NotImplementedError("get_codename unimplemented")
class LsbDetect(OsDetector): """ Generic detector for Debian, Ubuntu, Mint, and Pop! OS """ def __init__(self, lsb_name, get_version_fn=None): self.lsb_name = lsb_name if distro.__name__ == "distro": self.lsb_info = (distro.id(), distro.version(), distro.codename()) elif hasattr(distro, "linux_distribution"): self.lsb_info = distro.linux_distribution(full_distribution_name=0) elif hasattr(distro, "dist"): self.lsb_info = distro.dist() else: self.lsb_info = None def is_os(self): if self.lsb_info is None: return False # Work around platform returning 'Ubuntu' and distro returning 'ubuntu' return self.lsb_info[0].lower() == self.lsb_name.lower() def get_version(self): if self.is_os(): return self.lsb_info[1] raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return self.lsb_info[2] raise OsNotDetected('called in incorrect OS') class Debian(LsbDetect): def __init__(self, get_version_fn=None): super(Debian, self).__init__('debian', get_version_fn) def get_codename(self): if self.is_os(): v = self.get_version().split('.', 1)[0] return { '7': 'wheezy', '8': 'jessie', '9': 'stretch', '10': 'buster', '11': 'bullseye', '12': 'bookworm', '13': 'trixie', '14': 'forky', 'unstable': 'sid', 'rodete': 'trixie', }.get(v, '') class FdoDetect(OsDetector): """ Generic detector for operating systems implementing /etc/os-release, as defined by the os-release spec hosted at Freedesktop.org (Fdo): http://www.freedesktop.org/software/systemd/man/os-release.html Requires that the "ID", and "VERSION_ID" keys are set in the os-release file. Codename is parsed from the VERSION key if available: either using the format "foo, CODENAME" or "foo (CODENAME)." If the VERSION key is not present, the VERSION_ID is value is used as the codename. """ def __init__(self, fdo_id): release_info = read_os_release() if release_info is not None and "ID" in release_info and release_info["ID"] == fdo_id: self.release_info = release_info else: self.release_info = None def is_os(self): return self.release_info is not None and "VERSION_ID" in self.release_info def get_version(self): if self.is_os(): return self.release_info["VERSION_ID"] raise OsNotDetected("called in incorrect OS") def get_codename(self): if self.is_os(): if "VERSION" in self.release_info: version = self.release_info["VERSION"] # FDO style: works with Fedora, Debian, Suse. if '(' in version: codename = version[version.find("(") + 1:version.find(")")] # Ubuntu style elif '"' in version: codename = version[version.find(",") + 1:].lstrip(' ').split()[0] # Indeterminate style else: codename = version return codename.lower() else: return self.get_version() raise OsNotDetected("called in incorrect OS") class OpenEmbedded(OsDetector): """ Detect OpenEmbedded. """ def is_os(self): return "ROS_OS_OVERRIDE" in os.environ and os.environ["ROS_OS_OVERRIDE"] == "openembedded" def get_version(self): if self.is_os(): return "" raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return "" raise OsNotDetected('called in incorrect OS') class Conda(OsDetector): """ Detect Conda. """ def is_os(self): return "ROS_OS_OVERRIDE" in os.environ and \ (os.environ["ROS_OS_OVERRIDE"].lower().startswith("robostack") or os.environ["ROS_OS_OVERRIDE"].lower().startswith("conda")) def get_version(self): if self.is_os(): return "" raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return "" raise OsNotDetected('called in incorrect OS') class OpenSuse(OsDetector): """ Detect OpenSuse OS. """ def __init__(self, brand_file="/etc/SuSE-brand", release_file="/etc/SuSE-release"): self._brand_file = brand_file self._release_file = release_file def is_os(self): os_list = read_issue(self._brand_file) return os_list and os_list[0] == "openSUSE" def get_version(self): if self.is_os() and os.path.exists(self._brand_file): with open(self._brand_file, 'r') as fh: os_list = fh.read().strip().split('\n') if len(os_list) == 2: os_list = os_list[1].split(' = ') if os_list[0] == "VERSION": return os_list[1] raise OsNotDetected('cannot get version on this OS') def get_codename(self): # /etc/SuSE-release is deprecated since 13.1 if self._release_file is None: return "" if self.is_os() and os.path.exists(self._release_file): with open(self._release_file, 'r') as fh: os_list = fh.read().strip().split('\n') for line in os_list: kv = line.split(' = ') if kv[0] == "CODENAME": return kv[1] raise OsNotDetected('called in incorrect OS') # Source: https://en.wikipedia.org/wiki/MacOS#Versions _osx_codename_map = { '10.4': 'tiger', '10.5': 'leopard', '10.6': 'snow', '10.7': 'lion', '10.8': 'mountain lion', '10.9': 'mavericks', '10.10': 'yosemite', '10.11': 'el capitan', '10.12': 'sierra', '10.13': 'high sierra', '10.14': 'mojave', '10.15': 'catalina', '11': 'big sur', '12': 'monterey', '13': 'ventura', '14': 'sonoma', } def _osx_codename(major, minor): if major == 10: key = '%s.%s' % (major, minor) else: key = '%s' % (major) if key not in _osx_codename_map: raise OsNotDetected("unrecognized version: %s" % key) return _osx_codename_map[key] class OSX(OsDetector): """ Detect OS X """ def __init__(self, sw_vers_file="/usr/bin/sw_vers"): self._sw_vers_file = sw_vers_file def is_os(self): return os.path.exists(self._sw_vers_file) def get_codename(self): if self.is_os(): version = self.get_version() import distutils.version # To parse version numbers try: ver = distutils.version.StrictVersion(version).version except ValueError: raise OsNotDetected("invalid version string: %s" % (version)) return _osx_codename(*ver[0:2]) raise OsNotDetected('called in incorrect OS') def get_version(self): if self.is_os(): return _read_stdout([self._sw_vers_file, '-productVersion']) raise OsNotDetected('called in incorrect OS') class QNX(OsDetector): ''' Detect QNX realtime OS. @author: Isaac Saito ''' def __init__(self, uname_file='/bin/uname'): ''' @param uname_file: An executable that can be used for detecting OS name and version. ''' self._os_name_qnx = 'QNX' self._uname_file = uname_file def is_os(self): if os.path.exists(self._uname_file): std_out = _read_stdout([self._uname_file]) return std_out.strip() == self._os_name_qnx else: return False def get_codename(self): if self.is_os(): return '' raise OsNotDetected('called in incorrect OS') def get_version(self): if self.is_os() and os.path.exists(self._uname_file): return _read_stdout([self._uname_file, "-r"]) raise OsNotDetected('called in incorrect OS') class Arch(OsDetector): """ Detect Arch Linux. """ def __init__(self, release_file='/etc/arch-release'): self._release_file = release_file def is_os(self): return os.path.exists(self._release_file) def get_version(self): if self.is_os(): return "" raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return "" raise OsNotDetected('called in incorrect OS') class Manjaro(Arch): """ Detect Manjaro. """ def __init__(self, release_file='/etc/manjaro-release'): super(Manjaro, self).__init__(release_file) class Cygwin(OsDetector): """ Detect Cygwin presence on Windows OS. """ def is_os(self): return os.path.exists("/usr/bin/cygwin1.dll") def get_version(self): if self.is_os(): return _read_stdout(['uname', '-r']) raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return '' raise OsNotDetected('called in incorrect OS') class Gentoo(OsDetector): """ Detect Gentoo OS. """ def __init__(self, release_file="/etc/gentoo-release"): self._release_file = release_file def is_os(self): os_list = read_issue(self._release_file) return os_list and os_list[0] == "Gentoo" and os_list[1] == "Base" def get_version(self): if self.is_os(): os_list = read_issue(self._release_file) return os_list[4] raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return '' raise OsNotDetected('called in incorrect OS') class Funtoo(Gentoo): """ Detect Funtoo OS, a Gentoo Variant. """ def __init__(self, release_file="/etc/gentoo-release"): Gentoo.__init__(self, release_file) def is_os(self): os_list = read_issue(self._release_file) return os_list and os_list[0] == "Funtoo" and os_list[1] == "Linux" class FreeBSD(OsDetector): """ Detect FreeBSD OS. """ def __init__(self, uname_file="/usr/bin/uname"): self._uname_file = uname_file def is_os(self): if os.path.exists(self._uname_file): std_out = _read_stdout([self._uname_file]) return std_out.strip() == "FreeBSD" else: return False def get_version(self): if self.is_os() and os.path.exists(self._uname_file): return _read_stdout([self._uname_file, "-r"]) raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return '' raise OsNotDetected('called in incorrect OS') class Slackware(OsDetector): """ Detect SlackWare Linux. """ def __init__(self, release_file='/etc/slackware-version'): self._release_file = release_file def is_os(self): return os.path.exists(self._release_file) def get_version(self): if self.is_os(): os_list = read_issue(self._release_file) return os_list[1] raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return '' raise OsNotDetected('called in incorrect OS') class Windows(OsDetector): """ Detect Windows OS. """ def is_os(self): return platform.system() == "Windows" def get_version(self): if self.is_os(): return platform.version() raise OsNotDetected('called in incorrect OS') def get_codename(self): if self.is_os(): return platform.release() raise OsNotDetected('called in incorrect OS')
[docs]class OsDetect: """ This class will iterate over registered classes to lookup the active OS and version """ default_os_list = [] def __init__(self, os_list=None): if os_list is None: os_list = OsDetect.default_os_list self._os_list = os_list self._os_name = None self._os_version = None self._os_codename = None self._os_detector = None self._override = False
[docs] @staticmethod def register_default(os_name, os_detector): """ Register detector to be used with all future instances of :class:`OsDetect`. The new detector will have precedence over any previously registered detectors associated with *os_name*. :param os_name: OS key associated with OS detector :param os_detector: :class:`OsDetector` instance """ OsDetect.default_os_list.insert(0, (os_name, os_detector))
[docs] def detect_os(self, env=None): """ Detect operating system. Return value can be overridden by the :env:`ROS_OS_OVERRIDE` environment variable. :param env: override ``os.environ`` :returns: (os_name, os_version, os_codename), ``(str, str, str)`` :raises: :exc:`OsNotDetected` if OS could not be detected """ if env is None: env = os.environ if 'ROS_OS_OVERRIDE' in env: splits = env["ROS_OS_OVERRIDE"].split(':') self._os_name = splits[0] if len(splits) > 1: self._os_version = splits[1] if len(splits) > 2: self._os_codename = splits[2] else: self._os_codename = '' else: self._os_version = self._os_codename = '' self._override = True else: for os_name, os_detector in self._os_list: if os_detector.is_os(): self._os_name = os_name self._os_version = os_detector.get_version() self._os_codename = os_detector.get_codename() self._os_detector = os_detector break if self._os_name: return self._os_name, self._os_version, self._os_codename else: # No solution found attempted = [x[0] for x in self._os_list] raise OsNotDetected("Could not detect OS, tried %s" % attempted)
[docs] def get_detector(self, name=None): """ Get detector used for specified OS name, or the detector for this OS if name is ``None``. :raises: :exc:`KeyError` """ if name is None: if not self._os_detector: self.detect_os() return self._os_detector else: try: return [d for d_name, d in self._os_list if d_name == name][0] except IndexError: raise KeyError(name)
[docs] def add_detector(self, name, detector): """ Add detector to list of detectors used by this instance. *detector* will override any previous detectors associated with *name*. :param name: OS name that detector matches :param detector: :class:`OsDetector` instance """ self._os_list.insert(0, (name, detector))
[docs] def get_name(self): if not self._os_name: self.detect_os() return self._os_name
[docs] def get_version(self): if not self._os_version: self.detect_os() return self._os_version
[docs] def get_codename(self): if not self._os_codename: self.detect_os() return self._os_codename
OS_ALMALINUX = 'almalinux' OS_ALPINE = 'alpine' OS_AMAZON = 'amazon' OS_ARCH = 'arch' OS_BUILDROOT = 'buildroot' OS_MANJARO = 'manjaro' OS_CENTOS = 'centos' OS_EULEROS = 'euleros' OS_CYGWIN = 'cygwin' OS_DEBIAN = 'debian' OS_ELEMENTARY = 'elementary' OS_ELEMENTARY_OLD = 'elementary' OS_FEDORA = 'fedora' OS_FEDORA_ASAHI = 'fedora-asahi' OS_FREEBSD = 'freebsd' OS_FUNTOO = 'funtoo' OS_GENTOO = 'gentoo' OS_LINARO = 'linaro' OS_MINT = 'mint' OS_MX = 'mx' OS_NEON = 'neon' OS_OPENEMBEDDED = 'openembedded' OS_OPENSUSE = 'opensuse' OS_OPENSUSE13 = 'opensuse' OS_ORACLE = 'oracle' OS_CONDA = 'conda' OS_TIZEN = 'tizen' OS_SAILFISHOS = 'sailfishos' OS_OSX = 'osx' OS_POP = 'pop' OS_QNX = 'qnx' OS_RASPBIAN = 'raspbian' OS_RHEL = 'rhel' OS_ROCKY = 'rocky' OS_SLACKWARE = 'slackware' OS_UBUNTU = 'ubuntu' OS_CLEARLINUX = 'clearlinux' OS_NIXOS = 'nixos' OS_WINDOWS = 'windows' OS_ZORIN = 'zorin' OsDetect.register_default(OS_ALMALINUX, FdoDetect("almalinux")) OsDetect.register_default(OS_ALPINE, FdoDetect("alpine")) OsDetect.register_default(OS_AMAZON, FdoDetect("amzn")) OsDetect.register_default(OS_ARCH, Arch()) OsDetect.register_default(OS_BUILDROOT, FdoDetect("buildroot")) OsDetect.register_default(OS_MANJARO, Manjaro()) OsDetect.register_default(OS_CENTOS, FdoDetect("centos")) OsDetect.register_default(OS_EULEROS, FdoDetect("euleros")) OsDetect.register_default(OS_CYGWIN, Cygwin()) OsDetect.register_default(OS_DEBIAN, Debian()) OsDetect.register_default(OS_ELEMENTARY, LsbDetect("elementary")) OsDetect.register_default(OS_ELEMENTARY_OLD, LsbDetect("elementary OS")) OsDetect.register_default(OS_FEDORA, FdoDetect("fedora")) OsDetect.register_default(OS_FEDORA_ASAHI, FdoDetect("fedora-asahi-remix")) OsDetect.register_default(OS_FREEBSD, FreeBSD()) OsDetect.register_default(OS_FUNTOO, Funtoo()) OsDetect.register_default(OS_GENTOO, Gentoo()) OsDetect.register_default(OS_LINARO, LsbDetect("Linaro")) OsDetect.register_default(OS_MINT, LsbDetect("LinuxMint")) OsDetect.register_default(OS_MX, LsbDetect("MX")) OsDetect.register_default(OS_NEON, LsbDetect("neon")) OsDetect.register_default(OS_OPENEMBEDDED, OpenEmbedded()) OsDetect.register_default(OS_OPENSUSE, OpenSuse()) OsDetect.register_default(OS_OPENSUSE13, OpenSuse(brand_file='/etc/SUSE-brand', release_file=None)) OsDetect.register_default(OS_OPENSUSE, FdoDetect("opensuse-tumbleweed")) OsDetect.register_default(OS_OPENSUSE, FdoDetect("opensuse-leap")) OsDetect.register_default(OS_OPENSUSE, FdoDetect("opensuse")) OsDetect.register_default(OS_ORACLE, FdoDetect("ol")) OsDetect.register_default(OS_CONDA, Conda()) OsDetect.register_default(OS_TIZEN, FdoDetect("tizen")) OsDetect.register_default(OS_SAILFISHOS, FdoDetect("sailfishos")) OsDetect.register_default(OS_OSX, OSX()) OsDetect.register_default(OS_POP, LsbDetect("Pop")) OsDetect.register_default(OS_QNX, QNX()) OsDetect.register_default(OS_RASPBIAN, FdoDetect("raspbian")) OsDetect.register_default(OS_RHEL, FdoDetect("rhel")) OsDetect.register_default(OS_ROCKY, FdoDetect("rocky")) OsDetect.register_default(OS_SLACKWARE, Slackware()) OsDetect.register_default(OS_UBUNTU, LsbDetect("Ubuntu")) OsDetect.register_default(OS_CLEARLINUX, FdoDetect("clear-linux-os")) OsDetect.register_default(OS_NIXOS, FdoDetect("nixos")) OsDetect.register_default(OS_WINDOWS, Windows()) OsDetect.register_default(OS_ZORIN, LsbDetect("Zorin")) if __name__ == '__main__': detect = OsDetect() print("OS Name: %s" % detect.get_name()) print("OS Version: %s" % detect.get_version()) print("OS Codename: %s" % detect.get_codename())