chore: merge voxatron split and emulator folders

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 18:56:51 +01:00
commit 8633db0a22
19 files changed
+478 -171

No files matched your search

+14 -4
View File
@@ -2810,8 +2810,12 @@ def _executed(standalone_cores: Iterable[str]) -> list:
# 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.
# The entry root of a file read below the platform's roms directory, and of
# one read in an emulator's folder outside the BIOS tree that the registry
# describes. Each is located by the registry entry <root>_directory.
ROMS_ROOT = "roms"
EMULATORS_ROOT = "emulators"
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT)
def standalone_layout_path(entry: dict, profile: dict) -> str | None:
@@ -2905,8 +2909,9 @@ def platform_layouts(
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)
# roms/pico-8) or an emulator folder (RetroBat's emulators/pico8)
# is relative to it, and says so.
files.append({**moved, "root": tree} 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)}
@@ -3032,7 +3037,12 @@ def resolve_platform_cores(
if cores_config == "all_libretro":
result = every_libretro()
elif isinstance(cores_config, list):
core_set = {str(c) for c in cores_config}
# An emulator the platform launches standalone runs there even when
# its own list names only the cores: Batocera's es_systems.yml files
# lexaloffle and gsplus as standalone emulators and nowhere else.
core_set = {str(c) for c in cores_config} | {
str(c) for c in config.get("standalone_cores") or []
}
index = upstream_profile_index(profiles)
platform = str(config.get("_key") or "")
+4 -3
View File
@@ -25,6 +25,7 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from alternatives import set_keys, set_requirements, settle_sets
from common import (
OUTER_ROOTS,
platform_arg,
ProfileSelectionError,
_foreign_tree,
@@ -875,7 +876,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 f.get("root") != "roms" and declaration_settles(
in_platform = not save_root and f.get("root") not in OUTER_ROOTS and declaration_settles(
f, fname, platform_names, effective_path, index.get("folded_slots", frozenset()),
index.get("declared_contents"),
)
@@ -973,8 +974,8 @@ def _cross_reference_profile(
entry["root"] = "save"
elif f.get("unplaced"):
entry["unplaced"] = f["unplaced"]
elif f.get("root") == "roms":
entry["root"] = "roms"
elif f.get("root") in OUTER_ROOTS:
entry["root"] = f["root"]
elif index.get("launcher_candidates") is not None:
# What verify hands the launcher layer for the same entry.
probe = {
+30 -19
View File
@@ -39,7 +39,7 @@ sys.path.insert(0, os.path.dirname(__file__))
from alternatives import core_file_spares, settle_sets, slot_set_keys
from common import resolve_platform_cores as _platform_cores
from common import (
ROMS_ROOT,
OUTER_ROOTS,
installed_data_refs,
platform_arg,
require_directory,
@@ -1309,9 +1309,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.
if fe.get("root") in OUTER_ROOTS:
# Read below the roms directory or in an emulator folder: the
# pack is extracted into the BIOS folder and cannot reach
# them, the installer places these files.
continue
# Core extras: _collect_emulator_extras already adjusted
# destinations for slug-based platforms. Apply the effective
@@ -4508,11 +4509,11 @@ 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).
roms_root = fe.get("root") == ROMS_ROOT
outer_root = fe.get("root") if fe.get("root") in OUTER_ROOTS else None
if save_root:
manifest_dest = save_manifest_dest(fe)
elif roms_root:
# Relative to the roms directory, which the entry names.
elif outer_root:
# Relative to the roms or emulators folder the entry names.
manifest_dest = dest
else:
manifest_dest = _manifest_dest_of(full_dest, base_dest)
@@ -4524,8 +4525,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
elif entry is not None and outer_root:
entry["root"] = outer_root
if entry is None:
if not _claim_platform_omission(
omitted_by_destination.get(full_dest), source_emu
@@ -4685,6 +4686,13 @@ 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
# 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")
def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[..., None]) -> None:
"""Held by the collection, nowhere to put it on this platform.
@@ -4694,8 +4702,8 @@ def _record_unplaceable(unplaceable: list[dict], record_omission: Callable[...,
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") in OUTER_ROOTS:
key = f"{u['root']}:{where}"
elif u.get("root") == "save":
key = f"save:{save_manifest_dest(u)}"
else:
@@ -4887,10 +4895,10 @@ def generate_manifest(
if outside:
dest_fields: dict = {"dest": sanitize_pack_path(file_entry.get("path") or ""),
"root": "emulator"}
elif file_entry.get("root") == ROMS_ROOT:
elif file_entry.get("root") in OUTER_ROOTS:
dest_fields = {"dest": sanitize_pack_path(
file_entry.get("destination") or file_entry.get("path") or ""
), "root": ROMS_ROOT}
), "root": file_entry["root"]}
elif save_root:
dest_fields = {"dest": save_manifest_dest(file_entry), **save_entry_fields(file_entry)}
else:
@@ -5250,12 +5258,15 @@ 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 "")}
for root in OUTER_ROOTS:
layout = config.get(f"{root}_directory") 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
# holding the BIOS folder, which the platform names bios_folder.
result[f"{root}_directory"] = {
key: layout[key] for key in MANIFEST_OUTER_FIELDS if key in layout
}
saved = [entry for entry in manifest_files if entry.get("root") == "save"]
if saved:
layout = save_layout(config)
+21
View File
@@ -329,6 +329,8 @@ EXECUTABLE_DIRECTORY = "executable directory"
# relative to, and the roms directory of a platform whose registry names it.
BIOS_TREE = ""
ROMS_TREE = "roms"
# The emulator folders of a platform whose registry describes them.
EMULATORS_TREE = "emulators"
# A folder of neither tree, which no pack and no installer can fill.
OUTSIDE_TREE = "outside"
Place = tuple[str, str]
@@ -448,6 +450,22 @@ def folder_in_roms(directory: str, config: dict) -> str | None:
return None if re.search(r"[\s<>$:;]", rest) else rest
def folder_in_emulators(directory: str, config: dict) -> str | None:
"""The emulator folder a launcher names by its key, None for any other.
Only a platform whose registry describes its emulator folders has one
(``emulators_directory.launcher_spelling``): RetroBat's generators take
each from AppConfig.GetFullPath("<key>"), a folder of the RetroBat
install named after the key.
"""
spelling = str((config.get("emulators_directory") or {}).get("launcher_spelling") or "")
if "{key}" not in spelling:
return None
pattern = re.escape(spelling).replace(re.escape("{key}"), r"([\w.-]+)")
found = re.fullmatch(pattern, directory.strip())
return found.group(1) if found else None
@dataclass(frozen=True)
class ExecutedFile:
"""A file a platform's launcher executes, and where it lies.
@@ -474,6 +492,7 @@ def executed_files(config: dict) -> list[ExecutedFile]:
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
emulator_folder = folder_in_emulators(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 []):
@@ -485,6 +504,8 @@ def executed_files(config: dict) -> list[ExecutedFile]:
))
elif roms is not None:
place = (ROMS_TREE, 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:
place = None
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
+28 -23
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 ROMS_ROOT, sanitize_pack_path
from common import OUTER_ROOTS, 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,22 +44,23 @@ 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", ""))))
if fe.get("root") in OUTER_ROOTS:
return outer_destination(
fe["root"], 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 outer_destination(root: str, dest: str) -> str:
"""The key of a destination below the roms or an emulator folder.
def roms_destination(dest: str) -> str:
"""The key of a destination below the roms directory."""
return f"{ROMS_KEY}{dest}"
Kept apart from the BIOS tree's paths and never written into a pack:
the pack is extracted into the BIOS folder, and the installer places
these files (``root: roms``, ``root: emulators``).
"""
return f"{root}:{dest}"
def prefixed_destination(dest: str, prefix: str) -> str:
@@ -484,7 +485,7 @@ def unplaceable_extras(
for u in undeclared:
if not u.get("in_repo") and not u.get("unplaced"):
continue
if (not placement.slug_based and u.get("root") not in ("save", ROMS_ROOT)
if (not placement.slug_based and u.get("root") not in ("save", *OUTER_ROOTS)
and not u.get("unplaced")):
continue
dest = _extra_dest(u)
@@ -572,7 +573,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") == ROMS_ROOT):
or f.get("root") in OUTER_ROOTS):
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
@@ -1023,9 +1024,12 @@ 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"))
# The roms directory and emulator folders the registry describes,
# 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.config = config
self.norm_map = (
{_norm_system_id(sid): sid for sid in self.platform_systems}
@@ -1046,8 +1050,9 @@ class ExtraPlacement:
"""
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") in OUTER_ROOTS:
root = entry["root"]
return (dest, outer_destination(root, dest)) if root in self.outer else None
if entry.get("root") == "save":
placed = self.save_places(entry, dest)
return placed[0] if placed else None
@@ -1108,8 +1113,8 @@ class ExtraPlacement:
"""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") in OUTER_ROOTS:
return f"no_{entry['root']}_directory"
if entry.get("root") == "save":
if self.save is None:
return "no_save_directory"
@@ -1334,7 +1339,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") == ROMS_ROOT):
or f.get("root") in OUTER_ROOTS):
continue
# Installed with the emulator or by the platform's package,
# as the first pass already decided for the same entry.
@@ -1575,8 +1580,8 @@ 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
elif u.get("root") in OUTER_ROOTS:
extra["root"] = u["root"]
if u.get("system_dir"):
extra["system_dir"] = u["system_dir"]
if u.get("layout_path"):
+13 -9
View File
@@ -46,6 +46,7 @@ from alternatives import (
slot_set_keys,
)
from common import (
OUTER_ROOTS,
_foreign_tree,
_owner_tokens,
platform_arg,
@@ -607,7 +608,7 @@ def find_undeclared_files(
# of its own: two cores reading hiscore.dat there read
# two files.
("save", core_save_folder(profile)) if save_root
else ("roms",) if f.get("root") == "roms"
else (f["root"],) if f.get("root") in OUTER_ROOTS
# In the emulator's own folder: no other core reads it.
else ("emulator", emu_name) if f.get("unplaced") else None,
)
@@ -620,9 +621,9 @@ def find_undeclared_files(
# Nor does it answer for one read below the roms directory.
verdict = _candidate_verdict(
f, fname, is_standalone, include_all,
{} if save_root or f.get("root") == "roms" else declared_names,
{} if save_root or f.get("root") in OUTER_ROOTS else declared_names,
dest, folded_slots,
None if save_root or f.get("root") == "roms" else declared_contents,
None if save_root or f.get("root") in OUTER_ROOTS else declared_contents,
)
if verdict == "settled":
seen_files.add(seen_key)
@@ -795,9 +796,10 @@ def find_undeclared_files(
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 f.get("root") in OUTER_ROOTS:
# Read below the roms directory (ROCKNIX's roms/pico-8)
# or in an emulator folder (RetroBat's emulators/pico8).
entry["root"] = f["root"]
elif launcher_layer and not save_root:
launcher_candidates.append((emu_name, is_standalone, f, entry))
keys = set_keys((emu_name, is_standalone), f)
@@ -975,6 +977,7 @@ 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_emulators_directory": "read in an emulator folder the platform's registry does not describe",
"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",
@@ -998,11 +1001,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":
if not reason and u.get("root") in OUTER_ROOTS:
# The pack is extracted into the BIOS folder: the installer
# writes this file into the roms folder, the pack cannot.
# writes this file into the roms or emulator folder, the pack
# cannot.
u["in_pack"] = False
u["omitted"] = "read below the roms folder, which only the installer reaches"
u["omitted"] = f"read below the {u['root']} folder, which only the installer reaches"
continue
if reason:
u["in_pack"] = False