feat: add knulli, amberelec and emuelec platforms

Each OS publishes Batocera's systems dict (knulli-systems, and
batocera-systems at AmberELEC's newest prerelease commit and EmuELEC v4.8),
read with the shared parser. Cores come from each OS's own ES list.
AmberELEC and EmuELEC declare neogeo/ and ports/ beside bios/ from
/storage/roms, so their packs extract there and core_destination puts
core files under bios/.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 03:42:20 +02:00
1 parent 2fc62bd249
commit 2bdac466a9
18 files changed
+12933 -24

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
"""Exporter for AmberELEC's batocera-systems.
Batocera's script shape, so only the systems dict is rewritten. Two things
differ. Paths are relative to /storage/roms, where the list also names
neogeo/ beside bios/, and our destinations start there too. No entry carries
an emulator or core gate: coreExists() lets an ungated item through for
every build, and an addition is written the same way.
"""
from __future__ import annotations
from .base_exporter import BaseExporter
from .baseline import NativeFile
from .batocera_exporter import Exporter as BatoceraExporter
SOURCE_URL = (
"https://raw.githubusercontent.com/AmberELEC/AmberELEC/dev"
"/packages/amberelec/sources/scripts/batocera-systems"
)
class Exporter(BatoceraExporter):
"""Write AmberELEC's batocera-systems, corrected."""
PATH_PREFIX = ""
@classmethod
def writable(cls, fe: NativeFile, require: str = "") -> bool:
# The list gates nothing on a runner, so an addition needs none.
return BaseExporter.writable.__func__(cls, fe, require)
def _addition_keys(self, fe: NativeFile) -> list[tuple[str, str | None]] | None:
return []
@staticmethod
def platform_name() -> str:
return "amberelec"
@staticmethod
def file_modes() -> dict[str, int]:
# 100755 at a21975bf (git/trees).
return {"batocera-systems": 0o755}
@staticmethod
def native_sources() -> dict[str, str]:
return {"batocera-systems": SOURCE_URL}
+42 -11
View File
@@ -35,6 +35,27 @@ def _commented(text: str, position: int) -> bool:
return "#" in _STRING.sub("", text[line_start:position])
def _closing_line(text: str, start: int) -> int | None:
"""Index of the line holding the brace that closes `systems = {`.
Read from the parsed module, never executed. None when the script does
not parse or names no such assignment at line `start`.
"""
try:
tree = ast.parse(text)
except SyntaxError:
return None
for node in tree.body:
if (
isinstance(node, ast.Assign)
and node.lineno - 1 == start
and isinstance(node.value, ast.Dict)
and node.end_lineno is not None
):
return node.end_lineno - 1
return None
def _note_key(text: str) -> tuple[str, str]:
"""(file, zippedFile) of the item a line holds, ("", "") for none."""
path = re.search(r'"file"\s*:\s*"([^"]+)"', text)
@@ -55,6 +76,11 @@ class Exporter(BaseExporter):
# (duckstation-sa), which its checker does not know.
STANDALONE_RUNNERS = True
# What the script's paths add in front of a destination. Batocera's
# paths are relative to /userdata and our destinations to its bios/;
# AmberELEC's and EmuELEC's destinations already start at /storage/roms.
PATH_PREFIX = "bios/"
def __init__(self) -> None:
super().__init__()
# The emulator and core each system entry of the original is gated
@@ -200,8 +226,7 @@ class Exporter(BaseExporter):
alt = fe.native("alt_md5", "")
if alt:
item.append(f'"altmd5": "{alt}"')
declared = fe.native("native_path", "")
path = str(declared) if declared else f"bios/{fe.destination}"
path = self.declared_path(fe)
item.append(f'"file": "{path}"')
zipped = fe.native("zipped_file", "")
if zipped:
@@ -209,6 +234,12 @@ class Exporter(BaseExporter):
item.extend(self._runner_keys(originals.get(path)) if runner is None else runner)
return "{ " + ", ".join(item) + " }"
@classmethod
def declared_path(cls, fe: NativeFile) -> str:
"""The `file` the script names for an entry."""
declared = fe.native("native_path", "")
return str(declared) if declared else f"{cls.PATH_PREFIX}{fe.destination}"
@staticmethod
def _own_entry(system: NativeSystem, before: dict | None) -> dict:
"""The maintainer's entry for this system, out of the parsed pair."""
@@ -396,6 +427,9 @@ class Exporter(BaseExporter):
(i for i in range(start + 1, len(lines)) if lines[i].startswith("}")),
None,
)
if end is None:
# EmuELEC closes the dict with an indented brace.
end = _closing_line(original, start)
if end is None:
raise ValueError("batocera-systems: the systems dict is not closed")
return lines[: start + 1], lines[start + 1 : end], lines[end:]
@@ -520,14 +554,12 @@ class Exporter(BaseExporter):
content = produced[self.native_filename()]
issues: list[str] = []
block = content.split("\nsystems = {", 1)
if len(block) != 2:
return ["no systems dict in the output"]
end = block[1].find("\n}")
if end < 0:
return ["the systems dict is not closed"]
try:
exported = ast.literal_eval("{" + block[1][:end] + "\n}")
_head, body, _tail = self._split_original(content)
except ValueError as exc:
return [str(exc)]
try:
exported = ast.literal_eval("{" + "\n".join(body) + "\n}")
except (SyntaxError, ValueError) as exc:
return [f"the systems dict does not parse: {exc}"]
@@ -541,8 +573,7 @@ class Exporter(BaseExporter):
continue
declared = {bios.get("file", "") for bios in entry.get("biosFiles", [])}
for fe in files:
path = str(fe.native("native_path", "")) or f"bios/{fe.destination}"
if path not in declared:
if self.declared_path(fe) not in declared:
issues.append(f"absent: {system.native_id}/{fe.name}")
for native_id, entry in exported.items():
+32
View File
@@ -0,0 +1,32 @@
"""Exporter for EmuELEC's batocera-systems.
AmberELEC's case: paths relative to /storage/roms with ports/ and neogeo/
beside bios/, and no runner gate, since EmuELEC's checkBios never reads one.
The dict closes on an indented brace.
"""
from __future__ import annotations
from .amberelec_exporter import Exporter as AmberELECExporter
SOURCE_URL = (
"https://raw.githubusercontent.com/EmuELEC/EmuELEC/master"
"/packages/sx05re/emuelec/bin/batocera/batocera-systems"
)
class Exporter(AmberELECExporter):
"""Write EmuELEC's batocera-systems, corrected."""
@staticmethod
def platform_name() -> str:
return "emuelec"
@staticmethod
def file_modes() -> dict[str, int]:
# 100755 at v4.8 (git/trees).
return {"batocera-systems": 0o755}
@staticmethod
def native_sources() -> dict[str, str]:
return {"batocera-systems": SOURCE_URL}
+31
View File
@@ -0,0 +1,31 @@
"""Exporter for KNULLI's knulli-systems.
A fork of Batocera's script, checked the same way: es_systems names a
libretro core under `libretro` and a standalone by itself, and entries carry
Batocera's emulator and core gates.
"""
from __future__ import annotations
from .batocera_exporter import Exporter as BatoceraExporter
SOURCE_URL = (
"https://raw.githubusercontent.com/knulli-cfw/knulli-linux/knulli-main"
"/package/system/knulli-scripts/scripts/knulli-systems"
)
class Exporter(BatoceraExporter):
"""Write KNULLI's knulli-systems, corrected."""
@staticmethod
def platform_name() -> str:
return "knulli"
@staticmethod
def native_filename() -> str:
return "knulli-systems"
@staticmethod
def native_sources() -> dict[str, str]:
return {"knulli-systems": SOURCE_URL}