From ca897f24c2d14e1d89fb22aacf6e96802a4d65c3 Mon Sep 17 00:00:00 2001 From: R1kaB3rN <100738684+R1kaB3rN@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:16:48 -0700 Subject: [PATCH] Update launcher and packaging (#95) * umu_consts: update logic * umu_plugins: delete enable_flatpak function - For now, execute the launcher within the Flatpak sandbox. Breaking out the sandbox via flatpak-spawn is discouraged and breaks Flatpak's security model. It may also require other Flatpak applications to make additional changes in their manifest, which they may not be supportive to do for this practice. Unless necessary or proven to be very problematic, execute within the Flatpak sandbox * umu_util: only copy umu_version.json and reaper - To run games, the only required files to be in the user's home directory is the runtime platform and the reaper executable. The launcher files will remain in their system path that's been configured in build time. * Update Makefile to install umu-launcher in system path - The directory /usr/share/steam/compatibilitytool.d is an official system path supported by Valve's Steam client to search for community-based tools and to use them as compatibility tools to run games. Instead of copying the umu-launcher directory to ~/.local/share/Steam/compatibilitytool.d we can install it in its system path. That way, there's less to copy/remove and the files there can be managed by the distribution's package manager * umu_util: don't pass compatibilitytools.d path - The system path compatibilitytools.d directory will be used instead * umu_consts: delete enum * umu_run: update debug message * flatpak: limit access to ~/.local/share/Steam - The launcher only needs this directory to write Proton. For now, just limit access to this directory instead of exposing user's entire ~/.local/share * umu-launcher: hide umu-launcher compatibility tool - For now, hide this tool in the Steam client until the umu runtime platform is released. - Related to https://github.com/Open-Wine-Components/umu-launcher/issues/4 * Update tests * umu_plugins: remove enable_reaper * umu_run: add reaper to command list * umu_util: don't copy reaper - Reaper can be executed directly from the system path. All we need in the home directory is the runtime platform * umu_util: update launcher and runner - While those files will not be copied anymore, we still need to update them in umu_version.json if we want the configuration files to be in sync * Update tests * umu_test: update tests - Update the tests to account for the launcher files, umu-launcher and reaper not being copied to the home directory. Those files will remain in their system path defined at buid time, which makes the umu_version.json in ~/.local/share/umu and the runtime platform the only files in ~/.local/share/umu. --- Makefile.in | 10 +- ...rg.openwinecomponents.umu.umu-launcher.yml | 2 +- umu/umu-launcher/toolmanifest.vdf | 1 + umu/umu_consts.py | 13 +- umu/umu_plugins.py | 44 -- umu/umu_run.py | 22 +- umu/umu_test.py | 397 +----------------- umu/umu_test_plugins.py | 22 +- umu/umu_util.py | 167 +------- 9 files changed, 56 insertions(+), 622 deletions(-) diff --git a/Makefile.in b/Makefile.in index a8d12046f..90262efd9 100644 --- a/Makefile.in +++ b/Makefile.in @@ -109,14 +109,14 @@ $(OBJDIR)/.build-umu-launcher: | $(OBJDIR) umu-launcher: $(OBJDIR)/.build-umu-launcher umu-launcher-bin-install: umu-launcher - install -d $(DESTDIR)$(DATADIR)/$(INSTALLDIR)/umu-launcher - install -Dm 755 $(OBJDIR)/$(<)-run $(DESTDIR)$(DATADIR)/$(INSTALLDIR)/umu-launcher/umu-run + install -d $(DESTDIR)$(DATADIR)/steam/compatibilitytools.d/umu-launcher + install -Dm 755 $(OBJDIR)/$(<)-run $(DESTDIR)$(DATADIR)/steam/compatibilitytools.d/umu-launcher/umu-run umu-launcher-dist-install: $(info :: Installing umu-launcher ) - install -d $(DESTDIR)$(DATADIR)/$(INSTALLDIR)/umu-launcher - install -Dm 644 umu/umu-launcher/compatibilitytool.vdf -t $(DESTDIR)$(DATADIR)/$(INSTALLDIR)/umu-launcher - install -Dm 644 umu/umu-launcher/toolmanifest.vdf -t $(DESTDIR)$(DATADIR)/$(INSTALLDIR)/umu-launcher + install -d $(DESTDIR)$(DATADIR)/steam/compatibilitytools.d/umu-launcher + install -Dm 644 umu/umu-launcher/compatibilitytool.vdf -t $(DESTDIR)$(DATADIR)/steam/compatibilitytools.d/umu-launcher + install -Dm 644 umu/umu-launcher/toolmanifest.vdf -t $(DESTDIR)$(DATADIR)/steam/compatibilitytools.d/umu-launcher umu-launcher-install: umu-launcher-dist-install umu-launcher-bin-install diff --git a/packaging/flatpak/org.openwinecomponents.umu.umu-launcher.yml b/packaging/flatpak/org.openwinecomponents.umu.umu-launcher.yml index 41bfb8026..a8ef110a9 100644 --- a/packaging/flatpak/org.openwinecomponents.umu.umu-launcher.yml +++ b/packaging/flatpak/org.openwinecomponents.umu.umu-launcher.yml @@ -24,7 +24,7 @@ finish-args: - --filesystem=xdg-data/applications:rw - --filesystem=~/.steam:rw - --filesystem=~/Games:rw - - --filesystem=~/.local/share:rw + - --filesystem=~/.local/share/Steam:rw - --filesystem=~/.var/app/com.valvesoftware.Steam:rw - --filesystem=~/.var/app/org.openwinecomponents.umu.umu-launcher:rw - --filesystem=xdg-documents diff --git a/umu/umu-launcher/toolmanifest.vdf b/umu/umu-launcher/toolmanifest.vdf index 692c6dc3b..ec72c09d4 100644 --- a/umu/umu-launcher/toolmanifest.vdf +++ b/umu/umu-launcher/toolmanifest.vdf @@ -4,5 +4,6 @@ "commandline" "/umu-run %verb%" "version" "2" "use_tool_subprocess_reaper" "1" + "unlisted" "1" "compatmanager_layer_name" "umu-launcher" } diff --git a/umu/umu_consts.py b/umu/umu_consts.py index a4f84b27d..2dd58038c 100644 --- a/umu/umu_consts.py +++ b/umu/umu_consts.py @@ -14,13 +14,6 @@ class Color(Enum): DEBUG = "\u001b[35m" -class MODE(Enum): - """Represent the permission to apply to a file.""" - - USER_RW = 0o0644 - USER_RWX = 0o0755 - - SIMPLE_FORMAT = f"%(levelname)s: {Color.BOLD.value}%(message)s{Color.RESET.value}" DEBUG_FORMAT = f"%(levelname)s [%(module)s.%(funcName)s:%(lineno)s]:{Color.BOLD.value}%(message)s{Color.RESET.value}" # noqa: E501 @@ -40,10 +33,8 @@ class MODE(Enum): "getnativepath", } -FLATPAK_ID = environ.get("FLATPAK_ID") if environ.get("FLATPAK_ID") else "" +FLATPAK_ID = environ.get("FLATPAK_ID") or "" FLATPAK_PATH: Path = Path(environ.get("XDG_DATA_HOME"), "umu") if FLATPAK_ID else None -UMU_LOCAL: Path = ( - FLATPAK_PATH if FLATPAK_PATH else Path.home().joinpath(".local", "share", "umu") -) +UMU_LOCAL: Path = FLATPAK_PATH or Path.home().joinpath(".local", "share", "umu") diff --git a/umu/umu_plugins.py b/umu/umu_plugins.py index 3380f3fb8..928eb2933 100644 --- a/umu/umu_plugins.py +++ b/umu/umu_plugins.py @@ -141,19 +141,6 @@ def enable_steam_game_drive(env: Dict[str, str]) -> Dict[str, str]: return env -def enable_reaper(env: Dict[str, str], command: List[str], local: Path) -> List[str]: - """Enable Reaper to monitor and keep track of descendent processes.""" - command.extend( - [ - local.joinpath("reaper").as_posix(), - "UMU_ID=" + env["UMU_ID"], - "--", - ] - ) - - return command - - def enable_zenity(command: str, opts: List[str], msg: str) -> int: """Execute the command and pipe the output to Zenity. @@ -195,34 +182,3 @@ def enable_zenity(command: str, opts: List[str], msg: str) -> int: zenity_proc.stdin.close() return zenity_proc.wait() - - -def enable_flatpak( - env: Dict[str, str], local: Path, command: List[str], verb: str, opts: List[str] -) -> List[str]: - """Run umu in a Flatpak environment.""" - bin: str = which("flatpak-spawn") - - if not bin: - err: str = "flatpak-spawn not found\numu will fail to run the executable" - raise RuntimeError(err) - - command.append(bin, "--host") - for key, val in env.items(): - command.append(f"--env={key}={val}") - - enable_reaper(env, command, local) - - command.extend([local.joinpath("umu").as_posix(), "--verb", verb, "--"]) - command.extend( - [ - Path(env.get("PROTONPATH")).joinpath("proton").as_posix(), - verb, - env.get("EXE"), - ] - ) - - if opts: - command.extend([*opts]) - - return command diff --git a/umu/umu_run.py b/umu/umu_run.py index 484d47ff7..04bcb11d7 100755 --- a/umu/umu_run.py +++ b/umu/umu_run.py @@ -26,7 +26,6 @@ from umu_plugins import ( enable_steam_game_drive, set_env_toml, - enable_reaper, ) @@ -235,7 +234,11 @@ def set_env( def build_command( - env: Dict[str, str], local: Path, command: List[str], opts: List[str] = None + env: Dict[str, str], + local: Path, + root: Path, + command: List[str], + opts: List[str] = None, ) -> List[str]: """Build the command to be executed.""" verb: str = env["PROTON_VERB"] @@ -253,8 +256,13 @@ def build_command( err: str = "The following file was not found in PROTONPATH: proton" raise FileNotFoundError(err) - enable_reaper(env, command, local) - + command.extend( + [ + root.joinpath("reaper").as_posix(), + f"UMU_ID={env.get('UMU_ID')}", + "--", + ] + ) command.extend([local.joinpath("umu").as_posix(), "--verb", verb, "--"]) command.extend( [ @@ -322,9 +330,7 @@ def main() -> int: # noqa: D103 if FLATPAK_PATH and root == Path("/app/share/umu"): log.debug("Flatpak environment detected") log.debug("FLATPAK_ID: %s", FLATPAK_ID) - log.debug( - "The following path will be used to persist the runtime: %s", FLATPAK_PATH - ) + log.debug("Persisting the runtime at: %s", FLATPAK_PATH) # Setup the launcher and runtime files # An internet connection is required for new setups @@ -380,7 +386,7 @@ def main() -> int: # noqa: D103 executor.shutdown() # Run - build_command(env, UMU_LOCAL, command, opts) + build_command(env, UMU_LOCAL, root, command, opts) log.debug("%s", command) return run(command, check=False).returncode diff --git a/umu/umu_test.py b/umu/umu_test.py index 7864840ff..0c962b06b 100644 --- a/umu/umu_test.py +++ b/umu/umu_test.py @@ -14,7 +14,6 @@ from pathlib import Path from shutil import rmtree, copytree, copy from pwd import getpwuid -from umu_consts import MODE class TestGameLauncher(unittest.TestCase): @@ -115,14 +114,6 @@ def setUp(self): Path(self.test_user_share, "pressure-vessel").mkdir() Path(self.test_user_share, "pressure-vessel", "foo").touch() - # Mock umu-launcher - Path(self.test_user_share, "umu-launcher").mkdir() - Path(self.test_user_share, "umu-launcher", "compatibilitytool.vdf").touch() - Path(self.test_user_share, "umu-launcher", "toolmanifest.vdf").touch() - - # Mock Reaper - Path(self.test_user_share, "reaper").touch() - # Mock the proton file in the dir self.test_proton_dir.joinpath("proton").touch(exist_ok=True) @@ -164,106 +155,6 @@ def tearDown(self): if self.test_local_share.exists(): rmtree(self.test_local_share.as_posix()) - def test_copy(self): - """Test _copy when copying a subset of core files from a system path.""" - # Make files read-only - self.test_user_share.joinpath("reaper").chmod(0o0444) - self.test_user_share.joinpath("umu_run.py").chmod(0o0444) - self.test_user_share.joinpath("umu_consts.py").chmod(0o0444) - # Make umu-launcher files read-only - Path(self.test_user_share, "umu-launcher", "compatibilitytool.vdf").chmod( - 0o0444 - ) - Path(self.test_user_share, "umu-launcher", "toolmanifest.vdf").chmod(0o0444) - # Verify read-only before copying to ~/.local/share/umu - self.assertTrue( - self.test_user_share.joinpath("reaper").stat().st_mode == 33060, - "Expected reaper to be read only", - ) - self.assertTrue( - self.test_user_share.joinpath("umu_run.py").stat().st_mode == 33060, - "Expected umu_run to be read only", - ) - self.assertTrue( - self.test_user_share.joinpath("umu_consts.py").stat().st_mode == 33060, - "Expected umu_consts to be read only", - ) - self.assertTrue( - self.test_user_share.joinpath("umu-launcher", "compatibilitytool.vdf") - .stat() - .st_mode - == 33060, - "Expected compat vdf to be read only", - ) - self.assertTrue( - self.test_user_share.joinpath("umu-launcher", "toolmanifest.vdf") - .stat() - .st_mode - == 33060, - "Expected manifest vdf to be read only", - ) - # Copy from source dir - umu_util._copy( - self.test_user_share.joinpath("reaper"), - self.test_local_share.joinpath("reaper"), - MODE.USER_RWX, - ) - umu_util._copy( - self.test_user_share.joinpath("umu_run.py"), - self.test_local_share.joinpath("umu_run.py"), - MODE.USER_RWX, - ) - umu_util._copy( - self.test_user_share.joinpath("umu_consts.py"), - self.test_local_share.joinpath("umu_consts.py"), - ) - umu_util._copytree( - self.test_user_share.joinpath("umu-launcher"), - self.test_local_share.joinpath("umu-launcher"), - ) - # Test reaper for user rwx - # In particular, it's important umu_run and reaper are executable - # otherwise, the launcher will not work - self.assertTrue( - os.access(self.test_local_share.joinpath("reaper"), os.X_OK), - "Expected execute perm for reaper", - ) - self.assertTrue( - os.access(self.test_local_share.joinpath("reaper"), os.W_OK), - "Expected write perm for reaper", - ) - # Test umu_run.py for user rwx - self.assertTrue( - os.access(self.test_local_share.joinpath("umu_run.py"), os.X_OK), - "Expected execute perm for umu_run", - ) - self.assertTrue( - os.access(self.test_local_share.joinpath("umu_run.py"), os.W_OK), - "Expected write perm for umu_run", - ) - # Test launcher files for user rw - # Just test one of them since the default is rw and if one of them - # fails then that signals the rest will too - self.assertTrue( - os.access(self.test_local_share.joinpath("umu_consts.py"), os.W_OK), - "Expected write perm for launcher file", - ) - # Test umu-launcher - self.assertTrue( - os.access( - self.test_local_share.joinpath("umu-launcher", "compatibilitytool.vdf"), - os.W_OK, - ), - "Expected write perm for launcher file", - ) - self.assertTrue( - os.access( - self.test_local_share.joinpath("umu-launcher", "toolmanifest.vdf"), - os.W_OK, - ), - "Expected write perm for launcher file", - ) - def test_ge_proton(self): """Test check_env when the code name GE-Proton is set for PROTONPATH. @@ -355,9 +246,7 @@ def test_update_umu_empty(self): return_value=None, ): result = umu_util._update_umu( - self.test_user_share, self.test_local_share, - self.test_compat, json_root, json_local, ) @@ -404,52 +293,6 @@ def test_update_umu_empty(self): "Expected configuration files to be the same", ) - # Runner - self.assertTrue( - self.test_compat.joinpath("umu-launcher").is_dir(), - "Expected umu-launcher in compat", - ) - - for file in self.test_compat.joinpath("umu-launcher").glob("*"): - src = b"" - dst = b"" - - if file.name == "umu-run": - self.assertEqual( - self.test_compat.joinpath("umu-launcher", "umu-run").readlink(), - Path("../../../umu/umu_run.py"), - "Expected both symlinks to point to same dest", - ) - continue - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath("umu-launcher", file.name).open( - mode="rb" - ) as filer: - src = filer.read() - - self.assertEqual( - hashlib.blake2b(src).digest(), - hashlib.blake2b(dst).digest(), - "Expected files to be equal", - ) - - # Launcher - for file in self.test_local_share.glob("*.py"): - if not file.name.startswith("umu_test"): - src = b"" - dst = b"" - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath(file.name).open(mode="rb") as filer: - src = filer.read() - - if hashlib.blake2b(src).digest() != hashlib.blake2b(dst).digest(): - err = "Files did not get updated" - raise AssertionError(err) - # Runtime Platform self.assertTrue( self.test_local_share.joinpath( @@ -458,46 +301,12 @@ def test_update_umu_empty(self): "Expected runtime to in local share", ) - for file in self.test_local_share.joinpath( - json_local["umu"]["versions"]["runtime_platform"] - ).glob("*"): - if file.is_file(): - src = b"" - dst = b"" - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath( - json_root["umu"]["versions"]["runtime_platform"], file.name - ).open(mode="rb") as filer: - src = filer.read() - - if hashlib.blake2b(src).digest() != hashlib.blake2b(dst).digest(): - err = "Files did not get updated" - raise AssertionError(err) - # Pressure Vessel self.assertTrue( self.test_local_share.joinpath("pressure-vessel").is_dir(), "Expected pressure vessel to in local share", ) - for file in self.test_local_share.joinpath("pressure-vessel").glob("*"): - if file.is_file(): - src = b"" - dst = b"" - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath("pressure-vessel", file.name).open( - mode="rb" - ) as filer: - src = filer.read() - - if hashlib.blake2b(src).digest() != hashlib.blake2b(dst).digest(): - err = "Files did not get updated" - raise AssertionError(err) - def test_update_umu(self): """Test _update_umu by mocking an update to the runtime tools. @@ -511,27 +320,13 @@ def test_update_umu(self): result = None json_local = None json_root = umu_util._get_json(self.test_user_share, "umu_version.json") - py_files = [ - "umu_consts.py", - "umu_dl_util.py", - "umu_log.py", - "umu_plugins.py", - "umu_run.py", - "umu_test.py", - "umu_util.py", - ] rt_files = [ "run", "run-in-sniper", "umu", ] - runner_files = [ - "compatibilitytool.vdf", - "toolmanifest.vdf", - "umu-run", - ] # Mock an outdated umu_version.json in ~/.local/share/umu - # Downgrade these files: launcher, runner, runtime_platform + # Downgrade these files: launcher, runner, runtime_platform, reaper # We don't downgrade Pressure Vessel because it's a runtime property config = { "umu": { @@ -539,7 +334,7 @@ def test_update_umu(self): "launcher": "0.1-RC2", "runner": "0.1-RC2", "runtime_platform": "sniper_platform_0.20240125.75304", - "reaper": "1.0", + "reaper": "0.1", "pressure_vessel": "v0.20240212.0", } } @@ -554,8 +349,6 @@ def test_update_umu(self): # | +-- run-in-* (normal file) # | +-- umu (normal file) # | +-- umu_version.json (normal file) - # | +-- umu_*.py (normal file) - # | +-- umu-run (link file) # # To test for potential unintended removals in that dir and that a # selective update is performed, additional files will be added to the top-level @@ -582,14 +375,6 @@ def test_update_umu(self): "Expected umu_version.json to be in local share", ) - # Mock the launcher files - for file in py_files: - if file == "umu-run": - self.test_local_share.joinpath("umu-run").symlink_to("umu_run.py") - else: - with self.test_local_share.joinpath(file).open(mode="w") as filer: - filer.write("foo") - # Mock the runtime files self.test_local_share.joinpath( json_local["umu"]["versions"]["runtime_platform"] @@ -605,15 +390,6 @@ def test_update_umu(self): self.test_local_share.joinpath("pressure-vessel").mkdir() self.test_local_share.joinpath("pressure-vessel", "bar").touch() - # Mock umu-launcher - self.test_compat.joinpath("umu-launcher").mkdir() - for file in runner_files: - if file == "umu-run": - self.test_compat.joinpath("umu-run").symlink_to("../../../umu_run.py") - else: - with self.test_compat.joinpath(file).open(mode="w") as filer: - filer.write("foo") - # Update with patch.object( umu_util, @@ -621,9 +397,7 @@ def test_update_umu(self): return_value=None, ): result = umu_util._update_umu( - self.test_user_share, self.test_local_share, - self.test_compat, json_root, json_local, ) @@ -672,21 +446,6 @@ def test_update_umu(self): "Expected test Proton to survive after update", ) - # Verify the count for .local/share/umu - num_share = len( - [ - file - for file in self.test_user_share.glob("*") - if not file.name.startswith("umu_test") - ] - ) - num_local = len([file for file in self.test_local_share.glob("*")]) - self.assertEqual( - num_share, - num_local - 3, - "Expected /usr/share/umu and .local/share/umu to contain same files", - ) - # Check if the configuration files are equal because we update this on # every update of the tools with self.test_user_share.joinpath("umu_version.json").open(mode="rb") as file1: @@ -702,69 +461,6 @@ def test_update_umu(self): "Expected configuration files to be the same", ) - # Runner - # The hashes should be compared because we written data in the mocked files - self.assertTrue( - self.test_compat.joinpath("umu-launcher").is_dir(), - "Expected umu-launcher in compat", - ) - - # Verify the count for .local/share/Steam/umu-launcher - num_share = len( - [file for file in self.test_user_share.joinpath("umu-launcher").glob("*")] - ) - num_local = len( - [file for file in self.test_compat.joinpath("umu-launcher").glob("*")] - ) - - # Subtract one because a symbolic link is dynamically created - self.assertEqual( - num_share, - num_local, - "Expected .local/share/Steam/compatibilitytools.d/umu-launcher" - "and /usr/share/umu/umu-launcher to contain same files", - ) - - for file in self.test_compat.joinpath("umu-launcher").glob("*"): - src = b"" - dst = b"" - - if file.name == "umu-run": - self.assertEqual( - self.test_compat.joinpath("umu-launcher", "umu-run").readlink(), - Path("../../../umu/umu_run.py"), - "Expected both symlinks to point to same dest", - ) - continue - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath("umu-launcher", file.name).open( - mode="rb" - ) as filer: - src = filer.read() - - self.assertEqual( - hashlib.blake2b(src).digest(), - hashlib.blake2b(dst).digest(), - "Expected files to be equal", - ) - - # Launcher - for file in self.test_local_share.glob("*.py"): - if not file.name.startswith("umu_test"): - src = b"" - dst = b"" - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath(file.name).open(mode="rb") as filer: - src = filer.read() - - if hashlib.blake2b(src).digest() != hashlib.blake2b(dst).digest(): - err = "Files did not get updated" - raise AssertionError(err) - # Runtime Platform self.assertTrue( self.test_local_share.joinpath( @@ -773,46 +469,12 @@ def test_update_umu(self): "Expected runtime to in local share", ) - for file in self.test_local_share.joinpath( - json_local["umu"]["versions"]["runtime_platform"] - ).glob("*"): - if file.is_file(): - src = b"" - dst = b"" - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath( - json_root["umu"]["versions"]["runtime_platform"], file.name - ).open(mode="rb") as filer: - src = filer.read() - - if hashlib.blake2b(src).digest() != hashlib.blake2b(dst).digest(): - err = "Files did not get updated" - raise AssertionError(err) - # Pressure Vessel self.assertTrue( self.test_local_share.joinpath("pressure-vessel").is_dir(), "Expected pressure vessel to in local share", ) - for file in self.test_local_share.joinpath("pressure-vessel").glob("*"): - if file.is_file(): - src = b"" - dst = b"" - - with file.open(mode="rb") as filer: - dst = filer.read() - with self.test_user_share.joinpath("pressure-vessel", file.name).open( - mode="rb" - ) as filer: - src = filer.read() - - if hashlib.blake2b(src).digest() != hashlib.blake2b(dst).digest(): - err = "Files did not get updated" - raise AssertionError(err) - def test_install_umu(self): """Test _install_umu by mocking a first launch. @@ -827,16 +489,6 @@ def test_install_umu(self): umu-launcher is expected to be copied to compatibilitytools.d """ result = None - runner_files = {"compatibilitytool.vdf", "toolmanifest.vdf", "umu-run"} - py_files = { - "umu_consts.py", - "umu_dl_util.py", - "umu_log.py", - "umu_plugins.py", - "umu_run.py", - "umu_test.py", - "umu_util.py", - } json = umu_util._get_json(self.test_user_share, "umu_version.json") # Mock setting up the runtime @@ -848,7 +500,7 @@ def test_install_umu(self): return_value=None, ): result = umu_util._install_umu( - self.test_user_share, self.test_local_share, self.test_compat, json + self.test_user_share, self.test_local_share, json ) copytree( Path(self.test_user_share, "sniper_platform_0.20240125.75305"), @@ -875,22 +527,6 @@ def test_install_umu(self): "Expected umu_version.json to exist", ) - # umu-launcher - self.assertTrue( - Path(self.test_user_share, "umu-launcher").is_dir(), - "Expected umu-launcher to exist", - ) - for file in Path(self.test_compat, "umu-launcher").glob("*"): - if file.name not in runner_files: - err = "A non-runner file was copied" - raise AssertionError(err) - if file in runner_files and file.is_symlink(): - self.assertEqual( - file.readlink(), - Path("../../../umu-run"), - "Expected umu-run symlink to exist", - ) - # Pressure Vessel self.assertTrue( Path(self.test_user_share, "pressure-vessel").is_dir(), @@ -923,27 +559,6 @@ def test_install_umu(self): Path(self.test_local_share, "umu").is_file(), "Expected umu to exist" ) - # Python files - self.assertTrue( - list(self.test_local_share.glob("*.py")), - "Expected Python files to exist", - ) - for file in self.test_local_share.glob("*.py"): - if file.name not in py_files: - err = "A non-launcher file was copied" - raise AssertionError(err) - - # Symlink - self.assertTrue( - Path(self.test_local_share, "umu-run").is_symlink(), - "Expected umu to exist", - ) - self.assertEqual( - Path(self.test_local_share, "umu-run").readlink(), - Path("umu_run.py"), - "Expected umu-run -> umu_run.py", - ) - def test_get_json_err(self): """Test _get_json when specifying a corrupted umu_version.json file. @@ -1697,9 +1312,7 @@ def test_build_command(self): "setup_runtime", return_value=None, ): - umu_util._install_umu( - self.test_user_share, self.test_local_share, self.test_compat, json - ) + umu_util._install_umu(self.test_user_share, self.test_local_share, json) copytree( Path(self.test_user_share, "sniper_platform_0.20240125.75305"), Path(self.test_local_share, "sniper_platform_0.20240125.75305"), @@ -1718,7 +1331,7 @@ def test_build_command(self): # Build test_command = umu_run.build_command( - self.env, self.test_local_share, test_command + self.env, self.test_local_share, self.test_user_share, test_command ) self.assertIsInstance(test_command, list, "Expected a List from build_command") self.assertEqual( diff --git a/umu/umu_test_plugins.py b/umu/umu_test_plugins.py index 80e20d726..df991edb1 100644 --- a/umu/umu_test_plugins.py +++ b/umu/umu_test_plugins.py @@ -218,9 +218,7 @@ def test_build_command_entry(self): "setup_runtime", return_value=None, ): - umu_util._install_umu( - self.test_user_share, self.test_local_share, self.test_compat, json - ) + umu_util._install_umu(self.test_user_share, self.test_local_share, json) copytree( Path(self.test_user_share, "sniper_platform_0.20240125.75305"), Path(self.test_local_share, "sniper_platform_0.20240125.75305"), @@ -238,7 +236,9 @@ def test_build_command_entry(self): # Build with self.assertRaisesRegex(FileNotFoundError, "_v2-entry-point"): - umu_run.build_command(self.env, self.test_local_share, test_command) + umu_run.build_command( + self.env, self.test_local_share, self.test_user_share, test_command + ) def test_build_command_proton(self): """Test build_command. @@ -287,9 +287,7 @@ def test_build_command_proton(self): "setup_runtime", return_value=None, ): - umu_util._install_umu( - self.test_user_share, self.test_local_share, self.test_compat, json - ) + umu_util._install_umu(self.test_user_share, self.test_local_share, json) copytree( Path(self.test_user_share, "sniper_platform_0.20240125.75305"), Path(self.test_local_share, "sniper_platform_0.20240125.75305"), @@ -311,7 +309,9 @@ def test_build_command_proton(self): # Build with self.assertRaisesRegex(FileNotFoundError, "proton"): - umu_run.build_command(self.env, self.test_local_share, test_command) + umu_run.build_command( + self.env, self.test_local_share, self.test_user_share, test_command + ) def test_build_command_toml(self): """Test build_command. @@ -360,9 +360,7 @@ def test_build_command_toml(self): "setup_runtime", return_value=None, ): - umu_util._install_umu( - self.test_user_share, self.test_local_share, self.test_compat, json - ) + umu_util._install_umu(self.test_user_share, self.test_local_share, json) copytree( Path(self.test_user_share, "sniper_platform_0.20240125.75305"), Path(self.test_local_share, "sniper_platform_0.20240125.75305"), @@ -384,7 +382,7 @@ def test_build_command_toml(self): # Build test_command_result = umu_run.build_command( - self.env, self.test_local_share, test_command + self.env, self.test_local_share, self.test_user_share, test_command ) self.assertTrue( test_command_result is test_command, "Expected the same reference" diff --git a/umu/umu_util.py b/umu/umu_util.py index 1b75b906b..bf5ea87bb 100644 --- a/umu/umu_util.py +++ b/umu/umu_util.py @@ -1,6 +1,6 @@ from tarfile import open as tar_open, TarInfo from os import environ -from umu_consts import CONFIG, STEAM_COMPAT, UMU_LOCAL, MODE +from umu_consts import CONFIG, UMU_LOCAL from typing import Any, Dict, List, Callable from json import load, dump from umu_log import log @@ -137,14 +137,12 @@ def setup_umu(root: Path, local: Path) -> None: # New install or umu dir is empty if not local.exists() or not any(local.iterdir()): - return _install_umu(root, local, STEAM_COMPAT, json) + return _install_umu(root, local, json) - return _update_umu(root, local, STEAM_COMPAT, json, _get_json(local, CONFIG)) + return _update_umu(local, json, _get_json(local, CONFIG)) -def _install_umu( - root: Path, local: Path, steam_compat: Path, json: Dict[str, Any] -) -> None: +def _install_umu(root: Path, local: Path, json: Dict[str, Any]) -> None: """For new installations, copy all of the umu tools at a user-writable location. The designated locations to copy to will be: @@ -155,36 +153,11 @@ def _install_umu( """ log.debug("New install detected") log.console("Setting up Unified Launcher for Windows Games on Linux ...") - local.mkdir(parents=True, exist_ok=True) # Config log.console(f"Copied {CONFIG} -> {local}") - _copy(root.joinpath(CONFIG), local.joinpath(CONFIG)) - - # Reaper - log.console(f"Copied reaper -> {local}") - _copy(root.joinpath("reaper"), local.joinpath("reaper"), MODE.USER_RWX) - - # Launcher files - for file in root.glob("*.py"): - if not file.name.startswith(("umu_test", "umu_run")): - log.console(f"Copied {file} -> {local}") - _copy(file, local.joinpath(file.name)) - _copy(root.joinpath("umu_run.py"), local.joinpath("umu_run.py"), MODE.USER_RWX) - - local.joinpath("umu-run").symlink_to("umu_run.py") - - # Runner - log.console(f"Copied umu-launcher -> {steam_compat}") - steam_compat.mkdir(parents=True, exist_ok=True) - # Remove existing files if they exist -- this is a clean install. - if steam_compat.joinpath("umu-launcher").is_dir(): - rmtree(steam_compat.joinpath("umu-launcher").as_posix()) - _copytree( - root.joinpath("umu-launcher"), - steam_compat.joinpath("umu-launcher"), - ) + copy(root.joinpath(CONFIG), local.joinpath(CONFIG)) # Runtime platform setup_runtime(json) @@ -193,9 +166,7 @@ def _install_umu( def _update_umu( - root: Path, local: Path, - steam_compat: Path, json_root: Dict[str, Any], json_local: Dict[str, Any], ) -> None: @@ -220,41 +191,26 @@ def _update_umu( # Be lazy and just trust the integrity of local for key, val in json_root["umu"]["versions"].items(): if key == "reaper": - reaper: str = json_local["umu"]["versions"]["reaper"] - - # Directory is absent - if not local.joinpath("reaper").is_file(): - log.warning("Reaper not found") - _copy(root.joinpath("reaper"), local.joinpath("reaper"), MODE.USER_RWX) - log.console(f"Restored {key} to {val}") - - # Update - if val != reaper: - log.console(f"Updating {key} to {val}") - - local.joinpath("reaper").unlink(missing_ok=True) - _copy(root.joinpath("reaper"), local.joinpath("reaper"), MODE.USER_RWX) - - json_local["umu"]["versions"]["reaper"] = val + if val == json_local["umu"]["versions"]["reaper"]: + continue + log.console(f"Updating {key} to {val}") + json_local["umu"]["versions"]["reaper"] = val elif key == "runtime_platform": - # Old runtime runtime: str = json_local["umu"]["versions"]["runtime_platform"] - # Redownload the runtime if absent or pressure vessel is absent if ( not local.joinpath(runtime).is_dir() or not local.joinpath("pressure-vessel").is_dir() ): - # Redownload log.warning("Runtime Platform not found") if local.joinpath("pressure-vessel").is_dir(): rmtree(local.joinpath("pressure-vessel").as_posix()) if local.joinpath(runtime).is_dir(): rmtree(local.joinpath(runtime).as_posix()) - futures.append(executor.submit(setup_runtime, json_root)) log.console(f"Restoring Runtime Platform to {val} ...") - elif ( + continue + if ( local.joinpath(runtime).is_dir() and local.joinpath("pressure-vessel").is_dir() and val != runtime @@ -264,83 +220,17 @@ def _update_umu( rmtree(local.joinpath("pressure-vessel").as_posix()) rmtree(local.joinpath(runtime).as_posix()) futures.append(executor.submit(setup_runtime, json_root)) - json_local["umu"]["versions"]["runtime_platform"] = val elif key == "launcher": - # Launcher - is_missing: bool = False - launcher: str = json_local["umu"]["versions"]["launcher"] - - # Update - if val != launcher: - log.console(f"Updating {key} to {val}") - - # Python files - for file in root.glob("*.py"): - if not file.name.startswith(("umu_test", "umu_run")): - local.joinpath(file.name).unlink(missing_ok=True) - _copy(file, local.joinpath(file.name)) - _copy( - root.joinpath("umu_run.py"), - local.joinpath("umu_run.py"), - MODE.USER_RWX, - ) - - # Symlink - local.joinpath("umu-run").unlink(missing_ok=True) - local.joinpath("umu-run").symlink_to("umu_run.py") - - json_local["umu"]["versions"]["launcher"] = val + if val == json_local["umu"]["versions"]["launcher"]: continue - - # Check for missing files - for file in [ - file - for file in root.glob("*.py") - if not file.name.startswith(("umu_test", "umu_run")) - and not local.joinpath(file.name).is_file() - ]: - is_missing = True - log.warning("Missing %s", file.name) - _copy(file, local.joinpath(file.name)) - - if not local.joinpath("umu_run.py"): - log.warning("Missing %s", file.name) - _copy( - root.joinpath("umu_run.py"), - local.joinpath("umu_run.py"), - MODE.USER_RWX, - ) - - if is_missing: - log.console(f"Restored {key} to {val}") - local.joinpath("umu-run").unlink(missing_ok=True) - local.joinpath("umu-run").symlink_to("umu_run.py") + log.console(f"Updating {key} to {val}") + json_local["umu"]["versions"]["launcher"] = val elif key == "runner": - # Runner - runner: str = json_local["umu"]["versions"]["runner"] - - # Directory is absent - if not steam_compat.joinpath("umu-launcher").is_dir(): - log.warning("umu-launcher not found") - - _copytree( - root.joinpath("umu-launcher"), - steam_compat.joinpath("umu-launcher"), - ) - - log.console(f"Restored umu-launcher to {val}") - elif steam_compat.joinpath("umu-launcher").is_dir() and val != runner: - # Update - log.console(f"Updating {key} to {val}") - - rmtree(steam_compat.joinpath("umu-launcher").as_posix()) - _copytree( - root.joinpath("umu-launcher"), - steam_compat.joinpath("umu-launcher"), - ) - - json_local["umu"]["versions"]["runner"] = val + if val == json_local["umu"]["versions"]["runner"]: + continue + log.console(f"Updating {key} to {val}") + json_local["umu"]["versions"]["runner"] = val for _ in futures: _.result() @@ -379,24 +269,3 @@ def _get_json(path: Path, config: str) -> Dict[str, Any]: raise ValueError(err) return json - - -def _copy(src: Path, dst: Path, mode: MODE = MODE.USER_RW) -> None: - dst.parent.mkdir(parents=True, exist_ok=True) - - if src.is_symlink(): - dst.symlink_to(src.readlink()) - return - - copy(src, dst) - dst.chmod(mode.value) - - -def _copytree(src: Path, dest: Path, mode: MODE = MODE.USER_RW) -> None: - for file in src.iterdir(): - if file.is_dir(): - dest_subdir = dest / file.name - dest_subdir.mkdir(parents=True, exist_ok=True) - _copytree(file, dest_subdir) - else: - _copy(file, dest / file.name, mode)