diff --git a/umu/umu_dl_util.py b/umu/umu_dl_util.py index b1813321..b9ab392b 100644 --- a/umu/umu_dl_util.py +++ b/umu/umu_dl_util.py @@ -13,6 +13,7 @@ from umu_log import log from umu_consts import STEAM_COMPAT from tempfile import mkdtemp +from threading import Thread try: from tarfile import tar_filter @@ -275,7 +276,39 @@ def _get_latest( _fetch_proton(env, tmp, files) - _extract_dir(tmp.joinpath(tarball), steam_compat) + # Set latest UMU/GE-Proton + if version == "UMU-Proton": + threads: List[Thread] = [] + log.debug("Updating UMU-Proton") + old_versions: List[Path] = sorted( + [ + file + for file in steam_compat.glob("*") + if file.name.startswith(("UMU-Proton", "ULWGL-Proton")) + ] + ) + tar_path: Path = tmp.joinpath(tarball) + + # Extract the latest archive and update UMU-Proton + # Will extract and remove the previous stable versions + # Though, ideally, an in-place differential update would be + # performed instead for this job but this will do for now + log.debug("Extracting %s -> %s", tar_path, steam_compat) + extract: Thread = Thread(target=_extract_dir, args=[tar_path, steam_compat]) + extract.start() + threads.append(extract) + update: Thread = Thread( + target=_update_proton, args=[proton, steam_compat, old_versions] + ) + update.start() + threads.append(update) + for thread in threads: + thread.join() + else: + # For GE-Proton, keep the previous build. Since it's a rebase + # of bleeding edge, regressions are more likely to occur + _extract_dir(tmp.joinpath(tarball), steam_compat) + environ["PROTONPATH"] = steam_compat.joinpath(proton).as_posix() env["PROTONPATH"] = environ["PROTONPATH"] @@ -289,7 +322,6 @@ def _get_latest( tarball: str = files[1][0] # Digest mismatched - # Refer to the cache for old version next # Since we do not want the user to use a suspect file, delete it tmp.joinpath(tarball).unlink(missing_ok=True) return None @@ -299,7 +331,6 @@ def _get_latest( # Exit cleanly # Clean up extracted data and cache to prevent corruption/errors - # Refer to the cache for old version next _cleanup(tarball, proton_dir, tmp, steam_compat) return None except HTTPException: # Download failed @@ -307,3 +338,42 @@ def _get_latest( return None return env + + +def _update_proton(proton: str, steam_compat: Path, old_versions: List[Path]) -> None: + """Create a symbolic link and remove the previous UMU-Proton. + + The symbolic link will be used by clients to reference the PROTONPATH + which can be used for tasks such as killing the running wineserver in + the prefix + + Assumes that the directories that are named ULWGL/UMU-Proton is ours + and will be removed. + """ + threads: List[Thread] = [] + old: Path = None + log.debug("Old: %s", old_versions) + log.debug("Linking UMU-Latest -> %s", proton) + steam_compat.joinpath("UMU-Latest").unlink(missing_ok=True) + steam_compat.joinpath("UMU-Latest").symlink_to(proton) + + if not old_versions: + return + + old = old_versions.pop() + if old.is_dir(): + log.debug("Removing: %s", old) + oldest: Thread = Thread(target=rmtree, args=[old.as_posix()]) + oldest.start() + threads.append(oldest) + + for proton in old_versions: + if proton.is_dir(): + log.debug("Old stable build found") + log.debug("Removing: %s", proton) + sibling: Thread = Thread(target=rmtree, args=[proton.as_posix()]) + sibling.start() + threads.append(sibling) + + for thread in threads: + thread.join() diff --git a/umu/umu_test.py b/umu/umu_test.py index 58512403..7a083dcf 100644 --- a/umu/umu_test.py +++ b/umu/umu_test.py @@ -1012,6 +1012,77 @@ def test_latest_offline(self): self.assertFalse(self.env["PROTONPATH"], "Expected PROTONPATH to be empty") self.assertFalse(result, "Expected None to be returned from _get_latest") + def test_latest_umu(self): + """Test _get_latest when online and when an empty PROTONPATH is set. + + Tests that the latest UMU-Proton was set to PROTONPATH and old + stable versions were removed in the process. + """ + result = None + latest = Path("UMU-Proton-9.0-beta16") + latest.mkdir() + Path(f"{latest}.sha512sum").touch() + files = [(f"{latest}.sha512sum", ""), (f"{latest}.tar.gz", "")] + + # Mock the latest Proton in /tmp + test_archive = self.test_cache.joinpath(f"{latest}.tar.gz") + with tarfile.open(test_archive.as_posix(), "w:gz") as tar: + tar.add(latest.as_posix(), arcname=latest.as_posix()) + + # Mock old versions + self.test_compat.joinpath("UMU-Proton-9.0-beta15").mkdir() + self.test_compat.joinpath("UMU-Proton-9.0-beta14").mkdir() + self.test_compat.joinpath("ULWGL-Proton-8.0-5-2").mkdir() + + # Create foo files and GE-Proton. We do *not* want unintended + # removals + self.test_compat.joinpath("foo").mkdir() + self.test_compat.joinpath("GE-Proton9-2").mkdir() + + os.environ["PROTONPATH"] = "" + + with ( + patch("umu_dl_util._fetch_proton"), + ): + result = umu_dl_util._get_latest( + self.env, self.test_compat, self.test_cache, files + ) + self.assertTrue(result is self.env, "Expected the same reference") + # Verify the latest was set + self.assertEqual( + self.env.get("PROTONPATH"), + self.test_compat.joinpath(latest).as_posix(), + "Expected latest to be set", + ) + # Verify that the old versions were deleted + self.assertFalse( + self.test_compat.joinpath("UMU-Proton-9.0-beta15").exists(), + "Expected old version to be removed", + ) + self.assertFalse( + self.test_compat.joinpath("UMU-Proton-9.0-beta14").exists(), + "Expected old version to be removed", + ) + self.assertFalse( + self.test_compat.joinpath("ULWGL-Proton-8.0-5-2").exists(), + "Expected old version to be removed", + ) + # Verify foo files survived + self.assertTrue( + self.test_compat.joinpath("foo").exists(), "Expected foo to survive" + ) + self.assertTrue( + self.test_compat.joinpath("GE-Proton9-2").exists(), + "Expected GE-Proton9-2 to survive", + ) + self.assertTrue( + self.test_compat.joinpath("UMU-Latest").is_symlink(), + "Expected UMU-Latest symlink", + ) + + latest.rmdir() + Path(f"{latest}.sha512sum").unlink() + def test_steamcompat_nodir(self): """Test _get_from_steamcompat when Proton doesn't exist in compat dir. diff --git a/umu/umu_util.py b/umu/umu_util.py index 3c1a6cba..930f28c1 100644 --- a/umu/umu_util.py +++ b/umu/umu_util.py @@ -233,7 +233,7 @@ def _update_umu( if not local.joinpath("reaper").is_file(): log.warning("Reaper not found") copy(root.joinpath("reaper"), local.joinpath("reaper")) - log.console(f"Restored {key} to {val} ...") + log.console(f"Restored {key} to {val}") # Update if val != reaper: