feat: place files outside the bios tree

A platform that binds part of a standalone layout leaves the entries no
binding covers in a folder outside the BIOS tree: they are no longer
packed at the BIOS root, and the manifest names them (root: emulator,
outside_bios_folder). A launcher that executes a profile's file binds
the program folder: in the BIOS tree, below the roms directory a registry
entry names (root: roms, installed into the folder holding the BIOS
folder), or nowhere a pack reaches. A moved entry resolves by its own
path as well as by its destination.

uae4all gains kick.rom and rom.key in its working directory and loses a
not_bundled_on that named Recalbox, which runs uae4all-rpi. lexaloffle
declares that its programs read beside their executable, lists the
Windows build RetroBat runs and the pico8_dyn ROCKNIX runs on x86_64.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 18:22:34 +01:00
1 parent b5ee4ab09e
commit 65e1db9fe1
19 files changed
+802 -141

No files matched your search

+88 -14
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,11 +2755,30 @@ 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 _executed(standalone_cores: Iterable[str]) -> list:
"""The files a standalone set's launchers execute, as launchers read them."""
from launchers import ExecutedFile # noqa: PLC0415
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.
@@ -2744,13 +2794,16 @@ def standalone_layout_path(entry: dict, profile: dict) -> str | None:
return entry.get("path") or None
def standalone_destination(entry: dict, profile: dict, bindings: dict[str, str]) -> str | None:
"""Where a platform puts a standalone entry, relative to the BIOS root.
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 folders the platform's launchers give the layout
``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.
None when no binding covers the entry: it keeps the profile's layout.
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:
@@ -2762,15 +2815,21 @@ def standalone_destination(entry: dict, profile: dict, bindings: dict[str, str])
def standalone_bindings_for(
emu_name: str, profile: dict, standalone_cores: Iterable[str]
) -> dict[str, str]:
"""The folders a platform gives this emulator's standalone layout, by subfolder."""
) -> 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 {}
from launchers import standalone_bindings # noqa: PLC0415
names = {emu_name, *(str(c) for c in profile.get("cores") or [])}
return standalone_bindings(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(
@@ -2794,10 +2853,25 @@ def platform_layouts(
# 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.
# 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 []:
placed = standalone_destination(f, profile, bindings) if isinstance(f, dict) else None
files.append({**f, "standalone_path": placed} if placed else f)
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)}
+5 -1
View File
@@ -856,7 +856,7 @@ def _cross_reference_profile(
# archive.
# A platform declares files of its BIOS tree: none answers for one
# the core reads below the save directory.
in_platform = not save_root and declaration_settles(
in_platform = not save_root and f.get("root") != "roms" and declaration_settles(
f, fname, platform_names, effective_path, index.get("folded_slots", frozenset())
)
@@ -941,6 +941,10 @@ def _cross_reference_profile(
}
if save_root:
entry["root"] = "save"
elif f.get("unplaced"):
entry["unplaced"] = f["unplaced"]
elif f.get("root") == "roms":
entry["root"] = "roms"
elif index.get("launcher_candidates") is not None:
# What verify hands the launcher layer for the same entry.
probe = {
+48 -11
View File
@@ -38,6 +38,7 @@ from pathlib import Path, PurePosixPath
sys.path.insert(0, os.path.dirname(__file__))
from common import resolve_platform_cores as _platform_cores
from common import (
ROMS_ROOT,
installed_data_refs,
platform_arg,
require_directory,
@@ -1302,6 +1303,10 @@ def generate_pack(
continue
if not dest:
continue
if fe.get("root") == ROMS_ROOT:
# Read below the roms directory: the pack is extracted into
# the BIOS folder and cannot reach it, the installer places it.
continue
# Core extras: _collect_emulator_extras already adjusted
# destinations for slug-based platforms. Apply the effective
# prefix (base_dest, or inferred from YAML when base_dest is
@@ -4256,7 +4261,8 @@ def _attach_claims(
})
continue
claim_path, _ = validated_choice(
claim, claim_path, db, validation_index or {}, bios_dir, None, dest, owners,
claim, claim_path, db, validation_index or {}, bios_dir, None,
claim.get("layout_path") or dest, owners,
)
copy, _size = _manifest_entry(
claim_path, manifest_dest, [claim_emu], db, repo_root, {}
@@ -4447,9 +4453,14 @@ def _manifest_core_entries(
# extras prefix when it is an internal layout dir (RetroDECK bios/).
# A file read below the save directory is written relative to the
# directory the core is handed, and says so (save_entry_fields).
manifest_dest = (
save_manifest_dest(fe) if save_root else _manifest_dest_of(full_dest, base_dest)
)
roms_root = fe.get("root") == ROMS_ROOT
if save_root:
manifest_dest = save_manifest_dest(fe)
elif roms_root:
# Relative to the roms directory, which the entry names.
manifest_dest = dest
else:
manifest_dest = _manifest_dest_of(full_dest, base_dest)
entry, file_size = _manifest_entry(
local_path, manifest_dest, [source_emu] if source_emu else [],
@@ -4458,6 +4469,8 @@ def _manifest_core_entries(
)
if entry is not None and save_root:
entry.update(save_entry_fields(fe))
elif entry is not None and roms_root:
entry["root"] = ROMS_ROOT
if entry is None:
if not _claim_platform_omission(
omitted_by_destination.get(full_dest), source_emu
@@ -4624,10 +4637,18 @@ def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[...,
"""
for u in unplaceable:
where = u.get("path") or u.get("name", "")
if u.get("unplaced"):
key = f"emulator:{u.get('profile') or u.get('emulator', '')}:{where}"
elif u.get("root") == ROMS_ROOT:
key = f"roms:{where}"
elif u.get("root") == "save":
key = f"save:{save_manifest_dest(u)}"
else:
key = where
record_omission(
# Keyed apart from the BIOS tree's paths, which a file read below
# the save directory can repeat.
f"save:{save_manifest_dest(u)}" if u.get("root") == "save" else where,
# the save directory or in the emulator's folder can repeat.
key,
u, u.get("system") or "",
# The profile key, the name a target's core list and the
# installer's narrowing match: the display name matched none.
@@ -4805,12 +4826,22 @@ def generate_manifest(
if cores is None else extra_region_key(file_entry, declared_root(config))
)
save_root = file_entry.get("root") == "save"
# Read in the emulator's own folder, outside the BIOS tree: dest is
# the path the emulator reads, relative to that folder.
outside = bool(file_entry.get("unplaced"))
if outside:
dest_fields: dict = {"dest": sanitize_pack_path(file_entry.get("path") or ""),
"root": "emulator"}
elif file_entry.get("root") == ROMS_ROOT:
dest_fields = {"dest": sanitize_pack_path(
file_entry.get("destination") or file_entry.get("path") or ""
), "root": ROMS_ROOT}
elif save_root:
dest_fields = {"dest": save_manifest_dest(file_entry), **save_entry_fields(file_entry)}
else:
dest_fields = {"dest": manifest_destination(full_destination)}
omitted_by_destination[full_destination] = {
"dest": (
save_manifest_dest(file_entry) if save_root
else manifest_destination(full_destination)
),
**(save_entry_fields(file_entry) if save_root else {}),
**dest_fields,
"name": str(file_entry.get("name") or ""),
"system": system_id,
"required": bool(file_entry.get("required", True)),
@@ -5164,6 +5195,12 @@ def generate_manifest(
}
if data_directories:
result["data_directories"] = data_directories
roms = config.get("roms_directory") or {}
if any(entry.get("root") == ROMS_ROOT
for entry in [*manifest_files, *result["omitted_files"]]) and roms:
# Where the installer finds the roms directory: the folder holding
# the BIOS folder, which the platform names bios_folder.
result["roms_directory"] = {"bios_folder": str(roms.get("bios_folder") or "")}
saved = [entry for entry in manifest_files if entry.get("root") == "save"]
if saved:
layout = save_layout(config)
+138 -24
View File
@@ -216,6 +216,18 @@ HANDED_KINDS = ("links", "passes_path")
# that directory in standalone_directories: the call its code reads it with.
WORKING_DIRECTORY_PASSED = "working directory of "
WORKING_DIRECTORY = "getcwd"
# The handle a profile declares when its program reads its files beside its
# own executable: a launcher record executing one of the profile's files
# places the folder holding that file where the launcher runs it from.
EXECUTABLE_DIRECTORY = "executable directory"
# The trees a placed folder belongs to: the BIOS tree every destination is
# relative to, and the roms directory of a platform whose registry names it.
BIOS_TREE = ""
ROMS_TREE = "roms"
# A folder of neither tree, which no pack and no installer can fill.
OUTSIDE_TREE = "outside"
Place = tuple[str, str]
_HOME = re.compile(r"^(?:\$\{HOME\}|\$HOME|\$home|~)(?=/|$)")
# Flatpak sets XDG_DATA_HOME to ~/.var/app/<id>/data inside the sandbox.
@@ -264,6 +276,15 @@ class HandedDirectory:
directory: str
handles: frozenset[str]
source_ref: str
roms_folder: str | 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
def handed_directories(config: dict) -> list[HandedDirectory]:
@@ -295,37 +316,110 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
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_in_bios(raw, base),
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),
))
return found
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
folder and the folders its launchers run programs from.
"""
roms = str((config.get("roms_directory") or {}).get("path") or "").rstrip("/")
directory = directory.strip().rstrip("/")
if not roms or not directory.startswith(roms + "/"):
return None
rest = directory[len(roms) + 1:]
return None if re.search(r"[\s<>$:;]", rest) else rest
@dataclass(frozen=True)
class ExecutedFile:
"""A file a platform's launcher executes, and where it lies.
``place`` is (root, path): the BIOS tree or the roms directory, None
when the file lies outside both (RetroBat's emulator folders).
"""
emulator: str
name: str
place: Place | None
source_ref: str
def executed_files(config: dict) -> list[ExecutedFile]:
"""Every file a platform's launchers execute, from records passing it to exec."""
base = str(config.get("base_destination") or "bios")
found: list[ExecutedFile] = []
for record in config.get("launchers") or []:
if record.get("kind") not in ("fixed_name", "names_searched"):
continue
if str(record.get("passes_to") or "").strip() != "exec":
continue
raw = str(record.get("search_dir") or "").strip()
directories = _directories(record, base)
roms = folder_in_roms(raw, config) if raw and not directories else None
# A program file needs no extension (pico8, vox): the names are
# taken as written, placeholders left out.
for name in (str(n).strip() for n in record.get("names") or []):
if not name or re.search(r"[<>{}]", name):
continue
if directories:
place: Place | None = (BIOS_TREE, sanitize_pack_path(
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))
else:
place = None
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
found.append(ExecutedFile(
emulator, name.rsplit("/", 1)[-1], place, str(record.get("source_ref", ""))
))
return found
def standalone_bindings(
names: Iterable[str], declared: Iterable[str], handed: Iterable[HandedDirectory]
) -> dict[str, str]:
"""The folders of the BIOS tree a platform gives a standalone build's layout.
names: Iterable[str],
declared: Iterable[str],
handed: Iterable[HandedDirectory],
executed: Iterable[ExecutedFile] = (),
layout: 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
a subfolder of that layout ("" for the whole of it) to the folder of the
BIOS tree the emulator reads it from.
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.
A record handing a folder of the tree under a declared directory gives
it directly: RetroPie points Flycast's ~/.local/share/flycast at bios/dc.
A record handing a directory outside the tree is followed through the
links that put a folder of the tree at it or below it: ROCKNIX's patched
Flycast uses /storage/roms/dreamcast/data, which its start script links
to bios/dc, and RetroPie starts UAE4All2 in $md_inst, whose kickstarts
folder it links to bios/amiga.
A record handing a folder under a declared directory gives it directly:
RetroPie points Flycast's ~/.local/share/flycast at bios/dc. A record
handing a directory outside the trees is followed through the links that
put a folder at it or below it: ROCKNIX's patched Flycast uses
/storage/roms/dreamcast/data, which its start script links to bios/dc,
and RetroPie starts UAE4All2 in $md_inst, whose kickstarts folder it
links to bios/amiga.
A subfolder two records give two folders is left out: its files keep
the profile's layout, relative to the BIOS root.
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
pico-8/pico8 from its BIOS folder, ROCKNIX pico8_64 from
/storage/roms/pico-8.
A subfolder two records give two places is left out.
"""
names = set(names)
wanted = {normalize_handle(d) for d in declared}
@@ -340,7 +434,7 @@ def standalone_bindings(
return handle[len(directory) + 1:]
return None
found: dict[str, set[str]] = {}
found: dict[str, set[Place]] = {}
for record in records:
subs = {
sub for handle in record.handles for directory in wanted
@@ -348,21 +442,41 @@ def standalone_bindings(
}
for sub in subs:
if record.folder is not None:
found.setdefault(sub, set()).add(record.folder)
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:
if link.folder is None:
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(link.folder)
return {sub: next(iter(folders)) for sub, folders in found.items() if len(folders) == 1}
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 EXECUTABLE_DIRECTORY in wanted:
paths = list(layout)
for run in executed:
if run.emulator not in names:
continue
# A program run from a folder outside both trees (RetroBat's
# emulators/pico8) binds its folder to no place a pack fills.
root, path = run.place or (OUTSIDE_TREE, run.name)
folder = path.rsplit("/", 1)[0] if "/" in path else ""
for entry_path in paths:
if entry_path.rsplit("/", 1)[-1] != run.name:
continue
sub = entry_path.rsplit("/", 1)[0] if "/" in entry_path else ""
found.setdefault(sub, set()).add((root, folder))
return {sub: next(iter(places)) for sub, places in found.items() if len(places) == 1}
def bound_path(path: str, bindings: dict[str, str]) -> str | None:
"""Where the BIOS tree holds a layout path, None when no binding covers it.
def bound_path(path: str, bindings: dict[str, Place]) -> Place | None:
"""Where a platform holds a layout path, None when no binding covers it.
The longest subfolder that contains the path wins: a link of
kickstarts/ places kickstarts/kick13.rom even when the whole layout is
@@ -372,8 +486,8 @@ def bound_path(path: str, bindings: dict[str, str]) -> str | None:
if sub and not path.startswith(sub + "/"):
continue
rest = path[len(sub) + 1:] if sub else path
folder = bindings[sub]
return f"{folder}/{rest}" if folder else rest
root, folder = bindings[sub]
return (root, f"{folder}/{rest}" if folder else rest)
return None
+48 -6
View File
@@ -17,7 +17,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 sanitize_pack_path
from common import ROMS_ROOT, 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
@@ -43,11 +43,24 @@ def extra_full_destination(fe: dict, prefix: str) -> str:
"""
if fe.get("root") == "save":
return str(fe.get("pack_path") or "")
if fe.get("root") == ROMS_ROOT:
return roms_destination(sanitize_pack_path(fe.get("destination", fe.get("name", ""))))
return prefixed_destination(
sanitize_pack_path(fe.get("destination", fe.get("name", ""))), prefix
)
# Keys a destination below the roms directory apart from the BIOS tree's
# paths. Never written into a pack: the pack is extracted into the BIOS
# folder, and the installer places these files (``root: roms``).
ROMS_KEY = "roms:"
def roms_destination(dest: str) -> str:
"""The key of a destination below the roms directory."""
return f"{ROMS_KEY}{dest}"
def prefixed_destination(dest: str, prefix: str) -> str:
"""A core extra's destination from the pack root.
@@ -309,9 +322,11 @@ class ExtraResolver:
return asset, "release_asset", ""
if status in ("not_found", "external", "user_provided") or not local_path:
return None, status, ""
# The cores reading a moved layout's file are found under the
# emulator's own path, not the folder the platform put it in.
local_path, disagreement = validated_choice(
fe, local_path, self.db, self.validation_index, self.bios_dir, None,
dest, self.validation_owners,
fe.get("layout_path") or dest, self.validation_owners,
)
# The platform never reads the bytes, the core does: a copy it turns
# away by its own size check is not its file, on any platform.
@@ -462,9 +477,10 @@ def unplaceable_extras(
placement = ExtraPlacement(config, "", profiles)
unplaced: dict[tuple[str, str, str], str] = {}
for u in undeclared:
if not u.get("in_repo"):
if not u.get("in_repo") and not u.get("unplaced"):
continue
if not placement.slug_based and u.get("root") != "save":
if (not placement.slug_based and u.get("root") not in ("save", ROMS_ROOT)
and not u.get("unplaced")):
continue
dest = _extra_dest(u)
if not placement.places(u, dest):
@@ -550,7 +566,8 @@ def _agnostic_scan_extras(
continue
# 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):
if (read_from_save_dir(f, standalone) or f.get("unplaced")
or f.get("root") == ROMS_ROOT):
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
@@ -1001,6 +1018,9 @@ 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 the registry names, for a file a launcher runs
# from it (ROCKNIX's roms/pico-8).
self.roms = bool((config.get("roms_directory") or {}).get("path"))
self.config = config
self.norm_map = (
{_norm_system_id(sid): sid for sid in self.platform_systems}
@@ -1019,6 +1039,10 @@ class ExtraPlacement:
directory and goes below the platform's save folder, which a
platform keeping saves beside the content does not have.
"""
if entry.get("unplaced"):
return None
if entry.get("root") == ROMS_ROOT:
return (dest, roms_destination(dest)) if self.roms else None
if entry.get("root") == "save":
placed = self.save_places(entry, dest)
return placed[0] if placed else None
@@ -1039,6 +1063,8 @@ class ExtraPlacement:
platform that hands each system a save folder of its own goes once
below each system that runs the core.
"""
if entry.get("unplaced"):
return []
if entry.get("root") == "save":
return self.save_places(entry, dest)
placed = self.place(entry, dest)
@@ -1075,6 +1101,10 @@ class ExtraPlacement:
def unplaced_reason(self, entry: dict, dest: str) -> str:
"""Why ``place`` returned None for this entry."""
if entry.get("unplaced"):
return str(entry["unplaced"])
if entry.get("root") == ROMS_ROOT:
return "no_roms_directory"
if entry.get("root") == "save":
if self.save is None:
return "no_save_directory"
@@ -1196,6 +1226,12 @@ def _collect_emulator_extras(
# user's machine. The mode a file is bundled for was settled
# upstream, an entry scoped to another build never gets here.
continue
if u.get("unplaced"):
# Read in a folder outside the BIOS tree: named, held or not,
# never written where the emulator does not read it.
if unplaceable is not None:
unplaceable.append({**u, "unplaceable": u["unplaced"]})
continue
if not u["in_repo"] and absent is None:
continue
# For archive entries, use the archive name for resolution
@@ -1291,7 +1327,9 @@ def _collect_emulator_extras(
# Only duplicate files already covered at another destination.
# An unsourceable entry is a file no collection holds: the name
# covered elsewhere belongs to another program's file.
if fname not in covered_names or f.get("unsourceable"):
# 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") == ROMS_ROOT):
continue
# Installed with the emulator or by the platform's package,
# as the first pass already decided for the same entry.
@@ -1530,8 +1568,12 @@ def _report_extra(u: dict, name: str, dest: str) -> dict:
extra["root"] = "save"
if u.get("core_folder") is not None:
extra["core_folder"] = u["core_folder"]
elif u.get("root") == ROMS_ROOT:
extra["root"] = ROMS_ROOT
if u.get("system_dir"):
extra["system_dir"] = u["system_dir"]
if u.get("layout_path"):
extra["layout_path"] = u["layout_path"]
# 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 -1
View File
@@ -228,7 +228,7 @@ def profile_claims(
# outside the BIOS tree, where no platform declares a file.
if outside_gap_scope(entry, is_standalone) or read_from_save_dir(
entry, is_standalone
):
) or entry.get("unplaced"):
continue
# A member of a romset is not a file at the BIOS root: the archive
# holding it occupies the destination (_archive_claims). Claiming
@@ -306,6 +306,7 @@ def _archive_claims(
if isinstance(entry, dict)
and not outside_gap_scope(entry, is_standalone)
and not read_from_save_dir(entry, is_standalone)
and not entry.get("unplaced")
]
prefix = profile.get("archive_prefix", "")
claims: list[Claim] = []
+9 -7
View File
@@ -427,13 +427,15 @@ def _has_exploitable_data(entry: dict) -> bool:
def _system_dir_files(profile: dict, standalone: bool) -> list[dict]:
"""Entries of the build the platform runs, read from the system directory."""
return list(
filter(
read_from_system_dir,
filter_files_by_mode(profile.get("files", []), standalone=standalone),
)
)
"""Entries of the build the platform runs, read from the system directory.
An entry the platform's launchers leave outside the BIOS tree
(``unplaced``) is not: the platform has no file of its own there.
"""
return [
f for f in filter_files_by_mode(profile.get("files", []), standalone=standalone)
if read_from_system_dir(f) and not f.get("unplaced")
]
def _archives_not_members(files: list[dict]) -> list[dict]:
+23 -3
View File
@@ -592,7 +592,10 @@ def find_undeclared_files(
# Below the save directory each core is handed a folder
# of its own: two cores reading hiscore.dat there read
# two files.
("save", core_save_folder(profile)) if save_root else None,
("save", core_save_folder(profile)) if save_root
else ("roms",) if f.get("root") == "roms"
# In the emulator's own folder: no other core reads it.
else ("emulator", emu_name) if f.get("unplaced") else None,
)
if not fname or unread_on(f, config.get("_key", "")):
continue
@@ -600,9 +603,11 @@ def find_undeclared_files(
duplicate = seen_key in seen_files
if duplicate and (first is None or first["profile"] == emu_name):
continue
# Nor does it answer for one read below the roms directory.
verdict = _candidate_verdict(
f, fname, is_standalone, include_all,
{} if save_root else declared_names, dest, folded_slots,
{} if save_root or f.get("root") == "roms" else declared_names,
dest, folded_slots,
)
if verdict == "settled":
seen_files.add(seen_key)
@@ -759,7 +764,14 @@ def find_undeclared_files(
members = with_archive_members(f, profile.get("files", [])).get("members")
if members:
entry["members"] = members
if launcher_layer and not save_root:
if f.get("layout_path"):
entry["layout_path"] = f["layout_path"]
if f.get("unplaced"):
entry["unplaced"] = f["unplaced"]
elif f.get("root") == "roms":
# Read below the roms directory (ROCKNIX's roms/pico-8).
entry["root"] = "roms"
elif launcher_layer and not save_root:
launcher_candidates.append((emu_name, is_standalone, f, entry))
if duplicate:
first.setdefault("other_claims", []).append(entry)
@@ -867,6 +879,8 @@ def find_exclusion_notes(
_UNPLACED_REASONS = {
"outside_bios_folder": "read in a folder the platform keeps outside the BIOS tree",
"no_roms_directory": "read below a roms folder the platform's registry does not name",
"no_platform_slug": "no platform slug for its system",
"below_platform_folder": "below the folder the platform lists",
"no_save_directory": "read below a save directory the platform keeps in no fixed place",
@@ -890,6 +904,12 @@ def _mark_unplaced(config: dict, undeclared: list[dict], profiles: dict) -> None
reason = unplaced.get(
(u.get("emulator", ""), u.get("name", ""), u.get("path") or "")
)
if not reason and u.get("root") == "roms":
# The pack is extracted into the BIOS folder: the installer
# writes this file into the roms folder, the pack cannot.
u["in_pack"] = False
u["omitted"] = "read below the roms folder, which only the installer reaches"
continue
if reason:
u["in_pack"] = False
u["omitted"] = _UNPLACED_REASONS[reason]