mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-12 04:53:52 -06:00
feat: bind standalone layouts per system and pool
A launcher record can name the systems it is written for, and then
replaces there what the records naming none hand: RetroBat passes XRoar
-rompath bios/coco for coco and bios/dragon otherwise. An entry is
launched for the systems it names and those the platform declares a
file of its name under. A {system} search_dir gives one directory per
platform system, a colon search path each of its directories.
standalone_pools names settings a program reads as a list of
directories, each searched like a layout subfolder (openMSX filepool,
XRoar rompath). Any entry serves: the one the platform list fills
comes first, then the first of the BIOS tree. A handle above a
declared directory lands it where its link points (configure
--prefix). Record order now survives the standalone set.
This commit is contained in:
1 parent
8633db0a22
commit
cd38b9ed10
5 files changed
+422
-74
No files matched your search
@@ -1035,6 +1035,13 @@
|
||||
},
|
||||
"description": "What the standalone build resolves standalone_path against, as its code names it: a directory ($XDG_DATA_HOME/flycast), a setting (Dreamcast.BiosPath), an environment variable or the function returning the directory. A platform launcher record that links or passes a folder of its BIOS tree under one of these puts the standalone files in that folder."
|
||||
},
|
||||
"standalone_pools": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Settings the standalone build reads as a list of directories it searches one after the other, each mapped to the subfolder of the layout (relative to standalone_directories, empty for the whole of it) its directories stand for: openMSX's filepool entries are searched like share/systemroms, XRoar's rompath entries hold the ROMs themselves. A file in any entry is read, so several platform records naming entries give the first one a pack can fill."
|
||||
},
|
||||
"prose_written_at": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$",
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Systems the package registers, under the platform's own names, in the order the script adds them"
|
||||
"description": "Systems the package registers, under the platform's own names, in the order the script adds them. On a record handing a directory, the systems it is written for: the record applies to an emulator launched for one of them, and replaces there what the records naming no system hand"
|
||||
},
|
||||
"slot": {
|
||||
"type": "string",
|
||||
@@ -270,7 +270,7 @@
|
||||
},
|
||||
"search_dir": {
|
||||
"type": "string",
|
||||
"description": "Directory the launcher looks in, as the code resolves it"
|
||||
"description": "Directory the launcher looks in, as the code resolves it. {system} stands for the platform system the emulator is launched for"
|
||||
},
|
||||
"passes_to": {
|
||||
"type": "string",
|
||||
|
||||
+84
-14
@@ -6,7 +6,7 @@ and file resolution - eliminates DRY violations across scripts.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from collections import Counter
|
||||
|
||||
import argparse
|
||||
@@ -2685,6 +2685,10 @@ CORE_DIR_PREFIX = "coredir:"
|
||||
# Marks, in the same set, a file the platform's launcher executes
|
||||
# (launchers.executed_files), as JSON.
|
||||
EXECUTED_PREFIX = "exec:"
|
||||
# Marks, in the same set, a file name the platform declares under one of the
|
||||
# systems a launcher record is written for, as JSON [system, name]: it says
|
||||
# which launch reads a profile entry of that name.
|
||||
SYSTEM_FILE_PREFIX = "sysfile:"
|
||||
|
||||
|
||||
def core_system_dirs(
|
||||
@@ -2752,12 +2756,22 @@ def standalone_set(config: dict) -> set[str]:
|
||||
if names - {n for n in names if n.startswith(CORE_DIR_PREFIX)}:
|
||||
from launchers import executed_files, handed_directories # noqa: PLC0415
|
||||
|
||||
handed = handed_directories(config)
|
||||
# The record's position travels too: a search list is read in order.
|
||||
names |= {
|
||||
HANDED_PREFIX + json.dumps(
|
||||
[h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref,
|
||||
h.roms_folder]
|
||||
[position, h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref,
|
||||
h.roms_folder, sorted(h.systems), h.holds_declared]
|
||||
)
|
||||
for h in handed_directories(config)
|
||||
for position, h in enumerate(handed)
|
||||
}
|
||||
named = {system for h in handed for system in h.systems}
|
||||
names |= {
|
||||
SYSTEM_FILE_PREFIX + json.dumps([slug, str(f["name"]).lower()])
|
||||
for slug, system in (config.get("systems") or {}).items()
|
||||
if slug in named
|
||||
for f in system.get("files") or []
|
||||
if f.get("name")
|
||||
}
|
||||
names |= {
|
||||
EXECUTED_PREFIX + json.dumps([run.emulator, run.name, run.place, run.source_ref])
|
||||
@@ -2790,9 +2804,39 @@ def _handed(standalone_cores: Iterable[str]) -> list:
|
||||
for tag in standalone_cores:
|
||||
if not tag.startswith(HANDED_PREFIX):
|
||||
continue
|
||||
emulator, folder, directory, handles, ref, roms = json.loads(tag[len(HANDED_PREFIX):])
|
||||
found.append(HandedDirectory(emulator, folder, directory, frozenset(handles), ref, roms))
|
||||
return found
|
||||
position, emulator, folder, directory, handles, ref, roms, systems, held = json.loads(
|
||||
tag[len(HANDED_PREFIX):]
|
||||
)
|
||||
found.append(((position, tag), HandedDirectory(
|
||||
emulator, folder, directory, frozenset(handles), ref, roms, frozenset(systems), held
|
||||
)))
|
||||
return [h for _key, h in sorted(found, key=lambda pair: pair[0])]
|
||||
|
||||
|
||||
def system_file_index(standalone_cores: Iterable[str]) -> dict[str, set[str]]:
|
||||
"""File name, lower case -> the platform systems declaring it (SYSTEM_FILE_PREFIX)."""
|
||||
index: dict[str, set[str]] = {}
|
||||
for tag in standalone_cores:
|
||||
if tag.startswith(SYSTEM_FILE_PREFIX):
|
||||
slug, name = json.loads(tag[len(SYSTEM_FILE_PREFIX):])
|
||||
index.setdefault(name, set()).add(slug)
|
||||
return index
|
||||
|
||||
|
||||
def entry_launch_systems(entry: dict, index: Mapping[str, set[str]]) -> frozenset[str]:
|
||||
"""The platform systems, by our ids, that launch the emulator for an entry.
|
||||
|
||||
The systems the entry names, and those under which the platform declares
|
||||
a file of one of its names (``index``, system_file_index): Recalbox lists
|
||||
XRoar's coco3.rom under trs80coco, which our coco is.
|
||||
"""
|
||||
systems = {str(s) for s in entry.get("systems") or []}
|
||||
if entry.get("system"):
|
||||
systems.add(str(entry["system"]))
|
||||
for name in [entry.get("name"), *(entry.get("aliases") or [])]:
|
||||
if name:
|
||||
systems |= index.get(str(name).lower(), set())
|
||||
return frozenset(systems)
|
||||
|
||||
|
||||
def _executed(standalone_cores: Iterable[str]) -> list:
|
||||
@@ -2853,11 +2897,16 @@ def standalone_destination(
|
||||
|
||||
|
||||
def standalone_bindings_for(
|
||||
emu_name: str, profile: dict, standalone_cores: Iterable[str]
|
||||
emu_name: str, profile: dict, standalone_cores: Iterable[str],
|
||||
systems: Iterable[str] = (),
|
||||
) -> dict[str, tuple[str, str]]:
|
||||
"""The places a platform gives this emulator's standalone layout, by subfolder."""
|
||||
"""The places a platform gives this emulator's standalone layout, by subfolder.
|
||||
|
||||
``systems`` are those the entry is launched for (entry_launch_systems).
|
||||
"""
|
||||
declared = profile.get("standalone_directories") or []
|
||||
if not declared:
|
||||
pools = profile.get("standalone_pools") or {}
|
||||
if not declared and not pools:
|
||||
return {}
|
||||
from launchers import standalone_bindings # noqa: PLC0415
|
||||
|
||||
@@ -2867,7 +2916,8 @@ def standalone_bindings_for(
|
||||
if isinstance(f, dict) and (path := standalone_layout_path(f, profile))
|
||||
]
|
||||
return standalone_bindings(
|
||||
names, declared, _handed(standalone_cores), _executed(standalone_cores), layout
|
||||
names, declared, _handed(standalone_cores), _executed(standalone_cores), layout,
|
||||
pools, systems,
|
||||
)
|
||||
|
||||
|
||||
@@ -2887,8 +2937,28 @@ def platform_layouts(
|
||||
return [(profile, False)]
|
||||
return [(system_dir_view(profile, d), False) for d in directories]
|
||||
standalone = profile
|
||||
bindings = standalone_bindings_for(emu_name, profile, standalone_cores)
|
||||
if bindings:
|
||||
# Bindings can depend on the system an entry is launched for, so each
|
||||
# set of launch systems gets its own.
|
||||
by_systems: dict[frozenset[str], dict[str, tuple[str, str]]] = {}
|
||||
names = {emu_name, *(str(c) for c in profile.get("cores") or [])}
|
||||
per_system = bool(
|
||||
profile.get("standalone_directories") or profile.get("standalone_pools")
|
||||
) and any(h.systems and h.emulator in names for h in _handed(standalone_cores))
|
||||
index = system_file_index(standalone_cores) if per_system else {}
|
||||
|
||||
def bindings_of(entry: dict) -> dict[str, tuple[str, str]]:
|
||||
systems = entry_launch_systems(entry, index) if per_system else frozenset()
|
||||
if systems not in by_systems:
|
||||
by_systems[systems] = standalone_bindings_for(
|
||||
emu_name, profile, standalone_cores, systems
|
||||
)
|
||||
return by_systems[systems]
|
||||
|
||||
layout_entries = [
|
||||
f for f in profile.get("files") or []
|
||||
if isinstance(f, dict) and standalone_layout_path(f, profile) is not None
|
||||
]
|
||||
if any(bindings_of(f) for f in layout_entries):
|
||||
# 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.
|
||||
@@ -2901,7 +2971,7 @@ def platform_layouts(
|
||||
if not isinstance(f, dict) or standalone_layout_path(f, profile) is None:
|
||||
files.append(f)
|
||||
continue
|
||||
placed = standalone_destination(f, profile, bindings)
|
||||
placed = standalone_destination(f, profile, bindings_of(f))
|
||||
if placed is None or placed[0] == "outside":
|
||||
files.append({**f, "unplaced": OUTSIDE_BIOS_FOLDER})
|
||||
continue
|
||||
|
||||
+199
-58
@@ -17,7 +17,7 @@ under the launcher's name, a file the core accepts by content.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
|
||||
from common import declared_root, name_match_size_ok, resolve_local_file, sanitize_pack_path
|
||||
@@ -383,6 +383,11 @@ class HandedDirectory:
|
||||
handles: frozenset[str]
|
||||
source_ref: str
|
||||
roms_folder: str | None = None
|
||||
# The platform systems, by our ids, the record applies to. Empty when
|
||||
# the launcher hands the directory whatever the system.
|
||||
systems: frozenset[str] = frozenset()
|
||||
# Whether the platform's own list declares files in the folder.
|
||||
holds_declared: bool = False
|
||||
|
||||
def place(self) -> Place | None:
|
||||
"""The folder as a place, None when it lies outside both roots."""
|
||||
@@ -393,14 +398,55 @@ class HandedDirectory:
|
||||
return None
|
||||
|
||||
|
||||
# The placeholder a launcher record writes for the system it is launched
|
||||
# for: Recalbox passes -rompath os.path.join(BIOS, args.system) to XRoar.
|
||||
SYSTEM_PLACEHOLDER = "{system}"
|
||||
|
||||
|
||||
def native_systems(config: dict) -> dict[str, str]:
|
||||
"""The platform's own system names, each to our id for it.
|
||||
|
||||
A system states its name (native_id) or its files do (native_system).
|
||||
Without either, our id is the platform's name.
|
||||
"""
|
||||
found: dict[str, str] = {}
|
||||
for slug, system in (config.get("systems") or {}).items():
|
||||
names = {str(system["native_id"])} if system.get("native_id") else {
|
||||
str(f["native_system"]) for f in system.get("files") or [] if f.get("native_system")
|
||||
}
|
||||
for name in names or {slug}:
|
||||
found.setdefault(name, slug)
|
||||
return found
|
||||
|
||||
|
||||
def _listed_directories(raw: str) -> list[str] | None:
|
||||
"""The directories of a colon-separated search path, None for one directory.
|
||||
|
||||
XRoar's rompath is such a list (RetroDECK writes four directories), each
|
||||
entry a path or a variable spelling one. A Windows drive letter or a
|
||||
backslash means a single path.
|
||||
"""
|
||||
if ":" not in raw or "\\" in raw or re.match(r"^[A-Za-z]:", raw):
|
||||
return None
|
||||
parts = [p.strip() for p in raw.split(":")]
|
||||
if all(p and p[0] in "/$~" and not re.search(r"[\s,;]", p) for p in parts):
|
||||
return parts
|
||||
return None
|
||||
|
||||
|
||||
def handed_directories(config: dict) -> list[HandedDirectory]:
|
||||
"""Every whole directory a platform links or passes to a standalone build.
|
||||
|
||||
A record naming files (gpsp's gba_bios.bin) or one slot of a directory
|
||||
(RetroDECK's GC/EUR) hands over no whole directory, and a search_dir
|
||||
holding several directories names no single one.
|
||||
describing several directories in prose names no single one. A search
|
||||
path (XRoar's rompath) gives each of its directories, in order. A record
|
||||
naming systems applies to those systems only, and one whose search_dir
|
||||
holds {system} gives one directory per system the platform declares.
|
||||
"""
|
||||
base = str(config.get("base_destination") or "bios")
|
||||
natives = native_systems(config)
|
||||
declared = {d.lower() for d in declared_destinations(config)}
|
||||
found: list[HandedDirectory] = []
|
||||
for record in config.get("launchers") or []:
|
||||
if record.get("build") != "standalone" or record.get("kind") not in HANDED_KINDS:
|
||||
@@ -408,30 +454,48 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
|
||||
if record.get("names") or record.get("slot"):
|
||||
continue
|
||||
raw = str(record.get("search_dir") or "").strip()
|
||||
if not raw or re.search(r"[\s,:;]", raw):
|
||||
if not raw:
|
||||
continue
|
||||
directory = normalize_handle(raw)
|
||||
passes_to = str(record.get("passes_to") or "")
|
||||
if passes_to.startswith(WORKING_DIRECTORY_PASSED):
|
||||
# The program runs from the directory: what its code reads
|
||||
# through getcwd(), or relative to it, is below it.
|
||||
handles = frozenset({WORKING_DIRECTORY})
|
||||
named = frozenset(natives.get(str(s), str(s)) for s in record.get("systems") or [])
|
||||
if SYSTEM_PLACEHOLDER in raw:
|
||||
variants = [
|
||||
(raw.replace(SYSTEM_PLACEHOLDER, native), frozenset({slug}))
|
||||
for native, slug in sorted(natives.items())
|
||||
if not named or slug in named
|
||||
]
|
||||
else:
|
||||
handles = frozenset(
|
||||
handle
|
||||
for token in passes_to.split()
|
||||
if (handle := normalize_handle(token)) and handle != directory
|
||||
)
|
||||
folder = _folder_in_bios(raw, base)
|
||||
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
|
||||
found.append(HandedDirectory(
|
||||
emulator=emulator,
|
||||
folder=folder,
|
||||
directory=directory,
|
||||
handles=handles,
|
||||
source_ref=str(record.get("source_ref", "")),
|
||||
roms_folder=None if folder is not None else folder_in_roms(raw, config),
|
||||
))
|
||||
variants = [(raw, named)]
|
||||
passes_to = str(record.get("passes_to") or "")
|
||||
for spelled, systems in variants:
|
||||
parts = _listed_directories(spelled) or [spelled]
|
||||
if any(re.search(r"[\s,:;]", part) for part in parts):
|
||||
continue
|
||||
for part in parts:
|
||||
directory = normalize_handle(part)
|
||||
if passes_to.startswith(WORKING_DIRECTORY_PASSED):
|
||||
# The program runs from the directory: what its code
|
||||
# reads through getcwd(), or relative to it, is below it.
|
||||
handles = frozenset({WORKING_DIRECTORY})
|
||||
else:
|
||||
handles = frozenset(
|
||||
handle
|
||||
for token in passes_to.split()
|
||||
if (handle := normalize_handle(token)) and handle != directory
|
||||
)
|
||||
folder = _folder_in_bios(part, base)
|
||||
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
|
||||
found.append(HandedDirectory(
|
||||
emulator=emulator,
|
||||
folder=folder,
|
||||
directory=directory,
|
||||
handles=handles,
|
||||
source_ref=str(record.get("source_ref", "")),
|
||||
roms_folder=None if folder is not None else folder_in_roms(part, config),
|
||||
systems=systems,
|
||||
holds_declared=bool(folder) and any(
|
||||
d.startswith(f"{folder.lower()}/") for d in declared
|
||||
),
|
||||
))
|
||||
return found
|
||||
|
||||
|
||||
@@ -515,17 +579,113 @@ def executed_files(config: dict) -> list[ExecutedFile]:
|
||||
return found
|
||||
|
||||
|
||||
def _below(handle: str, directory: str) -> str | None:
|
||||
"""``handle`` relative to ``directory``, None when it is not inside it."""
|
||||
if handle == directory:
|
||||
return ""
|
||||
if handle.startswith(directory + "/"):
|
||||
return handle[len(directory) + 1:]
|
||||
return None
|
||||
|
||||
|
||||
def _join(*parts: str) -> str:
|
||||
return "/".join(p for p in parts if p)
|
||||
|
||||
|
||||
def _record_places(
|
||||
record: HandedDirectory, inner: str, records: list[HandedDirectory]
|
||||
) -> list[tuple[str, Place]]:
|
||||
"""(subfolder, place) for the directory ``inner`` below what a record hands.
|
||||
|
||||
A record handing a folder of a tree gives it directly. One handing a
|
||||
directory outside the trees is followed through the links that put a
|
||||
folder at that directory, below it or above it.
|
||||
"""
|
||||
if record.folder is not None:
|
||||
return [("", (BIOS_TREE, _join(record.folder, inner)))]
|
||||
base = _join(record.directory, inner)
|
||||
found: list[tuple[str, Place]] = []
|
||||
for link in records:
|
||||
target = link.place()
|
||||
if target is None:
|
||||
continue
|
||||
for handle in link.handles:
|
||||
rest = _below(handle, base)
|
||||
if rest is not None:
|
||||
found.append((rest, target))
|
||||
continue
|
||||
up = _below(base, handle) if inner else None
|
||||
if up:
|
||||
found.append(("", (target[0], _join(target[1], up))))
|
||||
if not found and record.roms_folder is not None:
|
||||
found.append(("", (ROMS_TREE, _join(record.roms_folder, inner))))
|
||||
return found
|
||||
|
||||
|
||||
def _bind(
|
||||
records: list[HandedDirectory],
|
||||
links: list[HandedDirectory],
|
||||
wanted: set[str],
|
||||
pools: Mapping[str, str],
|
||||
) -> dict[str, set[Place]]:
|
||||
"""The places each subfolder of the layout is given by ``records``.
|
||||
|
||||
A record whose handle is a declared directory, or lies below one, gives
|
||||
that subfolder. One whose handle lies above a declared directory gives
|
||||
the declared directory where it lands: RetroPie configures XRoar with
|
||||
--prefix=$md_inst, so ${prefix}/share/xroar/roms is $md_inst/share/xroar/roms.
|
||||
``links`` are the records followed from a directory outside the trees.
|
||||
|
||||
A handle in ``pools`` names a list of directories the program searches
|
||||
one after the other: openMSX's filepool, XRoar's rompath. A file in any
|
||||
of them is read, so the pool gives one: the directory the platform's own
|
||||
list already fills (Batocera's bios/openmsx beside bios/Machines), else
|
||||
the first of the BIOS tree, else the first a pack can fill.
|
||||
"""
|
||||
found: dict[str, set[Place]] = {}
|
||||
pooled: dict[str, list[tuple[bool, Place]]] = {}
|
||||
for record in records:
|
||||
for handle in record.handles:
|
||||
if handle in pools:
|
||||
for rest, place in _record_places(record, "", links):
|
||||
pooled.setdefault(_join(pools[handle], rest), []).append(
|
||||
(record.holds_declared, place)
|
||||
)
|
||||
continue
|
||||
for directory in wanted:
|
||||
sub = _below(handle, directory)
|
||||
inner = ""
|
||||
if sub is None:
|
||||
up = _below(directory, handle)
|
||||
if not up:
|
||||
continue
|
||||
sub, inner = "", up
|
||||
for rest, place in _record_places(record, inner, links):
|
||||
found.setdefault(_join(sub, rest), set()).add(place)
|
||||
for sub, entries in pooled.items():
|
||||
# The directory the platform's own list fills, else the first of the
|
||||
# BIOS tree, else the first a pack fills.
|
||||
filled = [p for held, p in entries if held]
|
||||
in_bios = [p for _held, p in entries if p[0] == BIOS_TREE]
|
||||
found.setdefault(sub, set()).add((filled or in_bios or [p for _h, p in entries])[0])
|
||||
return found
|
||||
|
||||
|
||||
def standalone_bindings(
|
||||
names: Iterable[str],
|
||||
declared: Iterable[str],
|
||||
handed: Iterable[HandedDirectory],
|
||||
executed: Iterable[ExecutedFile] = (),
|
||||
layout: Iterable[str] = (),
|
||||
pools: Mapping[str, str] | None = None,
|
||||
systems: Iterable[str] = (),
|
||||
) -> dict[str, Place]:
|
||||
"""The folders a platform gives a standalone build's layout.
|
||||
|
||||
``declared`` are the directories the build resolves its paths against,
|
||||
as its profile names them (``standalone_directories``). The result maps
|
||||
as its profile names them (``standalone_directories``), and ``pools``
|
||||
the settings it reads as search lists, each to the subfolder of the
|
||||
layout its directories stand for (``standalone_pools``). The result maps
|
||||
a subfolder of that layout ("" for the whole of it) to the place, a
|
||||
folder of the BIOS tree or of the roms directory, the emulator reads it
|
||||
from.
|
||||
@@ -538,6 +698,11 @@ def standalone_bindings(
|
||||
and RetroPie starts UAE4All2 in $md_inst, whose kickstarts folder it
|
||||
links to bios/amiga.
|
||||
|
||||
``systems`` are the platform systems, by our ids, an entry is launched
|
||||
for. A record naming systems replaces, for those systems, what the
|
||||
records naming none give: RetroBat passes XRoar -rompath bios/coco for
|
||||
coco and bios/dragon for every other system.
|
||||
|
||||
A profile whose program reads its files beside its executable declares
|
||||
EXECUTABLE_DIRECTORY, and ``layout`` holds its paths: a launcher that
|
||||
executes one of them gives the folder holding it. Batocera runs
|
||||
@@ -547,42 +712,18 @@ def standalone_bindings(
|
||||
A subfolder two records give two places is left out.
|
||||
"""
|
||||
names = set(names)
|
||||
wanted = {normalize_handle(d) for d in declared}
|
||||
if not wanted:
|
||||
pools = {normalize_handle(k): str(v).strip("/") for k, v in (pools or {}).items()}
|
||||
wanted = {normalize_handle(d) for d in declared} - set(pools)
|
||||
if not wanted and not pools:
|
||||
return {}
|
||||
systems = set(systems)
|
||||
records = [h for h in handed if h.emulator in names]
|
||||
generic = [h for h in records if not h.systems]
|
||||
specific = [h for h in records if h.systems & systems]
|
||||
|
||||
def below(handle: str, directory: str) -> str | None:
|
||||
if handle == directory:
|
||||
return ""
|
||||
if handle.startswith(directory + "/"):
|
||||
return handle[len(directory) + 1:]
|
||||
return None
|
||||
|
||||
found: dict[str, set[Place]] = {}
|
||||
for record in records:
|
||||
subs = {
|
||||
sub for handle in record.handles for directory in wanted
|
||||
if (sub := below(handle, directory)) is not None
|
||||
}
|
||||
for sub in subs:
|
||||
if record.folder is not None:
|
||||
found.setdefault(sub, set()).add((BIOS_TREE, record.folder))
|
||||
continue
|
||||
# A link into the BIOS tree comes first: the pack fills that.
|
||||
linked = False
|
||||
for link in records:
|
||||
target = link.place()
|
||||
if target is None:
|
||||
continue
|
||||
for handle in link.handles:
|
||||
rest = below(handle, record.directory)
|
||||
if rest is not None:
|
||||
key = "/".join(p for p in (sub, rest) if p)
|
||||
found.setdefault(key, set()).add(target)
|
||||
linked = True
|
||||
if not linked and record.roms_folder is not None:
|
||||
found.setdefault(sub, set()).add((ROMS_TREE, record.roms_folder))
|
||||
found = _bind(generic, generic, wanted, pools)
|
||||
# A link is followed whatever system it was written for.
|
||||
found.update(_bind(specific, specific + generic, wanted, pools))
|
||||
if EXECUTABLE_DIRECTORY in wanted:
|
||||
paths = list(layout)
|
||||
for run in executed:
|
||||
|
||||
@@ -23,12 +23,14 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from common import ( # noqa: E402
|
||||
entry_launch_systems,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
platform_layouts,
|
||||
standalone_bindings_for,
|
||||
standalone_destination,
|
||||
standalone_set,
|
||||
system_file_index,
|
||||
)
|
||||
from launchers import ( # noqa: E402
|
||||
bound_path,
|
||||
@@ -156,6 +158,133 @@ class Root(unittest.TestCase):
|
||||
))
|
||||
|
||||
|
||||
def bind(cfg: dict, declared: list[str], pools: dict[str, str] | None = None,
|
||||
systems: tuple[str, ...] = ()) -> dict[str, tuple[str, str]]:
|
||||
return standalone_bindings(
|
||||
{"emu"}, declared, handed_directories(cfg), pools=pools, systems=systems
|
||||
)
|
||||
|
||||
|
||||
class SearchLists(unittest.TestCase):
|
||||
"""A setting read as a list of directories, every one searched."""
|
||||
|
||||
POOLS = {"filepool": "share/systemroms"}
|
||||
|
||||
def filepool(self, folder: str, position: int) -> dict:
|
||||
return passes(
|
||||
"emu", f"/userdata/bios/{folder}",
|
||||
f"written: filepool add -path /userdata/bios/{folder} -types system_rom -position {position}",
|
||||
)
|
||||
|
||||
def test_two_pool_entries_give_the_first_one(self) -> None:
|
||||
# Batocera adds bios/Machines and bios/openmsx to openMSX's filepool.
|
||||
cfg = config(self.filepool("Machines", 1), self.filepool("openmsx", 2))
|
||||
self.assertEqual(
|
||||
bind(cfg, ["~/.openMSX"], self.POOLS), {"share/systemroms": ("", "Machines")}
|
||||
)
|
||||
|
||||
def test_the_entry_the_platform_list_fills_comes_first(self) -> None:
|
||||
cfg = config(self.filepool("Machines", 1), self.filepool("openmsx", 2))
|
||||
cfg["systems"] = {"msx": {"files": [{"name": "a.rom", "destination": "openmsx/a.rom"}]}}
|
||||
self.assertEqual(
|
||||
bind(cfg, ["~/.openMSX"], self.POOLS), {"share/systemroms": ("", "openmsx")}
|
||||
)
|
||||
|
||||
def test_without_the_pool_two_folders_give_nothing(self) -> None:
|
||||
cfg = config(self.filepool("Machines", 1), self.filepool("openmsx", 2))
|
||||
self.assertEqual(bind(cfg, ["~/.openMSX", "filepool"]), {})
|
||||
|
||||
def test_a_colon_list_gives_each_directory_in_order(self) -> None:
|
||||
# RetroDECK writes XRoar's rompath as four directories.
|
||||
found = handed_directories(config(passes(
|
||||
"emu", "$roms_path/coco:$roms_path/dragon32:$bios_path", "xroar.conf rompath",
|
||||
)))
|
||||
self.assertEqual(
|
||||
[h.directory for h in found], ["$roms_path/coco", "$roms_path/dragon32", "$bios_path"]
|
||||
)
|
||||
self.assertEqual(
|
||||
bind(config(passes("emu", "$roms_path/coco:$bios_path/xroar", "xroar.conf rompath")),
|
||||
[], {"rompath": ""}),
|
||||
{"": ("", "xroar")},
|
||||
)
|
||||
|
||||
def test_a_drive_letter_is_one_directory(self) -> None:
|
||||
self.assertEqual(handed_directories(config(passes("emu", "C:/bios", "rompath"))), [])
|
||||
|
||||
|
||||
class AboveTheDeclaredDirectory(unittest.TestCase):
|
||||
def test_a_prefix_lands_the_declared_directory_where_its_link_is(self) -> None:
|
||||
# RetroPie builds XRoar with --prefix=$md_inst and links
|
||||
# $md_inst/share/xroar/roms to $biosdir.
|
||||
cfg = config(
|
||||
passes("emu", "$md_inst", "${prefix} of configure --prefix"),
|
||||
link("emu", "$biosdir", "$md_inst/share/xroar/roms is a symlink to it"),
|
||||
base="BIOS",
|
||||
)
|
||||
self.assertEqual(bind(cfg, ["${prefix}/share/xroar/roms"]), {"": ("", "")})
|
||||
|
||||
def test_a_folder_of_the_tree_takes_the_rest_of_the_path(self) -> None:
|
||||
cfg = config(passes("emu", "$biosdir/emu", "EMU_HOME"), base="BIOS")
|
||||
self.assertEqual(bind(cfg, ["EMU_HOME/share/roms"]), {"": ("", "emu/share/roms")})
|
||||
|
||||
|
||||
class PerSystem(unittest.TestCase):
|
||||
"""A record written for some systems replaces, there, the others."""
|
||||
|
||||
CFG = {
|
||||
**config(
|
||||
passes("emu", "bios/dragon", "command line -rompath"),
|
||||
{**passes("emu", "bios/coco", "command line -rompath"), "systems": ["coco"]},
|
||||
),
|
||||
"systems": {"coco": {"files": [{"name": "x.zip", "native_system": "coco"}]}},
|
||||
}
|
||||
POOLS = {"-rompath": ""}
|
||||
|
||||
def test_the_system_record_wins_for_its_system(self) -> None:
|
||||
self.assertEqual(bind(self.CFG, [], self.POOLS, ("coco",)), {"": ("", "coco")})
|
||||
|
||||
def test_another_system_keeps_the_generic_record(self) -> None:
|
||||
self.assertEqual(bind(self.CFG, [], self.POOLS, ("dragon32",)), {"": ("", "dragon")})
|
||||
self.assertEqual(bind(self.CFG, [], self.POOLS), {"": ("", "dragon")})
|
||||
|
||||
def test_native_names_are_read_as_our_ids(self) -> None:
|
||||
cfg = {
|
||||
**config({**passes("emu", "bios/trs80coco", "-rompath"), "systems": ["trs80coco"]}),
|
||||
"systems": {"coco": {"native_id": "trs80coco", "files": []}},
|
||||
}
|
||||
self.assertEqual(
|
||||
[sorted(h.systems) for h in handed_directories(cfg)], [["coco"]]
|
||||
)
|
||||
|
||||
def test_a_system_placeholder_gives_one_directory_per_system(self) -> None:
|
||||
# Recalbox passes -rompath os.path.join(BIOS, args.system).
|
||||
cfg = {
|
||||
**config(passes("emu", "/recalbox/share/bios/{system}", "command line -rompath")),
|
||||
"systems": {
|
||||
"coco": {"native_id": "trs80coco", "files": []},
|
||||
"dragon64": {"native_id": "dragon", "files": []},
|
||||
},
|
||||
}
|
||||
self.assertEqual(
|
||||
[(h.folder, sorted(h.systems)) for h in handed_directories(cfg)],
|
||||
[("dragon", ["dragon64"]), ("trs80coco", ["coco"])],
|
||||
)
|
||||
self.assertEqual(bind(cfg, [], self.POOLS | {"-rompath": ""}, ("coco",)),
|
||||
{"": ("", "trs80coco")})
|
||||
self.assertEqual(bind(cfg, [], {"-rompath": ""}, ("dragon32",)), {})
|
||||
|
||||
def test_an_entry_is_launched_for_the_system_that_declares_its_name(self) -> None:
|
||||
cfg = {
|
||||
**config({**passes("emu", "bios/coco", "-rompath"), "systems": ["coco"]}),
|
||||
"systems": {"coco": {"files": [{"name": "coco3.rom", "native_system": "coco"}]}},
|
||||
}
|
||||
index = system_file_index(standalone_set(cfg))
|
||||
self.assertEqual(
|
||||
entry_launch_systems({"name": "COCO3.ROM", "system": "coco3"}, index),
|
||||
frozenset({"coco3", "coco"}),
|
||||
)
|
||||
|
||||
|
||||
class Layout(unittest.TestCase):
|
||||
LINKED = link("emu", "$biosdir/dc", "$home/.local/share/emu is a symlink to it")
|
||||
|
||||
@@ -367,3 +496,4 @@ class Scrapers(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in new issue
Block a user