refactor: read the roms root from romfolders

platforms/romfolders/<platform>.yml records the roms root from the OS's
own source (roms_root path and from_bios), the same fact the registry's
roms_directory restated for ROCKNIX alone. The manifest's roms_directory
now carries from_bios, which the installer follows from a BIOS folder of
the platform's name, and the registry key is gone. A list that installs
above its BIOS folder (AmberELEC) holds its roms root in the install
tree: a file a launcher runs from there is a plain dest. emulators_directory
stays in the registry: no rom folders file names emulator folders.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 19:03:49 +01:00
1 parent 8633db0a22
commit 3db2819150
12 files changed
+180 -52

No files matched your search

+50 -2
View File
@@ -509,6 +509,14 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -
# The registry key, which per-platform profile fields (installed_on,
# not_bundled_on) name: the display name differs (MiSTer FPGA).
config["_key"] = platform_name
# Where the platform's games live, as its rom folders file records it
# from the OS's own source (platforms/romfolders/<key>.yml, roms_root).
romfolders = os.path.join(platforms_dir, "romfolders", f"{platform_name}.yml")
if os.path.exists(romfolders):
with open(romfolders) as f:
roms_root = (source_yaml(f) or {}).get("roms_root")
if isinstance(roms_root, dict):
config["_roms_root"] = roms_root
_platform_config_cache[cache_key] = config
return config
@@ -2755,7 +2763,7 @@ def standalone_set(config: dict) -> set[str]:
names |= {
HANDED_PREFIX + json.dumps(
[h.emulator, h.folder, h.directory, sorted(h.handles), h.source_ref,
h.roms_folder]
h.roms_place]
)
for h in handed_directories(config)
}
@@ -2791,7 +2799,9 @@ def _handed(standalone_cores: Iterable[str]) -> list:
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))
found.append(HandedDirectory(
emulator, folder, directory, frozenset(handles), ref, tuple(roms) if roms else None
))
return found
@@ -2818,6 +2828,39 @@ EMULATORS_ROOT = "emulators"
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT)
def outer_layout(config: dict, root: str) -> dict | None:
"""Where a platform keeps the folder an outer root names, or None.
roms: the roms root its rom folders file records (``path``, the OS's
spelling, and ``from_bios``, relative to the BIOS folder), with the
BIOS folder's own name, base_destination, that a run's folder must
carry before ``from_bios`` is followed from it. emulators: the
registry's ``emulators_directory``, the folders a launcher searches
for an emulator by key, which no rom folders file describes.
"""
if root == ROMS_ROOT:
found = config.get("_roms_root") or {}
base = str(config.get("base_destination") or "")
path, relation = found.get("path"), found.get("from_bios")
if not (path and relation) or "<" in str(path):
return None
if base:
return {"path": str(path), "from_bios": str(relation), "bios_folder": base}
# A list whose dests start above the BIOS folder (AmberELEC's
# bios/...) installs into the folder holding it: a roms root there
# or below is part of the install tree, written as a plain dest.
declared = declared_root(config)
if not declared:
return None
inside = os.path.normpath(f"{declared}/{relation}")
if inside == ".." or inside.startswith("../"):
return None
return {"path": str(path), "install_prefix": "" if inside == "." else inside}
if root == EMULATORS_ROOT:
return config.get("emulators_directory") or None
return None
def standalone_layout_path(entry: dict, profile: dict) -> str | None:
"""The path of an entry relative to the directories its standalone build declares.
@@ -2908,6 +2951,11 @@ def platform_layouts(
tree, path = placed
moved = {**f, "standalone_path": path,
"layout_path": standalone_layout_path(f, profile)}
if tree == "install":
# Below the roms root inside the install tree (AmberELEC's
# /storage/roms): a plain dest the platform prefix skips.
files.append({**moved, "unprefixed": True})
continue
# A file the launcher runs from the roms directory (ROCKNIX's
# roms/pico-8) or an emulator folder (RetroBat's emulators/pico8)
# is relative to it, and says so.
+6 -4
View File
@@ -40,6 +40,7 @@ from alternatives import core_file_spares, settle_sets, slot_set_keys
from common import resolve_platform_cores as _platform_cores
from common import (
OUTER_ROOTS,
outer_layout,
installed_data_refs,
platform_arg,
require_directory,
@@ -4686,11 +4687,12 @@ def _record_absent(
)
# What a manifest's roms_directory and emulators_directory carry from the
# registry: the BIOS folder's name, and for emulator folders the places the
# What a manifest's roms_directory and emulators_directory carry: the BIOS
# folder's name, for the roms root its place relative to the BIOS folder
# (romfolders roms_root.from_bios), and for emulator folders the places the
# launcher searches, the one a missing folder is created at, and the config
# file whose key moves a folder.
MANIFEST_OUTER_FIELDS = ("bios_folder", "searched", "default", "config")
MANIFEST_OUTER_FIELDS = ("bios_folder", "from_bios", "searched", "default", "config")
def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[..., None]) -> None:
@@ -5259,7 +5261,7 @@ def generate_manifest(
if data_directories:
result["data_directories"] = data_directories
for root in OUTER_ROOTS:
layout = config.get(f"{root}_directory") or {}
layout = outer_layout(config, root) or {}
if layout and any(entry.get("root") == root
for entry in [*manifest_files, *result["omitted_files"]]):
# Where the installer finds the folder: relative to the folder
+30 -11
View File
@@ -331,6 +331,9 @@ BIOS_TREE = ""
ROMS_TREE = "roms"
# The emulator folders of a platform whose registry describes them.
EMULATORS_TREE = "emulators"
# The tree a platform's list installs into when it starts above the BIOS
# folder: a plain dest, with no prefix.
INSTALL_TREE = "install"
# A folder of neither tree, which no pack and no installer can fill.
OUTSIDE_TREE = "outside"
Place = tuple[str, str]
@@ -382,15 +385,13 @@ class HandedDirectory:
directory: str
handles: frozenset[str]
source_ref: str
roms_folder: str | None = None
roms_place: Place | None = None
def place(self) -> Place | None:
"""The folder as a place, None when it lies outside both roots."""
if self.folder is not None:
return (BIOS_TREE, self.folder)
if self.roms_folder is not None:
return (ROMS_TREE, self.roms_folder)
return None
return self.roms_place
def handed_directories(config: dict) -> list[HandedDirectory]:
@@ -430,19 +431,37 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
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),
roms_place=None if folder is not None else _roms_place(
config, folder_in_roms(raw, config)
),
))
return found
def _roms_place(config: dict, path: str | None) -> Place | None:
"""A path below the roms root as a place: in the install tree when the
platform installs above its BIOS folder (AmberELEC), else the roms tree."""
if path is None:
return None
from common import ROMS_ROOT, outer_layout # noqa: PLC0415
layout = outer_layout(config, ROMS_ROOT) or {}
if "install_prefix" in layout:
prefix = layout["install_prefix"]
return (INSTALL_TREE, sanitize_pack_path(f"{prefix}/{path}" if prefix else path))
return (ROMS_TREE, path)
def folder_in_roms(directory: str, config: dict) -> str | None:
"""A directory relative to the platform's roms directory, None outside it.
Only a platform whose registry names the roms directory has one
(``roms_directory.path``): ROCKNIX's /storage/roms, which holds its BIOS
Only a platform whose rom folders file records its roms root has one
(``roms_root.path``): ROCKNIX's /storage/roms, which holds its BIOS
folder and the folders its launchers run programs from.
"""
roms = str((config.get("roms_directory") or {}).get("path") or "").rstrip("/")
from common import ROMS_ROOT, outer_layout # noqa: PLC0415
roms = str((outer_layout(config, ROMS_ROOT) or {}).get("path") or "").rstrip("/")
directory = directory.strip().rstrip("/")
if not roms or not directory.startswith(roms + "/"):
return None
@@ -503,7 +522,7 @@ def executed_files(config: dict) -> list[ExecutedFile]:
f"{directories[0]}/{name}" if directories[0] else name
))
elif roms is not None:
place = (ROMS_TREE, sanitize_pack_path(f"{roms}/{name}" if roms else name))
place = _roms_place(config, sanitize_pack_path(f"{roms}/{name}" if roms else name))
elif emulator_folder is not None:
place = (EMULATORS_TREE, sanitize_pack_path(f"{emulator_folder}/{name}"))
else:
@@ -581,8 +600,8 @@ def standalone_bindings(
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))
if not linked and record.roms_place is not None:
found.setdefault(sub, set()).add(record.roms_place)
if EXECUTABLE_DIRECTORY in wanted:
paths = list(layout)
for run in executed:
+13 -7
View File
@@ -18,7 +18,7 @@ from common import resolve_local_file
from common import resolve_platform_cores
from common import core_system_dirs, platform_layouts, system_dir_path
from common import standalone_set as platform_standalone_set
from common import OUTER_ROOTS, sanitize_pack_path
from common import OUTER_ROOTS, outer_layout, sanitize_pack_path
from common import size_fits
from validation import deliverable, read_from_save_dir, read_from_system_dir
from packpaths import libretro_systems, save_destination, save_layout
@@ -44,6 +44,10 @@ def extra_full_destination(fe: dict, prefix: str) -> str:
"""
if fe.get("root") == "save":
return str(fe.get("pack_path") or "")
if fe.get("unprefixed"):
# A path of the install tree outside the BIOS folder (AmberELEC's
# pico-8/ beside bios/): no platform prefix in front.
return sanitize_pack_path(fe.get("destination", fe.get("name", "")))
if fe.get("root") in OUTER_ROOTS:
return outer_destination(
fe["root"], sanitize_pack_path(fe.get("destination", fe.get("name", "")))
@@ -573,7 +577,7 @@ def _agnostic_scan_extras(
# The scan walks the BIOS tree: a file read below the save
# directory is placed by the gap report's pass.
if (read_from_save_dir(f, standalone) or f.get("unplaced")
or f.get("root") in OUTER_ROOTS):
or f.get("root") in OUTER_ROOTS or f.get("unprefixed")):
continue
# An agnostic BIOS mode says the BIOS filename is free, not that
# every file the emulator loads is. A free filename still has a
@@ -1024,12 +1028,10 @@ class ExtraPlacement:
self.native_systems = native_slug_systems(config)
self.flat = bool(config.get("firmware_folder_flat"))
self.save = save_layout(config)
# The roms directory and emulator folders the registry describes,
# The roms root and emulator folders the platform's files describe,
# for a file a launcher runs from them (ROCKNIX's roms/pico-8,
# RetroBat's emulators/pico8).
self.outer = {
root for root in OUTER_ROOTS if config.get(f"{root}_directory")
}
self.outer = {root for root in OUTER_ROOTS if outer_layout(config, root)}
self.config = config
self.norm_map = (
{_norm_system_id(sid): sid for sid in self.platform_systems}
@@ -1050,6 +1052,8 @@ class ExtraPlacement:
"""
if entry.get("unplaced"):
return None
if entry.get("unprefixed"):
return dest, dest
if entry.get("root") in OUTER_ROOTS:
root = entry["root"]
return (dest, outer_destination(root, dest)) if root in self.outer else None
@@ -1339,7 +1343,7 @@ def _collect_emulator_extras(
# covered elsewhere belongs to another program's file.
# Nor is one placed outside the BIOS tree by the layout.
if (fname not in covered_names or f.get("unsourceable") or f.get("unplaced")
or f.get("root") in OUTER_ROOTS):
or f.get("root") in OUTER_ROOTS or f.get("unprefixed")):
continue
# Installed with the emulator or by the platform's package,
# as the first pass already decided for the same entry.
@@ -1586,6 +1590,8 @@ def _report_extra(u: dict, name: str, dest: str) -> dict:
extra["system_dir"] = u["system_dir"]
if u.get("layout_path"):
extra["layout_path"] = u["layout_path"]
if u.get("unprefixed"):
extra["unprefixed"] = True
# The archive layout the core opens ranks same-named copies by what
# they hold. Without it the first copy indexed won.
if u.get("contents"):
+2
View File
@@ -794,6 +794,8 @@ def find_undeclared_files(
entry["members"] = members
if f.get("layout_path"):
entry["layout_path"] = f["layout_path"]
if f.get("unprefixed"):
entry["unprefixed"] = True
if f.get("unplaced"):
entry["unplaced"] = f["unplaced"]
elif f.get("root") in OUTER_ROOTS: