diff --git a/install.ps1 b/install.ps1 index 19f51c43..eb5abf7d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049" + $defaultInstallSha256 = "5ed37d4dabc5a6b28a9093f38a25506b20ec2f49c3587692efd695b017d1caf4" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index fdd3f467..73bfd482 100644 --- a/install.py +++ b/install.py @@ -717,6 +717,28 @@ _BOARD_SOURCES: dict[str, tuple[str, str]] = { } +# A derivative installed under a platform's name, with its own board file. +# Knulli moves /usr/share/batocera to /usr/share/knulli (knulli-linux +# 57afa662, board/scripts/post-build-script.sh:264-278) and writes its board +# to knulli.arch (package/system/knulli-system/knulli-system.mk:98-99). Its +# packages are its own, so Batocera's per-board core lists do not apply. +_DERIVATIVE_BOARDS: dict[str, tuple[str, str]] = { + "batocera": ("Knulli", "/usr/share/knulli/knulli.arch"), +} + + +def derivative_board(platform: str) -> tuple[str, str] | None: + """(derivative, board) when a derivative of the platform names its board.""" + name, where = _DERIVATIVE_BOARDS.get(platform, ("", "")) + if not where: + return None + try: + board = Path(where).read_text(encoding="utf-8", errors="replace").strip().lower() + except OSError: + return None + return (name, board) if board else None + + def detect_board(platform: str) -> str | None: """The board this system was built for, or None where it names none.""" kind, where = _BOARD_SOURCES.get(platform, ("", "")) @@ -767,16 +789,22 @@ def _parse_ps1_var(path: Path, key: str) -> str | None: return None -# Embedded systems, first match wins: Knulli also carries the Batocera -# markers. The registry publishes the same rules as each manifest's -# `detect`, and a test holds the two equal. MiSTer is a path rule below. +# Embedded systems, first match wins. The registry publishes the same rules +# as each manifest's `detect`, and a test holds the two equal. MiSTer is a +# path rule below. _EMBEDDED_RULES: tuple[tuple[str, str, tuple[str, ...], str], ...] = ( # ROCKNIX writes OS_NAME and no ID since 2025 (scripts/image). Older # images wrote ID=rocknix. ("rocknix", "os_release", ("OS_NAME=rocknix", "ID=rocknix"), "/storage/roms/bios"), - ("batocera", "file_exists", ("/etc/knulli-release",), "/userdata/bios"), + # Knulli (knulli-linux 57afa662): OS_NAME in os-release + # (board/scripts/post-build-script.sh:233-236) and knulli.version + # (package/system/knulli-system/knulli-system.mk:98-102). + ("batocera", "os_release", ("OS_NAME=knulli",), "/userdata/bios"), + ("batocera", "file_exists", ("/usr/share/knulli/knulli.version",), "/userdata/bios"), ("lakka", "os_release", ("ID=lakka",), "/storage/system"), - ("batocera", "file_exists", ("/etc/batocera-version",), "/userdata/bios"), + # batocera.linux 38845725, batocera-system.mk:21-22 (the same lines in + # batocera-30): batocera.arch and batocera.version. + ("batocera", "file_exists", ("/usr/share/batocera/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",), "/run/muos/storage/bios"), @@ -4633,6 +4661,15 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: if board in known: target = board print(f" Board {board} detected (--every-board installs all boards)") + else: + print(f" Board {board} has no {PLATFORM_NAMES.get(plat_name, plat_name)} " + "core list: installing the files of every board") + else: + derived = derivative_board(plat_name) + if derived: + print(f" {derived[0]} board {derived[1]} detected. {derived[0]} " + "builds its own set of cores and publishes no core list " + "per board: installing the files of every board") if target: targets = target_lists.get(plat_name) if targets is None: diff --git a/install.sh b/install.sh index a9fb019f..094090f8 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="e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049" +DEFAULT_INSTALL_SHA256="5ed37d4dabc5a6b28a9093f38a25506b20ec2f49c3587692efd695b017d1caf4" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/platforms/_registry.yml b/platforms/_registry.yml index 819438d7..beb31913 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -313,14 +313,22 @@ platforms: target_source: https://github.com/batocera-linux/batocera.linux install: detect: - # Knulli is a Batocera derivative and carries its markers too. + # Knulli, a Batocera derivative (knulli-cfw/knulli-linux 57afa662): + # OS_NAME in os-release (board/scripts/post-build-script.sh:233-236), + # and knulli.version (package/system/knulli-system/knulli-system.mk:98-102). - os: linux - method: file_exists - file: /etc/knulli-release + method: os_release + match: [OS_NAME=knulli] bios_path: /userdata/bios - os: linux method: file_exists - file: /etc/batocera-version + file: /usr/share/knulli/knulli.version + bios_path: /userdata/bios + # batocera.linux 38845725, package/batocera/core/batocera-system/ + # batocera-system.mk:21-22 writes batocera.arch and batocera.version. + - os: linux + method: file_exists + file: /usr/share/batocera/batocera.version bios_path: /userdata/bios standalone_copies: *standalone_copies recalbox: diff --git a/tests/test_install.py b/tests/test_install.py index 44db4d6f..fa44f502 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1028,7 +1028,8 @@ class DetectionFollowsTheRegistry(unittest.TestCase): with unittest.mock.patch.object(install, "_parse_os_release", return_value={}), \ unittest.mock.patch.object( Path, "exists", - lambda p: str(p) in ("/etc/knulli-release", "/etc/batocera-version"), + lambda p: str(p) in ("/usr/share/knulli/knulli.version", + "/usr/share/batocera/batocera.version"), ): self.assertEqual(install._detect_embedded(), [("batocera", Path("/userdata/bios"))]) # What ROCKNIX writes since 2025: OS_NAME and HW_DEVICE, no ID. @@ -3952,5 +3953,43 @@ class MuOsIsReadFromItsRetroArchConfig(unittest.TestCase): self.assertEqual(rule[3], "/run/muos/storage/bios") +class BatoceraAndKnulliByWhatTheirBuildsWrite(unittest.TestCase): + """No Batocera build writes /etc/batocera-version (batocera-system.mk + writes /usr/share/batocera/batocera.version), and Knulli writes no + /etc/knulli-release and removes /usr/share/batocera: neither was + detected, and a Knulli board got every board's files without a word.""" + + def _detect(self, release: dict, files: tuple[str, ...]): + with unittest.mock.patch.object(install, "_parse_os_release", return_value=release), \ + unittest.mock.patch.object(Path, "exists", lambda p: str(p) in files): + return install._detect_embedded() + + def test_batocera_by_its_version_file(self): + self.assertEqual(self._detect({}, ("/usr/share/batocera/batocera.version",)), + [("batocera", Path("/userdata/bios"))]) + self.assertEqual(self._detect({}, ("/etc/batocera-version",)), []) + + def test_knulli_by_its_os_release_or_version_file(self): + self.assertEqual(self._detect({"OS_NAME": "knulli"}, ()), + [("batocera", Path("/userdata/bios"))]) + self.assertEqual(self._detect({}, ("/usr/share/knulli/knulli.version",)), + [("batocera", Path("/userdata/bios"))]) + + def test_a_knulli_board_is_named_and_not_narrowed(self): + board = Path(_tempdir(self)) / "knulli.arch" + board.write_text("a133") + with unittest.mock.patch.dict(install._DERIVATIVE_BOARDS, + {"batocera": ("Knulli", str(board))}): + self.assertEqual(install.derivative_board("batocera"), ("Knulli", "a133")) + self.assertIsNone(install.derivative_board("recalbox")) + + def test_the_run_says_why_every_board_is_installed(self): + source = (REPO_ROOT / "install.py").read_text(encoding="utf-8") + block = source[source.index("board = detect_board(plat_name)"):] + block = block[:block.index("if target:")] + self.assertIn("has no", block) + self.assertIn("derivative_board(plat_name)", block) + + if __name__ == "__main__": unittest.main() diff --git a/wiki/installer.md b/wiki/installer.md index 1937c0cb..190acfcc 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -279,9 +279,9 @@ wins, since a machine is only one of them. | Evidence | Platform | BIOS directory | |----------|----------|----------------| | `/etc/os-release` `OS_NAME=ROCKNIX` (or `ID=rocknix` on images before 2025) | ROCKNIX | `/storage/roms/bios` | -| `/etc/knulli-release` | Batocera | `/userdata/bios` | +| `/etc/os-release` `OS_NAME=knulli` or `/usr/share/knulli/knulli.version` | Batocera (Knulli) | `/userdata/bios` | | `/etc/os-release` `ID=lakka` | Lakka | `/storage/system` | -| `/etc/batocera-version` | Batocera | `/userdata/bios` | +| `/usr/share/batocera/batocera.version` | Batocera | `/userdata/bios` | | `/recalbox/recalbox.version` or `/usr/bin/recalbox-settings` | Recalbox | `/recalbox/share/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` | @@ -290,7 +290,10 @@ wins, since a machine is only one of them. Batocera, Recalbox, ROCKNIX and Lakka also name the board they were built for, and that board narrows the install unless `--target` or `--every-board` says -otherwise (see the options above). +otherwise (see the options above). A board with no core list of its own is +named and gets the files of every board. Knulli names its board in +`/usr/share/knulli/knulli.arch` but builds its own set of cores, so Batocera's +per-board lists are not applied to it and the run says so. Some platforms keep their BIOS in a fixed directory, and that directory is the evidence. They are looked for after the configurations below, for a platform