From cdb2cfa6f0c6468b3fb0c9aa8971e081afbb5c1a Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:28:35 +0200 Subject: [PATCH] feat: find RetroBat through the folder it records RetroBat.exe writes its folder to HKCU\SOFTWARE\RetroBat LatestKnownInstallPath on every start (retrobat-executable 702da66a, Program.cs:57 and 99, RegistryTools.cs:15-50). Only C:\RetroBat and the profile folder were looked at, so an install on D: was missed and a forced --platform retrobat wrote into C:\RetroBat\bios. The value is read through winreg, or reg.exe under WSL, before the fixed folders. --- install.ps1 | 2 +- install.py | 59 +++++++++++++++++++++++++++++++++++++++++ install.sh | 2 +- platforms/_registry.yml | 8 ++++++ tests/test_install.py | 47 ++++++++++++++++++++++++++++++++ wiki/installer.md | 1 + 6 files changed, 117 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 6bc44b5f..b5d6fbf8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "caec72e5ebd146509e3b1fabc7d8d4c5c921567592d6d1581df6e5cc50f3e96f" + $defaultInstallSha256 = "f05f862fcebc16e99d692a20a09163665828ea0383643fcc96a41b11d77fcff7" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 2c87e2f0..17a06481 100644 --- a/install.py +++ b/install.py @@ -1092,6 +1092,64 @@ def _detect_by_executable(os_type: str, found: list[tuple[str, Path]]) -> None: found.append((platform_name, newest.parent / subdir)) +# The registry's registry rules: (os, platform, key, value, BIOS folder under +# the folder the value names). RetroBat.exe writes its own folder there on +# every start (retrobat-executable 702da66a, RetroBat/RetroBat/Program.cs:57 +# and 99, RegistryTools.cs:15-50), wherever it was installed. The path rules +# only knew C:\RetroBat and the profile folder. +_REGISTRY_RULES: tuple[tuple[str, str, str, str, str], ...] = ( + ("windows", "retrobat", "HKCU\\SOFTWARE\\RetroBat", "LatestKnownInstallPath", "bios"), +) + + +def _read_registry(key: str, value: str) -> str | None: + """A string value of the Windows registry, read natively or, under WSL, + through reg.exe. None when the key, the value or the means is missing.""" + if os.name == "nt": + import winreg # noqa: PLC0415 + + hive, _, subkey = key.partition("\\") + roots = {"HKCU": winreg.HKEY_CURRENT_USER, "HKLM": winreg.HKEY_LOCAL_MACHINE} + try: + with winreg.OpenKey(roots[hive], subkey) as handle: + data, _kind = winreg.QueryValueEx(handle, value) + except (OSError, KeyError): + return None + return data.strip() if isinstance(data, str) and data.strip() else None + if not _WSL["active"]: + return None + try: + result = subprocess.run( + ["reg.exe", "query", key, "/v", value], + capture_output=True, text=True, errors="replace", timeout=10, + stdin=subprocess.DEVNULL, cwd="/mnt/c" if Path("/mnt/c").is_dir() else None, + ) + except (OSError, subprocess.SubprocessError): + return None + match = re.search( + rf"^\s*{re.escape(value)}\s+REG_(?:EXPAND_)?SZ\s+(.+?)\s*$", result.stdout, re.M + ) + return match.group(1) if match else None + + +def _detect_by_registry(os_type: str, found: list[tuple[str, Path]]) -> None: + """Add a platform whose own program names its folder in the registry. + + A folder since moved or removed is passed over, so the path rules still + get their turn. + """ + systems = ("windows",) if os_type in ("windows", "wsl") else () + for rule_os, platform_name, key, value, subdir in _REGISTRY_RULES: + if rule_os not in systems or any(name == platform_name for name, _ in found): + continue + folder = _read_registry(key, value) + if not folder: + continue + base = _host_path(folder.rstrip("\\/") or folder) + if base.is_dir(): + found.append((platform_name, base / subdir)) + + def _detect_by_path(os_type: str, found: list[tuple[str, Path]]) -> None: """Add what a registry path rule finds, for a platform not found yet.""" systems = ("linux", "windows") if os_type == "wsl" else (os_type,) @@ -1239,6 +1297,7 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: found.append(("bizhawk", bizhawk)) _detect_by_executable(os_type, found) + _detect_by_registry(os_type, found) _detect_by_path(os_type, found) return found diff --git a/install.sh b/install.sh index 72d44f87..54028c51 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="caec72e5ebd146509e3b1fabc7d8d4c5c921567592d6d1581df6e5cc50f3e96f" +DEFAULT_INSTALL_SHA256="f05f862fcebc16e99d692a20a09163665828ea0383643fcc96a41b11d77fcff7" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/platforms/_registry.yml b/platforms/_registry.yml index d8ef431d..495ccdc5 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -351,6 +351,14 @@ platforms: target_source: null install: detect: + # RetroBat.exe writes its own folder there on every start, wherever it + # was installed (retrobat-executable 702da66a, + # RetroBat/RetroBat/Program.cs:57 and 99, RegistryTools.cs:15-50). + - os: windows + method: registry + key: 'HKCU\SOFTWARE\RetroBat' + value: LatestKnownInstallPath + bios_subdir: bios # installer.iss: DefaultDirName=C:\RetroBat, the first place to look. - os: windows method: path_exists diff --git a/tests/test_install.py b/tests/test_install.py index b6282f3a..46daaf4b 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -3800,5 +3800,52 @@ class ANetworkFailureNamesTheNetwork(unittest.TestCase): self.assertNotIn("being written to", text) +class RetroBatIsFoundWhereItSaysItIs(unittest.TestCase): + """RetroBat on D:\\RetroBat was not detected, and --platform retrobat + wrote 7.6 GB into C:\\RetroBat\\bios, a folder it does not read. + RetroBat.exe writes its folder to the registry on every start.""" + + def test_the_table_is_the_registry(self): + import yaml # noqa: PLC0415 + + registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text()) + rules = { + (rule["os"], name, rule["key"], rule["value"], rule["bios_subdir"]) + for name, entry in registry["platforms"].items() + for rule in (entry.get("install") or {}).get("detect", []) + if rule["method"] == "registry" + } + self.assertEqual(set(install._REGISTRY_RULES), rules) + + def test_the_registry_folder_wins_over_the_default(self): + folder = Path(_tempdir(self)) + with unittest.mock.patch.object(install, "_read_registry", + return_value=str(folder) + "\\"): + found: list = [] + install._detect_by_registry("windows", found) + self.assertEqual(found, [("retrobat", folder / "bios")]) + + def test_a_folder_since_removed_is_passed_over(self): + gone = Path(_tempdir(self)) / "moved" + with unittest.mock.patch.object(install, "_read_registry", return_value=str(gone)): + found: list = [] + install._detect_by_registry("windows", found) + self.assertEqual(found, []) + + def test_wsl_reads_it_through_reg_exe(self): + output = ("\nHKEY_CURRENT_USER\\SOFTWARE\\RetroBat\n" + " LatestKnownInstallPath REG_SZ D:\\Games\\RetroBat\\\n\n") + with unittest.mock.patch.dict(install._WSL, {"active": True, "root": "/mnt"}), \ + unittest.mock.patch.object(install.os, "name", "posix"), \ + unittest.mock.patch.object( + install.subprocess, "run", + return_value=SimpleNamespace(stdout=output, returncode=0)) as run: + value = install._read_registry("HKCU\\SOFTWARE\\RetroBat", "LatestKnownInstallPath") + self.assertEqual(value, "D:\\Games\\RetroBat\\") + self.assertEqual(install._host_path(value.rstrip("\\")), + Path("/mnt/d/Games/RetroBat")) + self.assertEqual(run.call_args[0][0][:2], ["reg.exe", "query"]) + + if __name__ == "__main__": unittest.main() diff --git a/wiki/installer.md b/wiki/installer.md index e8b6b3c8..63ffad36 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -318,6 +318,7 @@ WSL does not pass them. A forced `--platform retrobat` falls back to | Evidence | Platform | BIOS directory | |----------|----------|----------------| +| `HKCU\SOFTWARE\RetroBat` `LatestKnownInstallPath`, which `RetroBat.exe` writes on every start (read through `reg.exe` under WSL) | RetroBat | that folder + `\bios`, before the fixed folders above | | `~/.config/EmuDeck/settings.sh` | EmuDeck | `emulationPath` + `/bios` | | `%APPDATA%\EmuDeck\settings.ps1` | EmuDeck | `$emulationPath` + `\bios` | | `~/.var/app/net.retrodeck.retrodeck/config/retrodeck/retrodeck.json` | RetroDECK | `paths.rd_home_path`, falling back to the pre-migration `retrodeck.cfg` and its `rdhome` |