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.
This commit is contained in:
Abdessamad Derraz committed 2026-10-18 20:51:08 +02:00
1 parent fc47ac9f1f
commit 16f8b87ef4
7 files changed
+118 -18

No files matched your search

+1 -1
View File
@@ -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 }
+50 -4
View File
@@ -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 <exe folder>\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 <EmuHawk folder>/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 '<BizHawk folder>{os.sep}Firmware'",
file=sys.stderr,
)
sys.exit(1)
else:
default_dest = _default_dest(os_type, args.platform)
print(
+1 -1
View File
@@ -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() {
+10 -2
View File
@@ -575,12 +575,20 @@ platforms:
target_source: null
install:
detect:
# A portable archive reading <EmuHawk folder>/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
+3 -4
View File
@@ -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"
+47 -2
View File
@@ -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()
+6 -4
View File
@@ -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
`<shared storage>/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.