From ff66560716660d2d4883594cf4fd20097aa40de4 Mon Sep 17 00:00:00 2001 From: Stelios Tsampas Date: Tue, 7 Jan 2025 19:51:33 +0200 Subject: [PATCH] umu: use runtimes as compatibility tools --- umu/umu_consts.py | 34 ++++++++++++++++++++------ umu/umu_run.py | 23 +++++++++++------- umu/umu_util.py | 61 ++++++++++++++++++++++++++++++----------------- 3 files changed, 80 insertions(+), 38 deletions(-) diff --git a/umu/umu_consts.py b/umu/umu_consts.py index 04b9f474a..d5d1bfb5d 100644 --- a/umu/umu_consts.py +++ b/umu/umu_consts.py @@ -1,4 +1,5 @@ import os +from dataclasses import dataclass from enum import Enum from pathlib import Path @@ -53,13 +54,6 @@ class GamescopeAtom(Enum): "getnativepath", } -RUNTIME_VERSIONS = { - # "1070560": ("scout", "steamrt1"), - "1391110": ("soldier", "steamrt2"), - "1628350": ("sniper", "steamrt3"), - # "": ("medic", "steamrt4"), -} - XDG_CACHE_HOME: Path = ( Path(os.environ["XDG_CACHE_HOME"]) if os.environ.get("XDG_CACHE_HOME") @@ -100,3 +94,29 @@ class GamescopeAtom(Enum): # Constant defined in prctl.h # See prctl(2) for more details PR_SET_CHILD_SUBREAPER = 36 + + +@dataclass +class UmuRuntime: + """Holds information about a runtime.""" + + name: str + version: str + path: Path | None = None + + def __post_init__(self) -> None: # noqa: D105 + if self.version == "native": + return + if self.path is None: + self.path = UMU_LOCAL.joinpath(self.name) + + +RUNTIME_VERSIONS = { + "host": UmuRuntime("host", "native" ), + "1070560": UmuRuntime("scout", "steamrt1"), + "1391110": UmuRuntime("soldier", "steamrt2"), + "1628350": UmuRuntime("sniper", "steamrt3"), + # "" : UmuRuntime("medic", "steamrt4"), +} + +RUNTIME_NAMES = {RUNTIME_VERSIONS[key].name: key for key in RUNTIME_VERSIONS} diff --git a/umu/umu_run.py b/umu/umu_run.py index 953fcb7ee..d7f9d7433 100755 --- a/umu/umu_run.py +++ b/umu/umu_run.py @@ -39,6 +39,8 @@ from umu.umu_consts import ( PR_SET_CHILD_SUBREAPER, PROTON_VERBS, + RUNTIME_NAMES, + RUNTIME_VERSIONS, STEAM_COMPAT, STEAM_WINDOW_ID, UMU_LOCAL, @@ -150,6 +152,11 @@ def check_env( os.environ["PROTONPATH"] = "" get_umu_proton(env, session_pools) + if (key := os.environ.get("PROTONPATH")) in RUNTIME_NAMES: + os.environ["PROTONPATH"] = str( + RUNTIME_VERSIONS[RUNTIME_NAMES[key]].path + ) + env["PROTONPATH"] = os.environ["PROTONPATH"] # If download fails/doesn't exist in the system, raise an error @@ -318,11 +325,11 @@ def build_command( ) raise FileNotFoundError(err) - runtime = SteamRuntime(local.as_posix()) - # Will run the game within the Steam Runtime w/o Proton - # Ideally, for reliability, executables should be compiled within - # the Steam Runtime if env.get("UMU_NO_TOOL") == "1": + runtime = CompatibilityTool(RUNTIME_VERSIONS[RUNTIME_NAMES["sniper"]].path) + # Will run the game within the Steam Runtime w/o Proton + # Ideally, for reliability, executables should be compiled within + # the Steam Runtime log.debug( "Compatibility tool disabled. Executing linux-native executable %s", env["EXE"] ) @@ -335,11 +342,9 @@ def build_command( # Setup compatibility tool # If the user explicitly requested to run without the runtime, # force runtime to None - compat_tool = CompatibilityTool( - env["PROTONPATH"], - shim, - None if env["UMU_NO_RUNTIME"] == "1" else runtime - ) + compat_tool = CompatibilityTool(env["PROTONPATH"], shim) + if env["UMU_NO_RUNTIME"] == "1": + compat_tool.runtime = None log.info("Using compatibility tool %s", compat_tool.display_name) # Will run the game outside the Steam Runtime w/ Proton if not compat_tool.runtime_enabled: diff --git a/umu/umu_util.py b/umu/umu_util.py index e21a432c8..20eb764ea 100644 --- a/umu/umu_util.py +++ b/umu/umu_util.py @@ -20,7 +20,7 @@ from urllib3.response import BaseHTTPResponse from Xlib import display -from umu.umu_consts import RUNTIME_VERSIONS, UMU_LOCAL +from umu.umu_consts import RUNTIME_VERSIONS, UMU_LOCAL, UmuRuntime from umu.umu_log import log @@ -350,10 +350,10 @@ def required_tool_appid(self) -> str | None: # noqa: D102 return str(ret) if (ret := self.tool_manifest.get("require_tool_appid")) else None @property - def required_tool_name(self) -> tuple: - """Map the required tool's appid to a tuple of commonly used names.""" + def required_runtime(self) -> UmuRuntime: + """Map the required tool's appid to a runtime known by umu.""" if self.required_tool_appid is None: - return None, None + return RUNTIME_VERSIONS["host"] return RUNTIME_VERSIONS[self.required_tool_appid] @property @@ -364,8 +364,7 @@ def command(self, verb: str) -> list[str]: """Return the tool specific entry point.""" tool_path = os.path.normpath(self.tool_path) cmd = "".join([shlex.quote(tool_path), self.tool_manifest["commandline"]]) - cmd = cmd.replace("_v2-entry-point", "umu") - cmd = cmd.replace("%verb%", str(verb)) + cmd = cmd.replace("%verb%", verb) return shlex.split(cmd) def as_str(self, verb: str): # noqa: D102 @@ -377,24 +376,20 @@ class SteamRuntime(SteamBase): def __init__(self, path: str) -> None: # noqa: D107 super().__init__(path) - - -class CompatibilityTool(SteamBase): - """A compatibility tool (Proton, luxtorpeda, etc).""" - - def __init__(self, tool_path: str, shim: Path, runtime: SteamRuntime | None) -> None: # noqa: D107 - super().__init__(tool_path) - _tool_path = Path(tool_path) - self.shim = shim - self.runtime = runtime if self.required_tool_appid is not None else None - if _tool_path.joinpath("compatibilitytool.vdf").exists(): - with _tool_path.joinpath("compatibilitytool.vdf").open(encoding="utf-8") as f: + self.runtime = ( + SteamRuntime(self.required_runtime.path.as_posix()) + if self.required_tool_appid is not None + else None + ) + _path = Path(path) + if _path.joinpath("compatibilitytool.vdf").exists(): + with _path.joinpath("compatibilitytool.vdf").open(encoding="utf-8") as f: # There can be multiple tools definitions in `compatibilitytools.vdf` # Take the first one and hope it is the one with the correct display_name compat_tools = tuple(vdf.load(f)["compatibilitytools"]["compat_tools"].values()) self.compatibility_tool = compat_tools[0] else: - self.compatibility_tool = {"display_name": _tool_path.name} + self.compatibility_tool = {"display_name": _path.name} @property def display_name(self) -> str | None: # noqa: D102 @@ -406,12 +401,34 @@ def runtime_enabled(self) -> bool: return self.runtime is not None def command(self, verb: str) -> list[str]: - """Return the fully qualified command for the tool . + """Return the fully qualified command for the runtime. - If the tool uses a runtime, its entry point is prepended to the tool's command. + If the runtime uses another runtime, its entry point is prepended to the local command. """ + log.info("Running '%s' using runtime '%s'", self.display_name, self.required_runtime.name) cmd = self.runtime.command(verb) if self.runtime is not None else [] - cmd.append(self.shim.as_posix()) cmd.extend(super().command(verb)) return cmd + +class CompatibilityTool(SteamRuntime): + """A compatibility tool (Proton, luxtorpeda, etc).""" + + def __init__(self, path: str, shim: Path) -> None: # noqa: D107 + super().__init__(path) + self.shim = shim + + def command(self, verb: str) -> list[str]: + """Return the fully qualified command for the tool. + + If the tool uses a runtime, its entry point is prepended to the tool's command. + """ + log.info("Running '%s' using runtime '%s'", self.display_name, self.required_runtime.name) + cmd = self.runtime.command(verb) if self.runtime is not None else [] + target = super(SteamRuntime, self).command(verb) + if self.layer in {"container-runtime", "scout-in-container"}: + cmd.extend([*target, self.shim.as_posix()]) + else: + cmd.extend([self.shim.as_posix(), *target]) + return cmd +