| # SPDX-License-Identifier: MIT |
| |
| """ |
| This module contains installer support for amd-debug-tools. |
| """ |
| |
| import argparse |
| import os |
| import shutil |
| import subprocess |
| from amd_debug.common import ( |
| print_color, |
| get_distro, |
| read_file, |
| systemd_in_use, |
| show_log_info, |
| fatal_error, |
| relaunch_sudo, |
| AmdTool, |
| ) |
| |
| |
| class Headers: # pylint: disable=too-few-public-methods |
| """Headers for the script""" |
| |
| MissingIasl = "ACPI extraction tool `iasl` is missing" |
| MissingEdidDecode = "EDID decoding tool `edid-decode` is missing" |
| MissingDiEdidDecode = "EDID decoding tool `di-edid-decode` is missing" |
| MissingEthtool = "Ethtool is missing" |
| InstallAction = "Attempting to install" |
| MissingFwupd = "Firmware update library `fwupd` is missing" |
| MissingPyudev = "Udev access library `pyudev` is missing" |
| MissingPackaging = "Python library `packaging` is missing" |
| MissingPandas = "Data library `pandas` is missing" |
| MissingTabulate = "Data library `tabulate` is missing" |
| MissingJinja2 = "Template library `jinja2` is missing" |
| MissingSeaborn = "Data visualization library `seaborn` is missing" |
| UnknownDistro = "No distro installation support available, install manually" |
| |
| |
| class DistroPackage: |
| """Base class for distro packages""" |
| |
| def __init__(self, deb, rpm, arch, message): |
| self.deb = deb |
| self.rpm = rpm |
| self.arch = arch |
| self.message = message |
| |
| def install(self): |
| """Install the package for a given distro""" |
| relaunch_sudo() |
| show_install_message(self.message) |
| dist = get_distro() |
| if dist in ("ubuntu", "debian"): |
| if not self.deb: |
| return False |
| installer = ["apt", "install", self.deb] |
| elif dist == "fedora": |
| if not self.rpm: |
| return False |
| release = read_file("/usr/lib/os-release") |
| variant = None |
| for line in release.split("\n"): |
| if line.startswith("VARIANT_ID"): |
| variant = line.split("=")[-1] |
| if variant != "workstation": |
| return False |
| installer = ["dnf", "install", "-y", self.rpm] |
| elif dist == "arch" or os.path.exists("/etc/arch-release"): |
| if not self.arch: |
| return False |
| installer = ["pacman", "-Sy", self.arch] |
| else: |
| print_color(Headers.UnknownDistro, "👀") |
| return True |
| |
| try: |
| subprocess.check_call(installer) |
| except subprocess.CalledProcessError as e: |
| fatal_error(e) |
| return True |
| |
| |
| class PyUdevPackage(DistroPackage): |
| """Pyudev package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="python3-pyudev", |
| rpm="python3-pyudev", |
| arch="python-pyudev", |
| message=Headers.MissingPyudev, |
| ) |
| |
| |
| class PackagingPackage(DistroPackage): |
| """Packaging package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="python3-packaging", |
| rpm=None, |
| arch="python-packaging", |
| message=Headers.MissingPackaging, |
| ) |
| |
| |
| class PandasPackage(DistroPackage): |
| """Class for handling the pandas package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="python3-pandas", |
| rpm="python3-pandas", |
| arch="python-pandas", |
| message=Headers.MissingPandas, |
| ) |
| |
| |
| class TabulatePackage(DistroPackage): |
| """Class for handling the tabulate package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="python3-tabulate", |
| rpm="python3-tabulate", |
| arch="python-tabulate", |
| message=Headers.MissingTabulate, |
| ) |
| |
| |
| class Jinja2Package(DistroPackage): |
| """Class for handling the jinja2 package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="python3-jinja2", |
| rpm="python3-jinja2", |
| arch="python-jinja", |
| message=Headers.MissingJinja2, |
| ) |
| |
| |
| class SeabornPackage(DistroPackage): |
| """Class for handling the seaborn package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="python3-seaborn", |
| rpm="python3-seaborn", |
| arch="python-seaborn", |
| message=Headers.MissingSeaborn, |
| ) |
| |
| |
| class IaslPackage(DistroPackage): |
| """Iasl package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="acpica-tools", |
| rpm="acpica-tools", |
| arch="acpica", |
| message=Headers.MissingIasl, |
| ) |
| |
| |
| class EthtoolPackage(DistroPackage): |
| """Ethtool package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="ethtool", |
| rpm="ethtool", |
| arch="ethtool", |
| message=Headers.MissingEthtool, |
| ) |
| |
| |
| class EdidDecodePackage(DistroPackage): |
| """Edid-Decode package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="edid-decode", |
| rpm="edid-decode", |
| arch=None, |
| message=Headers.MissingEdidDecode, |
| ) |
| |
| |
| class DisplayInfoPackage(DistroPackage): |
| """display info package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="libdisplay-info-bin", |
| rpm="libdisplay-info-tools", |
| arch="libdisplay-info", |
| message=Headers.MissingDiEdidDecode, |
| ) |
| |
| |
| class FwupdPackage(DistroPackage): |
| """Fwupd package""" |
| |
| def __init__(self): |
| super().__init__( |
| deb="gir1.2-fwupd-2.0", |
| rpm=None, |
| arch=None, |
| message=Headers.MissingFwupd, |
| ) |
| |
| |
| def show_install_message(message): |
| """Show an install message""" |
| action = Headers.InstallAction |
| message = f"{message}. {action}." |
| print_color(message, "👀") |
| |
| |
| class Installer(AmdTool): |
| """Installer class""" |
| |
| def __init__(self, tool_debug): |
| log_prefix = "installer" if tool_debug else None |
| super().__init__(log_prefix) |
| self.systemd = systemd_in_use() |
| self.systemd_path = os.path.join("/", "lib", "systemd", "system-sleep") |
| |
| # test if fwupd can report device firmware versions |
| try: |
| import gi # pylint: disable=import-outside-toplevel |
| from gi.repository import ( # pylint: disable=import-outside-toplevel |
| GLib as _, |
| ) |
| |
| gi.require_version("Fwupd", "2.0") |
| from gi.repository import ( # pylint: disable=import-outside-toplevel |
| Fwupd as _, |
| ) |
| |
| self.fwupd = True |
| except ImportError: |
| self.fwupd = False |
| except ValueError: |
| self.fwupd = False |
| self.requirements = [] |
| |
| def set_requirements(self, *args): |
| """Set the requirements for the installer""" |
| self.requirements = args |
| |
| def install_dependencies(self) -> bool: |
| """Install the dependencies""" |
| if "iasl" in self.requirements: |
| try: |
| iasl = subprocess.call(["iasl", "-v"], stdout=subprocess.DEVNULL) == 0 |
| except FileNotFoundError: |
| iasl = False |
| if not iasl: |
| package = IaslPackage() |
| if not package.install(): |
| return False |
| if "ethtool" in self.requirements: |
| try: |
| ethtool = ( |
| subprocess.call(["ethtool", "-h"], stdout=subprocess.DEVNULL) == 0 |
| ) |
| except FileNotFoundError: |
| ethtool = False |
| if not ethtool: |
| package = EthtoolPackage() |
| if not package.install(): |
| return False |
| # can be satisified by either edid-decode or di-edid-decode |
| if "edid-decode" in self.requirements: |
| try: |
| di_edid = ( |
| subprocess.call( |
| ["di-edid-decode", "--help"], |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.DEVNULL, |
| ) |
| == 255 |
| ) |
| except FileNotFoundError: |
| di_edid = False |
| try: |
| edid = ( |
| subprocess.call( |
| ["edid-decode", "--help"], stdout=subprocess.DEVNULL |
| ) |
| == 255 |
| ) |
| except FileNotFoundError: |
| edid = False |
| if not di_edid and not edid: |
| # try to install di-edid-decode first |
| package = DisplayInfoPackage() |
| if package.install(): |
| return True |
| # fall back to edid-decode instead |
| package = EdidDecodePackage() |
| if not package.install(): |
| return False |
| if "fwupd" in self.requirements and not self.fwupd: |
| package = FwupdPackage() |
| if not package.install(): |
| return False |
| if "pyudev" in self.requirements: |
| try: |
| import pyudev as _ # pylint: disable=import-outside-toplevel |
| except ModuleNotFoundError: |
| package = PyUdevPackage() |
| if not package.install(): |
| return False |
| if "packaging" in self.requirements: |
| try: |
| import packaging as _ # pylint: disable=import-outside-toplevel |
| except ModuleNotFoundError: |
| package = PackagingPackage() |
| if not package.install(): |
| return False |
| if "pandas" in self.requirements: |
| try: |
| import pandas as _ # pylint: disable=import-outside-toplevel |
| except ModuleNotFoundError: |
| package = PandasPackage() |
| if not package.install(): |
| return False |
| if "tabulate" in self.requirements: |
| try: |
| import tabulate as _ # pylint: disable=import-outside-toplevel |
| except ModuleNotFoundError: |
| package = TabulatePackage() |
| if not package.install(): |
| return False |
| if "jinja2" in self.requirements: |
| try: |
| import jinja2 as _ # pylint: disable=import-outside-toplevel |
| except ModuleNotFoundError: |
| package = Jinja2Package() |
| if not package.install(): |
| return False |
| if "seaborn" in self.requirements: |
| try: |
| import seaborn as _ # pylint: disable=import-outside-toplevel |
| except ModuleNotFoundError: |
| package = SeabornPackage() |
| if not package.install(): |
| return False |
| |
| return True |
| |
| def _check_systemd(self) -> bool: |
| """Check if the systemd path exists""" |
| if not os.path.exists(self.systemd_path): |
| print_color( |
| f"Systemd path does not exist: {self.systemd_path}", |
| "❌", |
| ) |
| return os.path.exists(self.systemd_path) |
| |
| def remove(self) -> bool: |
| """Remove the amd-s2idle hook""" |
| if self._check_systemd(): |
| f = "s2idle-hook" |
| t = os.path.join(self.systemd_path, f) |
| os.remove(t) |
| print_color( |
| f"Removed {f} from {self.systemd_path}", |
| "✅", |
| ) |
| else: |
| print_color("Systemd path does not exist, not removing hook", "🚦") |
| f = "amd-s2idle" |
| d = os.path.join( |
| "/", |
| "usr", |
| "local", |
| "share", |
| "bash-completion", |
| "completions", |
| ) |
| t = os.path.join(d, f) |
| if os.path.exists(t): |
| os.remove(t) |
| print_color(f"Removed {f} from {d}", "✅") |
| return True |
| |
| def install(self) -> bool: |
| """Install the amd-s2idle hook""" |
| import amd_debug # pylint: disable=import-outside-toplevel |
| |
| d = os.path.dirname(amd_debug.__file__) |
| if self._check_systemd(): |
| f = "s2idle-hook" |
| s = os.path.join(d, f) |
| t = os.path.join(self.systemd_path, f) |
| with open(s, "r", encoding="utf-8") as r: |
| with open(t, "w", encoding="utf-8") as w: |
| for line in r.readlines(): |
| if 'parser.add_argument("--path"' in line: |
| line = line.replace( |
| 'default=""', |
| f"default=\"{os.path.join(d, '..')}\"", |
| ) |
| w.write(line) |
| os.chmod(t, 0o755) |
| print_color( |
| f"Installed {f} to {self.systemd_path}", |
| "✅", |
| ) |
| else: |
| print_color("Systemd path does not exist, not installing hook", "🚦") |
| f = "amd-s2idle" |
| s = os.path.join(d, "bash", f) |
| t = os.path.join( |
| "/", |
| "usr", |
| "local", |
| "share", |
| "bash-completion", |
| "completions", |
| ) |
| os.makedirs(t, exist_ok=True) |
| shutil.copy(s, t) |
| print_color(f"Installed {f} to {t}", "✅") |
| return True |
| |
| |
| def parse_args(): |
| """Parse command line arguments""" |
| parser = argparse.ArgumentParser( |
| description="Install dependencies for AMD debug tools", |
| ) |
| parser.add_argument( |
| "--tool-debug", |
| action="store_true", |
| help="Enable tool debug logging", |
| ) |
| return parser.parse_args() |
| |
| |
| def install_dep_superset() -> None | int: |
| """Install all python superset dependencies""" |
| args = parse_args() |
| tool = Installer(tool_debug=args.tool_debug) |
| tool.set_requirements( |
| "iasl", |
| "ethtool", |
| "jinja2", |
| "pyudev", |
| "packaging", |
| "pandas", |
| "seaborn", |
| "tabulate", |
| "edid-decode", |
| ) |
| ret = tool.install_dependencies() |
| if ret: |
| print_color("All dependencies installed", "✅") |
| show_log_info() |
| if ret is False: |
| return 1 |
| return |