From 16f8b87ef47fec633d732f5c34f5e6eccad9a856 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:51:08 +0200 Subject: [PATCH] fix: find BizHawk beside its executable BizHawk reads the Firmware folder next to EmuHawk wherever the archive was extracted. The installer looked in ~/.config/BizHawk/Firmware, which no build reads, and wrote there when nothing was found. --- install.ps1 | 2 +- install.py | 54 ++++++++++++++++++++++++++++++++++++++--- install.sh | 2 +- platforms/_registry.yml | 12 +++++++-- scripts/packreadme.py | 7 +++--- tests/test_install.py | 49 +++++++++++++++++++++++++++++++++++-- wiki/installer.md | 10 +++++--- 7 files changed, 118 insertions(+), 18 deletions(-) diff --git a/install.ps1 b/install.ps1 index 33c2b253..72c24e32 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "7fb25b2ee8d7cdab5c852431732cf8ef92e6e9a81d24aeae7cb24b34dd413660" +$defaultInstallSha256 = "d86244b95c49d37d3950e0547fe13fa6df589dc21ba9868d46da4ab305522b3f" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 64c72d67..f3060a58 100644 --- a/install.py +++ b/install.py @@ -178,20 +178,17 @@ def _os_default_dests(os_type: str) -> dict[str, Path]: """ home = Path.home() if os_type == "windows": - profile = Path(os.environ.get("USERPROFILE", str(home))) return { # RetroArch's system directory defaults to \system # (platform_win32.c), and its installer to C:\RetroArch-Win64. "retroarch": Path("C:/RetroArch-Win64/system"), # RetroBat's installer defaults to C:\RetroBat (installer.iss). "retrobat": Path("C:/RetroBat/bios"), - "bizhawk": profile / "BizHawk" / "Firmware", } if os_type == "darwin": return {"retroarch": _macos_retroarch_base() / "system"} return { "retroarch": home / ".config" / "retroarch" / "system", - "bizhawk": home / ".config" / "BizHawk" / "Firmware", } # Set when the run is interrupted, so queued downloads end instead of draining @@ -949,7 +946,6 @@ _PATH_RULES: tuple[tuple[str, str, str, str | None], ...] = ( ("linux", "romm", "/romm/library/bios", None), ("linux", "retropie", "$HOME/RetroPie/BIOS", None), ("windows", "bizhawk", "%USERPROFILE%\\BizHawk\\Firmware", None), - ("linux", "bizhawk", "$HOME/.config/BizHawk/Firmware", None), ("linux", "misterfpga", "/media/fat/MiSTer", "/media/fat/games"), ) @@ -967,6 +963,38 @@ def _rule_path(template: str) -> Path: return Path(template) +# The registry's executable rules: (os, platform, glob under $HOME or +# %USERPROFILE%, folder beside the executable that holds the BIOS). BizHawk is +# a portable archive reading /Firmware (PathExtensions.cs +# DataDirectoryPath), wherever it was extracted; ~/.config/BizHawk/Firmware, +# the old guess, is a folder no build reads. +_EXE_RULES: tuple[tuple[str, str, tuple[str, ...], str], ...] = ( + ("linux", "bizhawk", ("$HOME/BizHawk*/EmuHawkMono.sh", "$HOME/*/BizHawk*/EmuHawkMono.sh"), + "Firmware"), + ("windows", "bizhawk", ("%USERPROFILE%\\BizHawk*\\EmuHawk.exe", + "%USERPROFILE%\\*\\BizHawk*\\EmuHawk.exe"), "Firmware"), +) + + +def _detect_by_executable(os_type: str, found: list[tuple[str, Path]]) -> None: + """Add a portable install found by its executable, the newest first.""" + systems = ("linux", "windows") if os_type == "wsl" else (os_type,) + for rule_os, platform_name, patterns, subdir in _EXE_RULES: + if rule_os not in systems or any(name == platform_name for name, _ in found): + continue + hits: list[Path] = [] + for pattern in patterns: + base, _, rest = pattern.partition("/" if "/" in pattern else "\\") + glob = "/".join(part for part in re.split(r"[\\/]", rest) if part) + try: + hits.extend(path for path in _rule_path(base).glob(glob) if path.is_file()) + except OSError: + continue + if hits: + newest = max(hits, key=lambda path: path.stat().st_mtime) + found.append((platform_name, newest.parent / 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,) @@ -1113,6 +1141,7 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: if bizhawk is not None and not any(name == "bizhawk" for name, _ in found): found.append(("bizhawk", bizhawk)) + _detect_by_executable(os_type, found) _detect_by_path(os_type, found) return found @@ -3060,6 +3089,13 @@ def _prompt_manual_platform(os_type: str) -> list[tuple[str, Path]]: return [(plat, dest)] +# Platforms with no install location of their own: the BIOS folder sits +# beside the executable, wherever the archive was extracted. +PORTABLE_ONLY = { + "bizhawk": "BizHawk reads the Firmware folder beside EmuHawk, and none was found.", +} + + def _default_dest(os_type: str, platform_name: str) -> Path: """Fall back path for a platform named on the command line but not found. @@ -3491,6 +3527,16 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: matched = [(n, p) for n, p in detected if n == args.platform] if matched: platforms = matched + elif args.platform in PORTABLE_ONLY: + # A portable emulator reads the folder beside its executable: + # any default would be a folder it never opens. + print( + f" Platform '{args.platform}' not found. {PORTABLE_ONLY[args.platform]} " + f"Name it with --dest, for example: --platform {args.platform} " + f"--dest '{os.sep}Firmware'", + file=sys.stderr, + ) + sys.exit(1) else: default_dest = _default_dest(os_type, args.platform) print( diff --git a/install.sh b/install.sh index 27e7e78d..b4be73f4 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="7fb25b2ee8d7cdab5c852431732cf8ef92e6e9a81d24aeae7cb24b34dd413660" +DEFAULT_INSTALL_SHA256="d86244b95c49d37d3950e0547fe13fa6df589dc21ba9868d46da4ab305522b3f" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/platforms/_registry.yml b/platforms/_registry.yml index ed2d4760..5537fa4b 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -575,12 +575,20 @@ platforms: target_source: null install: detect: + # A portable archive reading /Firmware wherever it was + # extracted (PathExtensions.cs DataDirectoryPath): found by its + # executable in the home folder or one level down, the newest first. - os: windows method: path_exists path: '%USERPROFILE%\BizHawk\Firmware' - os: linux - method: path_exists - path: $HOME/.config/BizHawk/Firmware + method: executable + file: [$HOME/BizHawk*/EmuHawkMono.sh, $HOME/*/BizHawk*/EmuHawkMono.sh] + bios_subdir: Firmware + - os: windows + method: executable + file: ['%USERPROFILE%\BizHawk*\EmuHawk.exe', '%USERPROFILE%\*\BizHawk*\EmuHawk.exe'] + bios_subdir: Firmware misterfpga: config: misterfpga.yml status: active diff --git a/scripts/packreadme.py b/scripts/packreadme.py index f630c0fe..1afc7c68 100644 --- a/scripts/packreadme.py +++ b/scripts/packreadme.py @@ -155,10 +155,9 @@ def _build_readme( ), "bizhawk": ( "INSTALLATION GUIDE\n\n" - " 1. Open your BizHawk installation folder\n" - " 2. Navigate to the Firmware subfolder:\n" - " - Windows: BizHawk\\Firmware\\\n" - " - Linux: ~/.config/BizHawk/Firmware/\n" + " 1. Open the folder you extracted BizHawk to, the one holding\n" + " EmuHawk.exe (Windows) or EmuHawkMono.sh (Linux)\n" + " 2. Navigate to its Firmware subfolder\n" " 3. Extract all files from this archive directly into your Firmware folder\n" " 4. In BizHawk: Config > Paths > Firmware should\n" " point to this folder\n\n" diff --git a/tests/test_install.py b/tests/test_install.py index e267e788..24bc7360 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -229,6 +229,10 @@ class TestDefaultsFollowTheRegistry(unittest.TestCase): elif rule.get("parse_key") == "system_directory": config = self._expand(rule["config"]) by_os.setdefault(rule["os"], set()).add(config.parent / "system") + if name in install.PORTABLE_ONLY: + # No folder of its own to fall back on: the run stops + # and asks for --dest (test_a_portable_platform_is_not_guessed). + continue for os_type, candidates in by_os.items(): with self.subTest(platform=name, os=os_type): self.assertIn(install._default_dest(os_type, name), candidates) @@ -1043,11 +1047,52 @@ class DetectionFollowsTheRegistry(unittest.TestCase): def test_a_home_layout_is_detected(self): home = self._home() (home / "RetroPie" / "BIOS").mkdir(parents=True) - (home / ".config" / "BizHawk" / "Firmware").mkdir(parents=True) found: list = [] install._detect_by_path("linux", found) self.assertIn(("retropie", home / "RetroPie" / "BIOS"), found) - self.assertIn(("bizhawk", home / ".config" / "BizHawk" / "Firmware"), found) + + def test_bizhawk_is_found_by_its_executable(self): + """~/.config/BizHawk/Firmware is a folder no build reads: the + archive extracted to ~/Games/BizHawk-2.11.1 reads the Firmware folder + beside EmuHawkMono.sh.""" + home = self._home() + extracted = home / "Games" / "BizHawk-2.11.1" + extracted.mkdir(parents=True) + (extracted / "EmuHawkMono.sh").write_text("#!/bin/sh\n") + found: list = [] + install._detect_by_executable("linux", found) + self.assertEqual(found, [("bizhawk", extracted / "Firmware")]) + self.assertNotIn("bizhawk", install._os_default_dests("linux")) + self.assertNotIn("bizhawk", install._os_default_dests("windows")) + + def test_a_portable_platform_is_not_guessed(self): + """--platform bizhawk with no install found wrote 924 files into + ~/.config/BizHawk/Firmware, then detected that folder next time.""" + home = self._home() + with unittest.mock.patch.object(sys, "argv", ["install.py", "--platform", "bizhawk"]), \ + unittest.mock.patch.object(install, "detect_os", return_value="linux"), \ + unittest.mock.patch.object(install, "detect_platforms", return_value=[]), \ + contextlib.redirect_stdout(io.StringIO()), \ + contextlib.redirect_stderr(io.StringIO()) as err, \ + self.assertRaises(SystemExit) as stop: + install.main() + self.assertEqual(stop.exception.code, 1) + self.assertIn("--dest", err.getvalue()) + self.assertFalse((home / ".config" / "BizHawk").exists()) + + def test_the_executable_table_is_the_registry(self): + import yaml # noqa: PLC0415 + + registry = yaml.safe_load( + (Path(__file__).resolve().parents[1] / "platforms" / "_registry.yml").read_text() + ) + rules = { + (rule["os"], name, tuple(rule["file"]), rule["bios_subdir"]) + for name, entry in registry["platforms"].items() + for rule in (entry.get("install") or {}).get("detect", []) + if rule["method"] == "executable" + } + self.assertEqual(set(install._EXE_RULES), rules) def test_a_windows_profile_path_is_detected(self): home = self._home() diff --git a/wiki/installer.md b/wiki/installer.md index f2caf848..64f1ad7e 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -285,7 +285,8 @@ not already found that way. | `~/.var/app/net.retrodeck.retrodeck` | RetroDECK | `~/retrodeck` | | `/romm/library/bios` | RomM | the same | | `~/RetroPie/BIOS` | RetroPie | the same | -| `%USERPROFILE%\BizHawk\Firmware` or `~/.config/BizHawk/Firmware` | BizHawk | the same | +| `%USERPROFILE%\BizHawk\Firmware` | BizHawk | the same | +| `EmuHawk.exe` or `EmuHawkMono.sh` in a `BizHawk*` folder of the home folder or one level down (`~/Games/BizHawk-2.11.1`), the newest | BizHawk | `Firmware` beside it | Desktop installs are read from their own configuration, and several can be found on one machine. @@ -338,9 +339,10 @@ destination falls back to the directory its detection looks for: `/storage/system` for Lakka, `~/retrodeck` for RetroDECK, `~/Emulation/bios` for EmuDeck, `/storage/roms/bios` for ROCKNIX, `/media/fat/games` for MiSTer, `~/RetroPie/BIOS` for RetroPie, `/romm/library/bios` for RomM, -`C:\RetroBat\bios` for RetroBat, `%USERPROFILE%\BizHawk\Firmware` (Windows) -or `~/.config/BizHawk/Firmware` for BizHawk, and RetroArch's own default -system directory for RetroArch. On Android a forced `--platform retroarch` falls back to +`C:\RetroBat\bios` for RetroBat, and RetroArch's own default system +directory for RetroArch. BizHawk has no such folder: it reads `Firmware` +beside its executable wherever the archive was extracted, so a forced +`--platform bizhawk` that finds no install stops and asks for `--dest`. On Android a forced `--platform retroarch` falls back to `/RetroArch/system` instead, a home directory there belonging to Termux rather than to the frontend. Pass `--dest` to say where instead of relying on that.