From 72499f349ca81cb2838eced9d7c465b2cf71c4a2 Mon Sep 17 00:00:00 2001 From: R1kaB3rN <100738684+R1kaB3rN@users.noreply.github.com> Date: Sat, 26 Oct 2024 14:42:52 -0700 Subject: [PATCH] feat: support running native Linux executables within SLR (#175) * umu_run: move runtime disable option after winetricks * umu_run: support running native linux executables within runtime - Up until now, we were running every executable through Proton (wine) as if it were a Windows executable. If the user or the client knows that Proton is not necessary and is safe to reliably run, we can skip the overhead of wine. * docs: update docs * umu_test: update tests * umu_run: use UMU_NO_PROTON for slr native usage * docs: update docs * umu_run: deprecate pressure-vessel value * umu_test: update tests * umu_run: skip downloading proton if running native * umu_run: skip proton file check --- docs/umu.1.scd | 6 +++ umu/umu_run.py | 50 ++++++++++++++++++------- umu/umu_test.py | 97 ++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 118 insertions(+), 35 deletions(-) diff --git a/docs/umu.1.scd b/docs/umu.1.scd index 0449b30f9..ea2311a22 100644 --- a/docs/umu.1.scd +++ b/docs/umu.1.scd @@ -152,6 +152,7 @@ _WINEPREFIX_ _UMU_LOG_ Optional. Enables debug logs for the launcher. + Set _1_ to log environment variables, _warning_ for warning messages, or _debug_ to enable all logs. _STORE_ @@ -168,6 +169,11 @@ _UMU_RUNTIME_UPDATE_ Set _0_ to disable updates. +_UMU_NO_PROTON_ + Optional. Runs the executable natively within the Steam Linux Runtime. Intended for native Linux games. + + Set _1_ to run the executable natively within the SLR. + # SEE ALSO _umu_(5), _winetricks_(1), _zenity_(1) diff --git a/umu/umu_run.py b/umu/umu_run.py index f730e97b1..72e479cf1 100755 --- a/umu/umu_run.py +++ b/umu/umu_run.py @@ -166,6 +166,10 @@ def check_env( env["WINEPREFIX"] = os.environ["WINEPREFIX"] + # Skip Proton if running a native Linux executable + if os.environ.get("UMU_NO_PROTON") == "1": + return env + # Proton Version if ( os.environ.get("PROTONPATH") @@ -296,6 +300,7 @@ def set_env( # Runtime env["UMU_NO_RUNTIME"] = os.environ.get("UMU_NO_RUNTIME") or "" env["UMU_RUNTIME_UPDATE"] = os.environ.get("UMU_RUNTIME_UPDATE") or "" + env["UMU_NO_PROTON"] = os.environ.get("UMU_NO_PROTON") or "" return env @@ -341,20 +346,10 @@ def build_command( proton: Path = Path(env["PROTONPATH"], "proton") entry_point: Path = local.joinpath("umu") - # Will run the game w/o Proton, effectively running the game as is. This - # option is intended for debugging purposes, and is otherwise useless - if env.get("UMU_NO_RUNTIME") == "1": - log.warning("Runtime Platform disabled") - return env["EXE"], *opts - - if not proton.is_file(): + if env.get("UMU_NO_PROTON") != "1" and not proton.is_file(): err: str = "The following file was not found in PROTONPATH: proton" raise FileNotFoundError(err) - if env.get("UMU_NO_RUNTIME") == "pressure-vessel": - log.warning("Using Proton without Runtime Platform") - return proton, env["PROTON_VERB"], env["EXE"], *opts - # Exit if the entry point is missing # The _v2-entry-point script and container framework tools are included in # the same image, so this can happen if the image failed to download @@ -365,11 +360,39 @@ def build_command( ) raise FileNotFoundError(err) - # Configure winetricks to not be prompted for any windows + # Winetricks if env.get("EXE", "").endswith("winetricks") and opts: # The position of arguments matter for winetricks # Usage: ./winetricks [options] [command|verb|path-to-verb] ... - opts = ["-q", *opts] + return ( + entry_point, + "--verb", + env["PROTON_VERB"], + "--", + proton, + env["PROTON_VERB"], + env["EXE"], + "-q", + *opts, + ) + + # 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_PROTON") == "1": + return ( + entry_point, + "--verb", + env["PROTON_VERB"], + "--", + env["EXE"], + *opts, + ) + + # Will run the game outside the Steam Runtime w/ Proton + if env.get("UMU_NO_RUNTIME") == "1": + log.warning("Runtime Platform disabled") + return proton, env["PROTON_VERB"], env["EXE"], *opts return ( entry_point, @@ -760,6 +783,7 @@ def main() -> int: # noqa: D103 "UMU_ZENITY": "", "UMU_NO_RUNTIME": "", "UMU_RUNTIME_UPDATE": "", + "UMU_NO_PROTON": "", } opts: list[str] = [] prereq: bool = False diff --git a/umu/umu_test.py b/umu/umu_test.py index 636b662f6..86268687b 100644 --- a/umu/umu_test.py +++ b/umu/umu_test.py @@ -1200,13 +1200,11 @@ def test_game_drive_empty(self): self.env["EXE"], "Expected EXE to be empty on empty string" ) - def test_build_command_noruntime(self): - """Test build_command when disabling the Steam Runtime. + def test_build_command_linux_exe(self): + """Test build_command when running a Linux executable. - UMU_NO_RUNTIME=1 disables the Steam Runtime, which is no different than - running the executable directly. - - Expects the list to contain one string element. + UMU_NO_PROTON=1 disables Proton, running the executable directly in the + Steam Linux Runtime. """ result_args = None test_command = [] @@ -1222,9 +1220,7 @@ def test_build_command_noruntime(self): os.environ["PROTONPATH"] = self.test_file os.environ["GAMEID"] = self.test_file os.environ["STORE"] = self.test_file - # Setting this mocks a Flatpak environment and UMU_NO_RUNTIME is - # only valid for Flatpak apps - os.environ["UMU_NO_RUNTIME"] = "1" + os.environ["UMU_NO_PROTON"] = "1" # Args result_args = umu_run.parse_args() # Config @@ -1233,14 +1229,39 @@ def test_build_command_noruntime(self): umu_run.setup_pfx(self.env["WINEPREFIX"]) # Env umu_run.set_env(self.env, result_args) - # Mock setting UMU_NO_RUNTIME. This will not be set in the function - # because the FLATPAK_PATH constant will evaluate to None - self.env["UMU_NO_RUNTIME"] = os.environ["UMU_NO_RUNTIME"] # Game drive umu_run.enable_steam_game_drive(self.env) os.environ |= self.env + # Mock setting up the runtime + with ( + patch.object(umu_runtime, "_install_umu", return_value=None), + ): + umu_runtime.setup_umu( + self.test_user_share, self.test_local_share, None + ) + copytree( + Path(self.test_user_share, "sniper_platform_0.20240125.75305"), + Path( + self.test_local_share, "sniper_platform_0.20240125.75305" + ), + dirs_exist_ok=True, + symlinks=True, + ) + copy( + Path(self.test_user_share, "run"), + Path(self.test_local_share, "run"), + ) + copy( + Path(self.test_user_share, "run-in-sniper"), + Path(self.test_local_share, "run-in-sniper"), + ) + copy( + Path(self.test_user_share, "umu"), + Path(self.test_local_share, "umu"), + ) + # Build test_command = umu_run.build_command(self.env, self.test_local_share) self.assertIsInstance( @@ -1248,16 +1269,25 @@ def test_build_command_noruntime(self): ) self.assertEqual( len(test_command), - 1, - f"Expected 1 element, received {len(test_command)}", + 5, + f"Expected 5 element, received {len(test_command)}", + ) + + entry_point, opt, verb, sep, exe = [*test_command] + self.assertEqual( + entry_point, + self.test_local_share / "umu", + "Expected an entry point", ) - exe, *_ = [*test_command] + self.assertEqual(opt, "--verb", "Expected --verb") + self.assertEqual(verb, "waitforexitandrun", "Expected PROTON_VERB") + self.assertEqual(sep, "--", "Expected --") self.assertEqual(exe, self.env["EXE"], "Expected the EXE") def test_build_command_nopv(self): """Test build_command when disabling Pressure Vessel. - UMU_NO_RUNTIME=pressure-vessel disables Pressure Vessel, allowing + UMU_NO_RUNTIME=1 disables Pressure Vessel, allowing the launcher to run Proton on the host -- Flatpak environment. Expects the list to contain 3 string elements. @@ -1276,9 +1306,7 @@ def test_build_command_nopv(self): os.environ["PROTONPATH"] = self.test_file os.environ["GAMEID"] = self.test_file os.environ["STORE"] = self.test_file - # Setting this mocks a Flatpak environment and UMU_NO_RUNTIME is - # only valid for Flatpak apps - os.environ["UMU_NO_RUNTIME"] = "pressure-vessel" + os.environ["UMU_NO_RUNTIME"] = "1" # Args result_args = umu_run.parse_args() # Config @@ -1287,12 +1315,37 @@ def test_build_command_nopv(self): umu_run.setup_pfx(self.env["WINEPREFIX"]) # Env umu_run.set_env(self.env, result_args) - # Mock setting UMU_NO_RUNTIME. This will not be set in the function - # because the FLATPAK_PATH constant will evaluate to None - self.env["UMU_NO_RUNTIME"] = os.environ["UMU_NO_RUNTIME"] # Game drive umu_run.enable_steam_game_drive(self.env) + # Mock setting up the runtime + with ( + patch.object(umu_runtime, "_install_umu", return_value=None), + ): + umu_runtime.setup_umu( + self.test_user_share, self.test_local_share, None + ) + copytree( + Path(self.test_user_share, "sniper_platform_0.20240125.75305"), + Path( + self.test_local_share, "sniper_platform_0.20240125.75305" + ), + dirs_exist_ok=True, + symlinks=True, + ) + copy( + Path(self.test_user_share, "run"), + Path(self.test_local_share, "run"), + ) + copy( + Path(self.test_user_share, "run-in-sniper"), + Path(self.test_local_share, "run-in-sniper"), + ) + copy( + Path(self.test_user_share, "umu"), + Path(self.test_local_share, "umu"), + ) + os.environ |= self.env # Build