fix: place standalone files in the linked folder

standalone_path is relative to the directories a standalone build
resolves it against, now declared per profile (standalone_directories).
A launcher record that links or passes a whole folder of the BIOS tree
under one of them gives that folder, and platform_layouts rebases the
standalone view once for the builder, the manifest, verify and slots
(launchers.standalone_root, common.standalone_destination).

Flycast on RetroPie and ROCKNIX reads bios/dc, on EmuDeck
bios/flycast/bios, on Batocera (FLYCAST_BIOS_PATH) and RetroBat
(Dreamcast.BiosPath) bios/dc. The ROCKNIX, Batocera and RetroBat
scrapers now record the patched data directory, the environment
variable and the setting that say so.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 12:39:03 +02:00
1 parent e96f6af583
commit d5a25190a3
14 files changed
+674 -7

No files matched your search

+41 -1
View File
@@ -647,6 +647,46 @@ def gsplus(cg: Configgen) -> list[Launcher]:
)]
# Generators that hand a directory of /userdata/bios to the emulator through
# its environment, with the module holding their path constants.
ENVIRONMENT_GENERATORS = (("flycast.flycastGenerator", "flycast/flycastPaths.py"),)
def environment_directories(cg: Configgen) -> list[Launcher]:
"""Command(..., env={KEY: <path under BIOS>}) in the generator."""
launchers = []
for module, paths_module in ENVIRONMENT_GENERATORS:
src = cg.generator(module)
constants = dict(cg.paths)
for node in cg.source(f"{GENERATORS}/{paths_module}").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
prefix = cg.bios.rstrip("/")
found = [
(key.value, value, key)
for call in _calls(src.tree, attr="Command")
for keyword in call.keywords
if keyword.arg == "env" and isinstance(keyword.value, ast.Dict)
for key, item in zip(keyword.value.keys, keyword.value.values)
if isinstance(key, ast.Constant)
and (value := _path_value(item, constants)) is not None
and (value == prefix or value.startswith(prefix + "/"))
]
if not found:
src.fail("the command environment names no /userdata/bios directory")
launchers.extend(
Launcher(
emulator=cg.emulator(module), build="standalone", kind="passes_path",
names=[], search_dir=value, passes_to=f"environment {key}",
source_ref=src.ref([(node.lineno, node.end_lineno)]),
)
for key, value, node 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"))
@@ -701,7 +741,7 @@ def core_file_options(cg: Configgen) -> list[Launcher]:
EXTRACTORS: tuple[Callable[[Configgen], list[Launcher]], ...] = (
duckstation, duckstation_legacy, hatari, xemu, melonds, mupen64plus,
x16emu, gsplus, core_file_options,
x16emu, gsplus, environment_directories, core_file_options,
)
+31
View File
@@ -22,6 +22,7 @@ SOURCE_FILES = (
f"{GENERATORS}/Ares.Generator.cs",
f"{GENERATORS}/Bizhawk.Generator.cs",
f"{GENERATORS}/Duckstation.Generator.cs",
f"{GENERATORS}/Flycast.Generator.cs",
f"{GENERATORS}/Hatari.Generator.cs",
f"{GENERATORS}/Jgenesis.Generator.cs",
f"{GENERATORS}/KegaFusion.Generator.cs",
@@ -1185,6 +1186,35 @@ def duckstation(sources: dict[str, Source]) -> list[Record]:
]
def flycast(sources: dict[str, Source]) -> list[Record]:
src = sources[f"{GENERATORS}/Flycast.Generator.cs"]
folder = src.find(
r'string biosPath = Path\.Combine\(' + BIOS + r', "(\w+)"\);', "biosPath under bios"
)
write = src.find(
r'ini\.WriteValue\("(\w+)", "(Dreamcast\.BiosPath)", biosPath\);',
"Dreamcast.BiosPath write",
folder.end(),
)
return [
_record(
"flycast",
"standalone",
"passes_path",
[],
source_ref(
(
src.path,
[src.lines(folder.start(), folder.end()), src.lines(write.start(), write.end())],
)
),
search_dir=f"bios/{folder.group(1)}",
passes_to=f"emu.cfg [{write.group(1)}] {write.group(2)} = bios/{folder.group(1)}",
fails_when_missing=False,
)
]
# Generators that write each BIOS path into the emulator's configuration
# only when the file exists: string x = Path.Combine(bios, ..., "name");
# if (File.Exists(x)) ini.WriteValue("section", "key", x).
@@ -1401,6 +1431,7 @@ EXTRACTORS: tuple[Callable[[dict[str, Source]], list[Record]], ...] = (
amiberry,
ares,
duckstation,
flycast,
guarded_writes,
hatari,
hatari_libretro,
+47
View File
@@ -612,6 +612,52 @@ def links(tree: RepoTree) -> list[Launcher]:
return records
# Packages whose build patch replaces the function an emulator resolves a
# directory with by a fixed path: ROCKNIX's Flycast returns
# /storage/roms/dreamcast/data as its data directory, which start_flycast.sh
# then links to the BIOS tree.
DIRECTORY_PATCHES = (
("flycast", f"{STANDALONE}/flycast-sa/patches/000-conf-dir.patch", "find_user_data_dir"),
)
def patched_directories(tree: RepoTree) -> list[Launcher]:
records = []
for emulator, path, function in DIRECTORY_PATCHES:
lines = tree.read(path).splitlines()
target = next(
(line[len("+++ b/"):] for line in lines if line.startswith("+++ b/")), None
)
start = next(
(n for n, line in enumerate(lines, 1) if re.search(rf"\b{function}\(\)", line)),
None,
)
returned = None
if target is not None and start is not None:
returned = next(
(
(n, match.group(1))
for n, line in enumerate(lines[start:], start + 1)
if (match := re.match(r'^\+\s*return "(/[^"]+)";', line))
),
None,
)
if returned is None:
raise LauncherPatternMissing(path, f"{function}() patched to return a fixed path")
number, directory = returned
records.append(Launcher(
emulator=emulator,
build="standalone",
kind="passes_path",
names=[],
source_ref=source_ref([(path, {start, number})]),
search_dir=directory.rstrip("/"),
passes_to=f"{function}() in {target} returns it",
fails_when_missing=False,
))
return records
def _existence_guard(script: Script, link: Link) -> int | None:
"""The line of a -e test of the BIOS-side path the link waits for, if any."""
last = max(link.lines)
@@ -639,6 +685,7 @@ EXTRACTORS: tuple[Callable[[RepoTree], list[Launcher]], ...] = (
duckstation,
aethersx2,
armsx2,
patched_directories,
links,
)