chore: merge launcher placement

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 18:23:46 +01:00
commit 56ed37c3f0
32 files changed
+1289 -160

No files matched your search

+117 -31
View File
@@ -1482,6 +1482,29 @@ def resolve_local_file(
system_dir = file_entry.get("system_dir")
if system_dir and dest_hint.startswith(f"{system_dir}/"):
dest_hint = dest_hint[len(system_dir) + 1:]
# A launcher that moves the layout (EmuDeck links azahar's sysdata to
# bios/azahar/keys) changes where the file goes, not the tail the
# collection indexes it under. Both are asked, and the stronger proof
# wins: a content or path match over a name, the destination on a tie.
layout = file_entry.get("layout_path")
if layout and layout != dest_hint:
plain = {k: v for k, v in file_entry.items() if k != "layout_path"}
answers = [
resolve_local_file(
plain, db, zip_contents, dest_hint=hint, _depth=_depth,
data_dir_registry=data_dir_registry,
)
for hint in (dest_hint, str(layout))
]
for found in answers:
if found[0] is not None and (
resolution_is_hash_exact(found[1]) or found[1] == "path_exact"
):
return found
for found in answers:
if found[0] is not None and found[1] not in ("not_found", "hash_mismatch"):
return found
return next((found for found in answers if found[0] is not None), answers[0])
# A name written with its directory is the destination when no path is
# given: gam4980/E.BIN resolved by its basename took another model's OS
# in verify --emulator and the emulator pack, while the platform pack,
@@ -2624,6 +2647,9 @@ HANDED_PREFIX = "handed:"
# Marks, in the same set, a libretro core and the system directory the
# platform hands it (``core_system_dirs``), as JSON.
CORE_DIR_PREFIX = "coredir:"
# Marks, in the same set, a file the platform's launcher executes
# (launchers.executed_files), as JSON.
EXECUTED_PREFIX = "exec:"
def core_system_dirs(
@@ -2689,14 +2715,19 @@ def standalone_set(config: dict) -> set[str]:
for core in entry.get("cores") or []
}
if names - {n for n in names if n.startswith(CORE_DIR_PREFIX)}:
from launchers import handed_directories # noqa: PLC0415
from launchers import executed_files, handed_directories # noqa: PLC0415
names |= {
HANDED_PREFIX + json.dumps(
[h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref]
[h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref,
h.roms_folder]
)
for h in handed_directories(config)
}
names |= {
EXECUTED_PREFIX + json.dumps([run.emulator, run.name, run.place, run.source_ref])
for run in executed_files(config)
}
return names
@@ -2724,38 +2755,81 @@ def _handed(standalone_cores: Iterable[str]) -> list:
for tag in standalone_cores:
if not tag.startswith(HANDED_PREFIX):
continue
emulator, folder, directory, handles, ref = json.loads(tag[len(HANDED_PREFIX):])
found.append(HandedDirectory(emulator, folder, directory, frozenset(handles), ref))
emulator, folder, directory, handles, ref, roms = json.loads(tag[len(HANDED_PREFIX):])
found.append(HandedDirectory(emulator, folder, directory, frozenset(handles), ref, roms))
return found
def standalone_destination(entry: dict, root: str | None) -> str:
"""Where a standalone build reads an entry, relative to the BIOS root.
def _executed(standalone_cores: Iterable[str]) -> list:
"""The files a standalone set's launchers execute, as launchers read them."""
from launchers import ExecutedFile # noqa: PLC0415
``standalone_path`` is relative to the directories the build resolves
it against (``standalone_directories``). A platform that hands one of
them a folder of its BIOS tree puts the file under that folder:
RetroPie and ROCKNIX give Flycast bios/dc, EmuDeck bios/flycast/bios.
An entry with no ``standalone_path`` keeps ``path``, the layout both
builds share.
found = []
for tag in standalone_cores:
if tag.startswith(EXECUTED_PREFIX):
emulator, name, place, ref = json.loads(tag[len(EXECUTED_PREFIX):])
found.append(ExecutedFile(emulator, name, tuple(place) if place else None, ref))
return found
# Why a standalone entry has no destination on a platform that relocates its
# layout: the manifest's omission reason, named as the installer words it.
OUTSIDE_BIOS_FOLDER = "outside_bios_folder"
# The entry root of a file read below the platform's roms directory.
ROMS_ROOT = "roms"
def standalone_layout_path(entry: dict, profile: dict) -> str | None:
"""The path of an entry relative to the directories its standalone build declares.
``standalone_path`` when the entry has one. A profile with no libretro
build has one layout, so its ``path`` is that layout: UAE4All2 reads
kickstarts/kick13.rom below its working directory.
"""
own = entry.get("standalone_path")
if own and root:
return f"{root}/{own}"
return own or entry.get("path") or entry.get("name", "")
if own:
return own
if "libretro" in str(profile.get("type", "")):
return None
return entry.get("path") or None
def standalone_root_for(
def standalone_destination(
entry: dict, profile: dict, bindings: dict[str, tuple[str, str]]
) -> tuple[str, str] | None:
"""Where a platform puts a standalone entry: (tree, path).
``bindings`` are the places the platform's launchers give the layout
(launchers.standalone_bindings): RetroPie and ROCKNIX give Flycast bios/dc,
EmuDeck bios/flycast/bios, RetroPie gives UAE4All2's kickstarts/ bios/amiga,
ROCKNIX runs PICO-8 from roms/pico-8. The tree is launchers.BIOS_TREE or
launchers.ROMS_TREE. None when no binding covers the entry.
"""
layout = standalone_layout_path(entry, profile)
if layout is None or not bindings:
return None
from launchers import bound_path # noqa: PLC0415
return bound_path(layout, bindings)
def standalone_bindings_for(
emu_name: str, profile: dict, standalone_cores: Iterable[str]
) -> str | None:
"""The folder a platform gives this emulator's standalone layout, or None."""
) -> dict[str, tuple[str, str]]:
"""The places a platform gives this emulator's standalone layout, by subfolder."""
declared = profile.get("standalone_directories") or []
if not declared:
return None
from launchers import standalone_root # noqa: PLC0415
return {}
from launchers import standalone_bindings # noqa: PLC0415
names = {emu_name, *(str(c) for c in profile.get("cores") or [])}
return standalone_root(names, declared, _handed(standalone_cores))
layout = [
path for f in profile.get("files") or []
if isinstance(f, dict) and (path := standalone_layout_path(f, profile))
]
return standalone_bindings(
names, declared, _handed(standalone_cores), _executed(standalone_cores), layout
)
def platform_layouts(
@@ -2774,19 +2848,31 @@ def platform_layouts(
return [(profile, False)]
return [(system_dir_view(profile, d), False) for d in directories]
standalone = profile
root = standalone_root_for(emu_name, profile, standalone_cores)
if root:
bindings = standalone_bindings_for(emu_name, profile, standalone_cores)
if bindings:
# The standalone view carries the platform's destination, so every
# reader of standalone_path (builder, manifest, verify, slots) places
# the file in the folder the platform hands the emulator.
standalone = {
**profile,
"files": [
{**f, "standalone_path": standalone_destination(f, root)}
if isinstance(f, dict) and f.get("standalone_path") else f
for f in profile.get("files") or []
],
}
# An entry of the layout no binding covers is read in a folder the
# platform keeps outside the BIOS tree: UAE4All2's kick.rom in
# RetroPie's $md_inst. It is marked, never left at the BIOS root
# where the emulator does not read it.
files = []
for f in profile.get("files") or []:
if not isinstance(f, dict) or standalone_layout_path(f, profile) is None:
files.append(f)
continue
placed = standalone_destination(f, profile, bindings)
if placed is None or placed[0] == "outside":
files.append({**f, "unplaced": OUTSIDE_BIOS_FOLDER})
continue
tree, path = placed
moved = {**f, "standalone_path": path,
"layout_path": standalone_layout_path(f, profile)}
# A file the launcher runs from the roms directory (ROCKNIX's
# roms/pico-8) is relative to it, and says so.
files.append({**moved, "root": ROMS_ROOT} if tree else moved)
standalone = {**profile, "files": files}
dual = {c.removeprefix(DUAL_MODE_PREFIX) for c in standalone_cores
if c.startswith(DUAL_MODE_PREFIX)}
if "libretro" in str(profile.get("type", "")) and dual & {