fix: detect Batocera and Knulli by build files

No Batocera build writes /etc/batocera-version: batocera-system.mk:21-22
(batocera.linux 38845725, unchanged since batocera-30) writes
/usr/share/batocera/batocera.version. Knulli (knulli-linux 57afa662) adds
OS_NAME=knulli to os-release, writes /usr/share/knulli/knulli.version and
removes /usr/share/batocera, and no Knulli source writes /etc/knulli-release.
Neither was detected. A Knulli board is named and not narrowed by
Batocera's per-board lists, and an unlisted board is named.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 05:44:27 +02:00
1 parent edda080c59
commit 89c533e9ba
6 files changed
+102 -15

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 = "e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049"
$defaultInstallSha256 = "5ed37d4dabc5a6b28a9093f38a25506b20ec2f49c3587692efd695b017d1caf4"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+42 -5
View File
@@ -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:
+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="e85a34efb84620b8c65858d209c9741a715f8ff6a61397f9ee161c0931a9a049"
DEFAULT_INSTALL_SHA256="5ed37d4dabc5a6b28a9093f38a25506b20ec2f49c3587692efd695b017d1caf4"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+12 -4
View File
@@ -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:
+40 -1
View File
@@ -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()
+6 -3
View File
@@ -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