diff --git a/install.ps1 b/install.ps1 index d0de691a..19f51c43 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 } diff --git a/install.py b/install.py index 6c8fbe6e..fdd3f467 100644 --- a/install.py +++ b/install.py @@ -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() diff --git a/install.sh b/install.sh index 41a56140..a9fb019f 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="67bba818ec05142e431155c27919e087d0d5aa1d5e148019ebae630f0dd26bdf" +DEFAULT_INSTALL_SHA256="e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/platforms/_registry.yml b/platforms/_registry.yml index 433339a8..819438d7 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -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 diff --git a/tests/test_install.py b/tests/test_install.py index 31dd80d5..44db4d6f 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -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() diff --git a/wiki/installer.md b/wiki/installer.md index a649467a..1937c0cb 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -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` |