fix: place slot claims where the pack puts extras

This commit is contained in:
Abdessamad Derraz committed 2026-10-10 12:07:00 +02:00
1 parent 2c2f284414
commit 3d0170264a
4 files changed
+89 -23

No files matched your search

+8 -4
View File
@@ -1733,7 +1733,7 @@ def platform_declarations(
declaring a file under a different name than the emulator profile
(e.g. Batocera ROM1 vs gsplus ROM).
"""
prefix = "" if config.get("base_destination") else declared_root(config).lower()
prefix = declared_root(config).lower()
declared: dict[str, list[tuple[int | None, str]]] = {}
by_md5 = db.get("indexes", {}).get("by_md5", {})
files_db = db.get("files", {})
@@ -1767,11 +1767,15 @@ def platform_declarations(
def declared_root(config: dict) -> str:
"""The directory nearly every declared destination starts with, if any.
"""The BIOS directory a platform writes into every destination, if any.
A platform without base_destination writes it into each destination;
core files go under the same directory.
Only a platform whose base_destination is explicitly empty does that
(RetroDECK writes bios/ in front of each file), and core files go under
the same directory. A config that does not state a base says nothing,
and its common folder is a system's, not the BIOS root.
"""
if config.get("base_destination", None) != "":
return ""
roots = [
d.split("/", 1)[0]
for system in config.get("systems", {}).values()
+49 -17
View File
@@ -436,6 +436,45 @@ def _archive_prefix_extras(
return extras
class ExtraPlacement:
"""Where a pack puts a core's file, decided once for every reader.
The builder, and slots.py judging what a destination holds, both read
this: placed apart, slot arbitration compared RetroDECK's bios/ claims
with core claims at the root and never saw them meet.
"""
def __init__(self, config: dict, base_dest: str, profiles: dict) -> None:
from common import _norm_system_id
self.prefix = _detect_extras_prefix(config, base_dest)
self.slug_based, self.sys_to_slug = _detect_slug_structure(config)
self.platform_systems = set(config.get("systems", {}).keys())
self.norm_map = (
{_norm_system_id(sid): sid for sid in self.platform_systems}
if self.slug_based
else {}
)
self.profiles = profiles
def place(self, entry: dict, dest: str) -> tuple[str, str] | None:
"""(pack-relative destination, full destination), or None.
``entry`` names its profile and may name its own system. A
slug-based platform cannot place a file of a system it has no
folder for.
"""
if self.slug_based:
slug = _slug_for(
entry, self.profiles, self.platform_systems, self.norm_map,
self.sys_to_slug,
)
if not slug:
return None
dest = f"{slug}/{dest}"
return dest, (f"{self.prefix}/{dest}" if self.prefix else dest)
def _collect_emulator_extras(
config: dict,
emulators_dir: str,
@@ -469,7 +508,7 @@ def _collect_emulator_extras(
Works for ANY platform (RetroArch, Batocera, Recalbox, etc.)
"""
from common import _norm_system_id, resolve_platform_cores
from common import resolve_platform_cores
from verify import find_undeclared_files
profiles = (
@@ -479,13 +518,9 @@ def _collect_emulator_extras(
)
# Detect destination conventions for core extras
extras_prefix = _detect_extras_prefix(config, base_dest)
is_slug_based, sys_to_slug = _detect_slug_structure(config)
platform_systems = set(config.get("systems", {}).keys())
norm_map: dict[str, str] = {}
if is_slug_based:
for sid in platform_systems:
norm_map[_norm_system_id(sid)] = sid
placement = ExtraPlacement(config, base_dest, profiles)
extras_prefix = placement.prefix
is_slug_based = placement.slug_based
# Use strict YAML names (no DB alias enrichment) so that files known
# under an alias still get packed at the emulator's expected path.
@@ -508,15 +543,12 @@ def _collect_emulator_extras(
dest = f"{raw_dest}{u['name']}" if raw_dest.endswith("/") else raw_dest
# Slug-based platforms: prefix dest with system slug
if is_slug_based:
slug = _slug_for(u, profiles, platform_systems, norm_map, sys_to_slug)
if not slug:
if unplaceable is not None:
unplaceable.append(u)
continue
dest = f"{slug}/{dest}"
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
placed = placement.place(u, dest)
if placed is None:
if unplaceable is not None:
unplaceable.append(u)
continue
dest, full_dest = placed
if claimants is not None:
claimants[(u.get("emulator", ""), u.get("name", ""), u.get("path") or "")] = (
sanitize_pack_path(dest)
+21 -2
View File
@@ -136,9 +136,14 @@ def profile_claims(
standalone_cores: set[str] | None = None,
zip_contents: dict | None = None,
data_dir_registry: dict | None = None,
config: dict | None = None,
) -> list[Claim]:
"""What each emulator profile says belongs at each destination it names.
With the platform's config, a claim sits where the pack puts the core's
file (ExtraPlacement): under RetroDECK's bios/, under RomM's system
folder. A file the platform cannot place claims nothing.
A file can exist in one build of an emulator and not the other, and the
two builds read from different directories. The mode is decided per
emulator, not per pack: a platform runs some of its emulators as libretro
@@ -148,6 +153,11 @@ def profile_claims(
``standalone_path``. This is the gate verify already applies.
"""
standalone_cores = standalone_cores or set()
placement = None
if config is not None:
from packextras import ExtraPlacement # noqa: PLC0415
placement = ExtraPlacement(config, base_dest, profiles)
claims: list[Claim] = []
for emu_name, profile in sorted(profiles.items()):
if profile.get("type") in ("launcher", "alias"):
@@ -174,7 +184,15 @@ def profile_claims(
) or entry.get("name") or ""
if not dest:
continue
full = f"{base_dest}/{dest}" if base_dest else dest
if placement is None:
full = f"{base_dest}/{dest}" if base_dest else dest
else:
placed = placement.place(
{"profile": emu_name, "system": entry.get("system")}, dest
)
if placed is None:
continue
full = placed[1]
# The owner's own copy is the one the builder ships.
local, status = resolve_local_file(
{**entry, "source_profile": emu_name},
@@ -236,7 +254,8 @@ def find_conflicts(
# claim at all matches what ships is a disagreement.
by_slot: dict[str, list[Claim]] = {}
for claim in profile_claims(
profiles, db, base_dest, standalone_cores, zip_contents, data_dir_registry
profiles, db, base_dest, standalone_cores, zip_contents, data_dir_registry,
config=config,
):
key = _normalize(claim.destination)
platform = by_dest.get(key)