fix: place openmsx system ROMs through filepool

openMSX reads share/systemroms below OPENMSX_HOME or ~/.openMSX and
every filepool entry by SHA1. The profile paths drop RetroBat's
openmsx/ prefix and declare those directories and the filepool.

Batocera's config_directories reads the filepool lines its generator
writes, KNULLI reuses it. RetroBat records OPENMSX_HOME, RetroPie the
moveConfigDir of ~/.openMSX that its systemroms link lies in, and the
configure --prefix of xroar that its roms link lies in.

Batocera and KNULLI system ROMs move from openmsx/share/systemroms to
openmsx/, beside the files their own lists declare, RetroPie's to
BIOS/openmsx, the folder linked as share/systemroms.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 19:32:00 +01:00
1 parent cd38b9ed10
commit e6dbbe69ed
13 files changed
+394 -60

No files matched your search

+54 -2
View File
@@ -15,7 +15,8 @@ launcher looks for nothing".
from __future__ import annotations
import ast
from collections.abc import Callable
import re
from collections.abc import Callable, Iterable
from typing import Any
from .configgen_launchers import (
@@ -898,6 +899,56 @@ def environment_directories(cg: Configgen) -> list[Launcher]:
return launchers
# Generators that hand the emulator a /userdata/bios directory through its
# configuration: a line written to the file it reads (openMSX's script.tcl
# filepool entries) or a key saved to it.
CONFIG_DIRECTORY_GENERATORS = ("openmsx.openmsxGenerator",)
def config_directories(
cg: Configgen, modules: Iterable[str] = CONFIG_DIRECTORY_GENERATORS
) -> list[Launcher]:
"""file.write("<line naming a BIOS directory>") and settings.save(key, <BIOS directory>)."""
prefix = cg.bios.rstrip("/")
inside = re.compile(rf"{re.escape(prefix)}(?:/[^\s\"';]*)?")
launchers = []
for module in modules:
src = cg.generator(module)
constants = dict(cg.paths)
for node in src.tree.body:
name = _assigned_name(node)
value = _path_value(getattr(node, "value", None), constants)
if name is not None and value is not None:
constants[name] = value
found: list[tuple[str, str, ast.Call]] = []
for call in _calls(src.tree, attr="write"):
text = call.args[0] if len(call.args) == 1 else None
if isinstance(text, ast.Constant) and isinstance(text.value, str):
line = text.value.strip()
named = inside.search(line)
if named:
found.append((named.group(0), f"written: {line}", call))
for call in _calls(src.tree, attr="save"):
if len(call.args) != 2 or not isinstance(call.args[0], ast.Constant):
continue
value = _path_value(call.args[1], constants)
if value is None and isinstance(call.args[1], ast.Constant):
value = str(call.args[1].value)
if value is not None and (value.rstrip("/") == prefix or value.startswith(prefix + "/")):
found.append((value, str(call.args[0].value), call))
if not found:
src.fail("hands no /userdata/bios directory through its config")
launchers.extend(
Launcher(
emulator=cg.emulator(module), build="standalone", kind="passes_path",
names=[], search_dir=directory, passes_to=passes_to,
source_ref=src.ref([src.span(call)]),
)
for directory, passes_to, call in found
)
return launchers
# libretro core options whose value names the BIOS file the core opens.
FILE_OPTIONS = (("_o2em_options", "o2em_bios"), ("_neocd_options", "neocd_bios"))
@@ -952,7 +1003,8 @@ def core_file_options(cg: Configgen) -> list[Launcher]:
EXTRACTORS: tuple[Callable[[Configgen], list[Launcher]], ...] = (
duckstation, duckstation_legacy, hatari, xemu, melonds, mupen64plus,
x16emu, gsplus, lexaloffle, environment_directories, core_file_options,
x16emu, gsplus, lexaloffle, environment_directories, config_directories,
core_file_options,
)
+12 -2
View File
@@ -31,6 +31,7 @@ import yaml
try:
from .batocera_launchers import (
Configgen,
config_directories,
_path_value,
_render,
_system_branches,
@@ -53,6 +54,7 @@ try:
except ImportError:
from batocera_launchers import ( # type: ignore[no-redef]
Configgen,
config_directories,
_path_value,
_render,
_system_branches,
@@ -84,9 +86,18 @@ PICO8_INSTALLER = "board/fsoverlay/usr/bin/knulli-install-pico8"
ES_FEATURES = "package/emulationstation/knulli-es-system/es_features.yml"
# Batocera's extractors that read KNULLI's generators unchanged.
# The generators of Batocera's config_directories that KNULLI keeps in the
# same shape: openMSX's filepool lines.
CONFIG_DIRECTORY_GENERATORS = ("openmsx.openmsxGenerator",)
def knulli_config_directories(cg: Configgen) -> list[Launcher]:
return config_directories(cg, CONFIG_DIRECTORY_GENERATORS)
SHARED = (
duckstation, duckstation_legacy, hatari, xemu, melonds, mupen64plus, x16emu,
environment_directories,
environment_directories, knulli_config_directories,
)
@@ -365,7 +376,6 @@ GENERIC_SITES = (
GenericSites("amiberry/amiberryGenerator.py", "passes_path"),
GenericSites("fsuae/fsuaeGenerator.py", "passes_path"),
GenericSites("mame/mameGenerator.py", "passes_path"),
GenericSites("openmsx/openmsxGenerator.py", "passes_path"),
GenericSites("fpinball/fpinballGenerator.py", "passes_path"),
GenericSites(
"libretro/libretroRetroarchCustom.py", "passes_path", "libretro", emulator="retroarch"
+23
View File
@@ -1279,6 +1279,29 @@ def openmsx(sources: dict[str, Source]) -> list[Record]:
)
if not records:
raise LauncherPatternMissing(src.path, "biosFiles entries")
# OPENMSX_HOME is the user directory: openMSX reads share/systemroms below it.
home = src.find(
rf'string (\w+) = Path\.Combine\({BIOS}, "([^"]+)"\);', "OPENMSX_HOME folder"
)
handed = src.find(
rf'ret\.EnvironmentVariables\["(OPENMSX_HOME)"\] = {home.group(1)};',
"OPENMSX_HOME environment", home.end(),
)
records.append(
_record(
"openmsx",
"standalone",
"passes_path",
[],
source_ref(
(src.path, [src.lines(home.start(), home.end()),
src.lines(handed.start(), handed.end())])
),
search_dir=f"bios/{home.group(2)}",
passes_to=f"environment {handed.group(1)}",
fails_when_missing=False,
)
)
return records
+49
View File
@@ -525,6 +525,7 @@ class PackageReader:
self._links(links)
self._outside_bios(events)
self._working_directory(events)
self._reaching_links(events)
return self.records
@staticmethod
@@ -828,6 +829,54 @@ class PackageReader:
)
def _reaching_links(self, events: list[Event]) -> None:
"""The directories outside the BIOS one that a whole-directory link lies in.
openmsx's moveConfigDir "$home/.openMSX" "$configdir/msx/openmsx"
puts the emulator's directory where the systemroms link to BIOS/openmsx
lies, and xroar's ./configure --prefix="$md_inst" builds the program
whose share/xroar/roms links to $biosdir. Neither line names the BIOS
directory, and each says where the link is read from.
"""
suffix = " is a symlink to it"
linked = [
record.passes_to.removesuffix(suffix).rstrip("/")
for record in self.records
if record.kind == "links" and not record.names
and (record.passes_to or "").endswith(suffix)
]
if not linked:
return
def holds(directory: str) -> bool:
directory = directory.strip("\"'").rstrip("/")
return bool(directory) and any(
path == directory or path.startswith(directory + "/") for path in linked
)
for event in events:
if event.heredoc or not event.words or _mentions_bios(event.raw):
continue
command = event.words[0][0]
if command == "moveConfigDir" and len(event.words) >= 3:
link, real = event.words[1][0], event.words[2][0]
if holds(real):
self.record(
"links", [], [(event.path, event.line)],
search_dir=real, passes_to=f"{link}{suffix}",
fails_when_missing=False, depends_on_option=self._depends(event),
)
elif command.endswith("configure"):
for word in event.words[1:]:
prefix = word[0].strip("\"'")
if prefix.startswith("--prefix=") and holds(prefix[len("--prefix="):]):
self.record(
"passes_path", [], [(event.path, event.line)],
search_dir=prefix[len("--prefix="):].strip("\"'"),
passes_to="${prefix} of configure --prefix",
fails_when_missing=False, depends_on_option=self._depends(event),
)
def _working_directory(self, events: list[Event]) -> None:
"""The folder a generated launch script enters before it runs the emulator."""
if self.package.id not in WORKING_DIRECTORY: