From 6a852737a832c0ed9cde25f683643ab6df3a6df8 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:27:08 +0200 Subject: [PATCH] fix: one file under two spellings keeps both cores BlastEm opens 32X_M_BIOS.bin and PicoDrive 32X_M_BIOS.BIN. On a case-insensitive platform the manifest kept the first and dropped the second core's claim, so an install narrowed to PicoDrive (RetroArch on Switch) lost the 32X BIOS its pack carries. The claim now joins the entry kept. --- scripts/generate_pack.py | 34 ++++++++++++++++++++++++++++++--- tests/test_install_selection.py | 10 ++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 355a1d51..56975f37 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -3658,6 +3658,29 @@ def _manifest_core_entries( else: full_dest = dest + if ( + case_insensitive + and full_dest not in seen_destinations + and full_dest.lower() in seen_lower + ): + # One file under two spellings: BlastEm opens 32X_M_BIOS.bin and + # PicoDrive 32X_M_BIOS.BIN. The second core's claim joins the + # entry kept, or a pack built without the first core loses the + # file the installer would still have to place. Collisions are + # rare, so the entry is looked up only when one happens. + held = next( + (e for e in manifest_files + if e["dest"].lower() == _manifest_dest_of(full_dest, base_dest).lower()), + None, + ) + if held is not None and held.get("cores") is not None: + _attach_claims( + held, [fe], db, bios_dir, zip_contents, offline, required_only, + validation_index, owners, dest, held["dest"], repo_root, + data_registry, + ) + continue + if placement_block( full_dest, seen_destinations, seen_lower, seen_parents, case_insensitive, @@ -3700,9 +3723,7 @@ def _manifest_core_entries( # Manifest dests are relative to base_destination. Keep the inferred # extras prefix when it is an internal layout dir (RetroDECK bios/). - manifest_dest = full_dest - if base_dest and manifest_dest.startswith(f"{base_dest}/"): - manifest_dest = manifest_dest[len(base_dest) + 1:] + manifest_dest = _manifest_dest_of(full_dest, base_dest) entry, file_size = _manifest_entry( local_path, manifest_dest, [source_emu] if source_emu else [], @@ -3746,6 +3767,13 @@ def _manifest_core_entries( return total_size +def _manifest_dest_of(full_dest: str, base_dest: str) -> str: + """A pack destination as the manifest writes it, below base_destination.""" + if base_dest and full_dest.startswith(f"{base_dest}/"): + return full_dest[len(base_dest) + 1:] + return full_dest + + def _claim_platform_omission(held: dict | None, emulator: str) -> bool: """Record a core's claim on a destination the platform already omits. diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index 3cbf6857..66c8d9dc 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -634,9 +634,15 @@ class InstallMatchesATargetManifest(unittest.TestCase): full = self._manifest(platform) built = self._manifest(platform, set(cores), target) files, omitted = install.narrow_to_cores(full, cores) + # A case-insensitive platform reads one file under both + # spellings (BlastEm's 32X_M_BIOS.bin, PicoDrive's .BIN). + from common import load_platform_config + + config = load_platform_config(platform, str(REPO_ROOT / "platforms")) + fold = str.lower if config.get("case_insensitive_fs") else str self.assertEqual( - {(f["dest"], f["sha1"]) for f in files}, - {(f["dest"], f["sha1"]) for f in built["files"]}, + {(fold(f["dest"]), f["sha1"]) for f in files}, + {(fold(f["dest"]), f["sha1"]) for f in built["files"]}, ) self.assertEqual( {o["dest"] for o in omitted},