From 3d0170264a78a518bb8b55304873422b9474f6f4 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:07:00 +0200 Subject: [PATCH] fix: place slot claims where the pack puts extras --- scripts/common.py | 12 +++++--- scripts/packextras.py | 66 ++++++++++++++++++++++++++++++++----------- scripts/slots.py | 23 +++++++++++++-- tests/test_slots.py | 11 ++++++++ 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/scripts/common.py b/scripts/common.py index e5a610de..9a80eb03 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -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() diff --git a/scripts/packextras.py b/scripts/packextras.py index 881e35bd..63fd5ccc 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -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) diff --git a/scripts/slots.py b/scripts/slots.py index 6c76e40b..1c042fca 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -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) diff --git a/tests/test_slots.py b/tests/test_slots.py index e6e85a94..8c6ef418 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -125,6 +125,17 @@ class TestConflicts(unittest.TestCase): } } + def test_a_core_claim_sits_where_the_pack_puts_it(self): + """RetroDECK writes bios/ into every destination and leaves its base + empty; core files go under bios/ too. Claimed at the root, they never + met the platform's and four contradictions went unreported.""" + config = {"base_destination": "", "systems": {"console": {"files": [ + {"name": "IPL.bin", "destination": "bios/GC/JAP/IPL.bin", "md5": "m" * 32}, + {"name": "other.bin", "destination": "bios/other.bin"}, + ]}}} + conflicts = slots.find_conflicts(config, self._profile("GC/JAP/IPL.bin"), REGIONS_DB) + self.assertEqual([c.destination for c in conflicts], ["bios/GC/JAP/IPL.bin"]) + def test_wrong_region_under_a_region_slot_is_reported(self): conflicts = slots.find_conflicts( self._config("m" * 32), self._profile("GC/JAP/IPL.bin"), REGIONS_DB