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:
Abdessamad Derraz committed 2026-11-09 19:30:06 +01:00
1 parent 8633db0a22
commit cd38b9ed10
5 files changed
+422 -74

No files matched your search

+7
View File
@@ -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}$",
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+130
View File
@@ -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()