"""Identify the host OS without losing the launcher's execution runtime.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path import re EXECUTION_OS_RELEASE = Path("/etc/os-release") CONTAINER_HOST_OS_RELEASE = Path("/run/host/os-release") STEAM_RUNTIME_OS_ID = "steamrt" @dataclass(frozen=True) class SystemIdentity: """OS releases visible to the launcher process and its container host.""" execution_release: dict[str, str] host_release: dict[str, str] | None @property def operating_system_release(self) -> dict[str, str]: return self.host_release or self.execution_release @property def running_inside_steam_runtime(self) -> bool: return ( self.execution_release.get("ID", "").strip().casefold() == STEAM_RUNTIME_OS_ID ) def detect_system_identity( *, execution_os_release: Path = EXECUTION_OS_RELEASE, host_os_release: Path = CONTAINER_HOST_OS_RELEASE, ) -> SystemIdentity: """Read the runtime OS and the standard container-host identity, if present.""" execution = read_os_release(execution_os_release) host: dict[str, str] | None = None if execution.get("ID", "").strip().casefold() == STEAM_RUNTIME_OS_ID: candidate = read_os_release(host_os_release) candidate_id = candidate.get("ID", "").strip().casefold() if candidate_id and candidate_id != STEAM_RUNTIME_OS_ID: host = candidate return SystemIdentity(execution_release=execution, host_release=host) def detect_distribution_id() -> str: """Return the host distribution ID when a container exposes it.""" release = detect_system_identity().operating_system_release return release.get("ID", "").strip().casefold() def is_running_inside_steam_runtime() -> bool: """Return whether the launcher process itself is inside Steam Runtime.""" return detect_system_identity().running_inside_steam_runtime def read_os_release(path: Path) -> dict[str, str]: """Read bounded freedesktop os-release fields without invoking host tools.""" try: lines = path.read_text(encoding="utf-8", errors="replace").splitlines() except OSError: return {} result: dict[str, str] = {} for line in lines: if not line or line.startswith("#") or "=" not in line: continue key, raw = line.split("=", 1) if not re.fullmatch(r"[A-Z][A-Z0-9_]*", key): continue value = raw.strip() if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: value = value[1:-1] value = value.replace(r"\n", " ").replace(r"\"", '"').replace(r"\\", "\\") result[key] = value[:255] return result