fix: read muOS RetroArch from its own config

muOS runs RetroArch on a config copied from its default, which reads
/run/muos/storage/bios, bound to MUOS/bios on the second card when that
card holds one, and loads cores from /opt/muos/share/core (MustardOS/internal
f41f4a89, script/control/retroarch.sh:8-20, script/device/bind.sh:116-150,
retroarch.default.cfg:2806,3151). The installer wrote to /mnt/mmc/MUOS/bios
and --installed-cores found no core. The config now decides both.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 05:35:45 +02:00
1 parent 15cb52d551
commit edda080c59
6 files changed
+96 -6

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "67bba818ec05142e431155c27919e087d0d5aa1d5e148019ebae630f0dd26bdf"
$defaultInstallSha256 = "e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+28 -2
View File
@@ -779,7 +779,7 @@ _EMBEDDED_RULES: tuple[tuple[str, str, tuple[str, ...], str], ...] = (
("batocera", "file_exists", ("/etc/batocera-version",), "/userdata/bios"),
("recalbox", "file_exists", ("/recalbox/recalbox.version",), "/recalbox/share/bios"),
("recalbox", "file_exists", ("/usr/bin/recalbox-settings",), "/recalbox/share/bios"),
("retroarch", "file_exists", ("/opt/muos",), "/mnt/mmc/MUOS/bios"),
("retroarch", "file_exists", ("/opt/muos",), "/run/muos/storage/bios"),
("retroarch", "file_exists", ("/mnt/mmc/MUOS",), "/mnt/mmc/MUOS/bios"),
("retroarch", "file_exists", ("/home/ark", "/opt/system"), "/roms/bios"),
("retroarch", "file_exists", ("/mnt/vendor/bin/dmenu.bin",), "/mnt/mmc/bios"),
@@ -803,6 +803,30 @@ def _detect_embedded() -> list[tuple[str, Path]]:
return []
# muOS runs RetroArch on the config it copies from its default on first launch
# (internal f41f4a89, script/control/retroarch.sh:8-20). The default names
# /run/muos/storage/bios, a bind mount of MUOS/bios on the second card when
# that card holds one, else on the first (script/device/bind.sh:4,116-150),
# and loads cores from /opt/muos/share/core (retroarch.default.cfg:2806,3151).
MUOS_ROOT = Path("/opt/muos")
MUOS_RETROARCH_CONFIGS = (
MUOS_ROOT / "share" / "info" / "config" / "retroarch.cfg",
MUOS_ROOT / "share" / "emulator" / "retroarch" / "retroarch.default.cfg",
)
def _muos_system_dir() -> Path | None:
"""The system directory of muOS's RetroArch, read from its config, which
also records the config for finding the installed cores."""
if not MUOS_ROOT.is_dir():
return None
for cfg in MUOS_RETROARCH_CONFIGS:
system_dir = _parse_retroarch_system_dir(cfg)
if system_dir is not None:
return system_dir
return None
def _android_storage_root() -> Path:
"""Shared storage root, the parent RetroArch builds its defaults from."""
value = os.environ.get("EXTERNAL_STORAGE", "").strip()
@@ -1175,7 +1199,9 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
found.extend(_detect_android())
if os_type in ("linux", "wsl"):
found.extend(_detect_embedded())
embedded = _detect_embedded()
muos = _muos_system_dir() if embedded and embedded[0][0] == "retroarch" else None
found.extend([("retroarch", muos)] if muos else embedded)
# EmuDeck (Linux/SteamOS)
home = Path.home()
+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="67bba818ec05142e431155c27919e087d0d5aa1d5e148019ebae630f0dd26bdf"
DEFAULT_INSTALL_SHA256="e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+14 -1
View File
@@ -269,10 +269,23 @@ platforms:
parse_key: system_directory
# Handheld firmwares that run RetroArch as their frontend: muOS,
# ArkOS, and the stock Linux of Anbernic devices (dmenu).
# muOS copies its default config on first launch and RetroArch reads
# /run/muos/storage/bios, bound to MUOS/bios on the second card when
# it holds one, else the first (MustardOS/internal f41f4a89,
# script/control/retroarch.sh:8-20, script/device/bind.sh:4,116-150,
# share/emulator/retroarch/retroarch.default.cfg:2806,3151).
- os: linux
method: config_file
config: /opt/muos/share/info/config/retroarch.cfg
parse_key: system_directory
- os: linux
method: config_file
config: /opt/muos/share/emulator/retroarch/retroarch.default.cfg
parse_key: system_directory
- os: linux
method: file_exists
file: /opt/muos
bios_path: /mnt/mmc/MUOS/bios
bios_path: /run/muos/storage/bios
- os: linux
method: file_exists
file: /mnt/mmc/MUOS
+50
View File
@@ -3902,5 +3902,55 @@ class EmuDeckWindowsIsFoundAsItsScriptsFindIt(unittest.TestCase):
install._host_path("C:/Emulation/bios"))
class MuOsIsReadFromItsRetroArchConfig(unittest.TestCase):
"""muOS wrote to /mnt/mmc/MUOS/bios while RetroArch reads
/run/muos/storage/bios, bound to the second card when it holds MUOS/bios,
and --installed-cores found no core in /opt/muos/share/core."""
def _muos(self, *, user_config: bool) -> tuple[Path, Path]:
root = Path(_tempdir(self))
muos = root / "opt" / "muos"
system_dir, cores = root / "run" / "bios", root / "core"
cores.mkdir(parents=True)
configs = (muos / "share/info/config/retroarch.cfg",
muos / "share/emulator/retroarch/retroarch.default.cfg")
default = configs[1]
default.parent.mkdir(parents=True)
default.write_text(f'libretro_directory = "{cores}"\n'
f'system_directory = "{root / "default-bios"}"\n')
if user_config:
configs[0].parent.mkdir(parents=True)
configs[0].write_text(f'libretro_directory = "{cores}"\n'
f'system_directory = "{system_dir}"\n')
for patcher in (
unittest.mock.patch.object(install, "MUOS_ROOT", muos),
unittest.mock.patch.object(install, "MUOS_RETROARCH_CONFIGS", configs),
unittest.mock.patch.object(
install, "_detect_embedded",
lambda: [("retroarch", Path("/run/muos/storage/bios"))]),
unittest.mock.patch.dict(os.environ, {"HOME": str(root / "home")}),
unittest.mock.patch.dict(install._RETROARCH_CONFIGS, clear=True),
):
patcher.start()
self.addCleanup(patcher.stop)
return (system_dir if user_config else root / "default-bios"), cores
def test_the_users_config_decides(self):
system_dir, cores = self._muos(user_config=True)
found = install.detect_platforms("linux")
self.assertIn(("retroarch", system_dir), found)
self.assertNotIn(("retroarch", Path("/run/muos/storage/bios")), found)
self.assertIn(cores, install.core_dirs("retroarch", system_dir))
def test_before_the_first_launch_the_default_does(self):
system_dir, cores = self._muos(user_config=False)
self.assertIn(("retroarch", system_dir), install.detect_platforms("linux"))
self.assertIn(cores, install.core_dirs("retroarch", system_dir))
def test_without_a_config_the_bound_folder_is_used(self):
rule = next(r for r in install._EMBEDDED_RULES if r[2] == ("/opt/muos",))
self.assertEqual(rule[3], "/run/muos/storage/bios")
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -283,7 +283,8 @@ wins, since a machine is only one of them.
| `/etc/os-release` `ID=lakka` | Lakka | `/storage/system` |
| `/etc/batocera-version` | Batocera | `/userdata/bios` |
| `/recalbox/recalbox.version` or `/usr/bin/recalbox-settings` | Recalbox | `/recalbox/share/bios` |
| `/opt/muos` or `/mnt/mmc/MUOS/` | RetroArch | `/mnt/mmc/MUOS/bios` |
| `/opt/muos` | RetroArch (muOS) | `system_directory` of `/opt/muos/share/info/config/retroarch.cfg`, else of the default it is copied from: `/run/muos/storage/bios`, which muOS binds to `MUOS/bios` on the second card when that card holds one, else the first. Cores are read from that config's `libretro_directory` (`/opt/muos/share/core`) |
| `/mnt/mmc/MUOS/` | RetroArch | `/mnt/mmc/MUOS/bios` |
| `/home/ark` and `/opt/system` | RetroArch | `/roms/bios` |
| `/mnt/vendor/bin/dmenu.bin` | RetroArch | `/mnt/mmc/bios` |