From 6e8f2df24f6082ce83474df9ec20b0e1cfd2c859 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:38:16 +0200 Subject: [PATCH] fix: keep both cases on lakka and retropie Both run on Linux but inherited RetroArch's case folding, so a core file differing from a platform name only by case left their packs: MesenCE's bios.col and np2kai's lowercase 2608_*.wav, which those cores open with that exact case. They are now case-sensitive, the shared groups fold only where the platform does, and Lakka gets its own pack instead of sharing RetroArch's. --- platforms/_registry.yml | 5 +++ scripts/common.py | 68 +++++++++++++++++------------- scripts/split_pack.py | 2 +- tests/test_platform_inheritance.py | 30 +++++++++++++ tests/test_verify_pack_choice.py | 2 +- wiki/advanced-usage.md | 6 +-- wiki/architecture.md | 12 +++--- wiki/troubleshooting.md | 8 ++-- 8 files changed, 91 insertions(+), 42 deletions(-) diff --git a/platforms/_registry.yml b/platforms/_registry.yml index 183f0c33..0d232c1c 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -283,6 +283,9 @@ platforms: scraper: libretro inherits_from: retroarch cores: all_libretro + # Lakka is a Linux distribution: two names that differ by case are two + # files there, not the one RetroArch's Windows and macOS builds see. + case_insensitive_fs: false schedule: weekly target_scraper: lakka_targets target_source: https://buildbot.libretro.com/nightly/ @@ -386,6 +389,8 @@ platforms: source: https://github.com/RetroPie/RetroPie-Setup # No cores here: the registry overrides the config, and RetroPie's own # scriptmodules are what say which packages it ships. + # RetroPie runs on Linux, where case tells two files apart. + case_insensitive_fs: false schedule: null target_scraper: retropie_targets target_source: https://retropie.org.uk/stats/pkgflags/ diff --git a/scripts/common.py b/scripts/common.py index 2aa366e2..e629b21e 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -187,35 +187,6 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - # Resolve shared group includes shared_path = os.path.join(platforms_dir, "_shared.yml") - if os.path.exists(shared_path): - shared_real = os.path.realpath(shared_path) - if shared_real not in _shared_yml_cache: - with open(shared_path) as f: - _shared_yml_cache[shared_real] = yaml_load(f) or {} - shared = _shared_yml_cache[shared_real] - shared_groups = shared.get("shared_groups", {}) - for system in config.get("systems", {}).values(): - for group_name in system.get("includes", []): - if group_name in shared_groups: - existing = { - (f.get("name"), f.get("destination", f.get("name"))) - for f in system.get("files", []) - } - existing_lower = { - f.get("destination", f.get("name", "")).lower() - for f in system.get("files", []) - } - for gf in shared_groups[group_name]: - key = (gf.get("name"), gf.get("destination", gf.get("name"))) - dest_lower = gf.get("destination", gf.get("name", "")).lower() - if key not in existing and dest_lower not in existing_lower: - # Marked as ours: the platform's own file does not - # carry it, and an export must not count it kept. - system.setdefault("files", []).append( - {**gf, "shared_group": group_name} - ) - existing.add(key) - # Merge metadata from _registry.yml. The registry is our curated source; # the scraped YAML may be incomplete (missing cores, metadata fields). # Registry fields supplement (not replace) the scraped config. @@ -252,6 +223,42 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") - if key != "cores" and key not in config: config[key] = val + # A shared group's file joins a system unless the platform already has + # it there. Case only makes two names one where the filesystem folds it: + # Lakka and RetroPie run on Linux, and np2kai opens 2608_bd.wav beside + # System.dat's 2608_BD.WAV. + def fold(destination: str) -> str: + return destination.lower() if config.get("case_insensitive_fs") else destination + + if os.path.exists(shared_path): + shared_real = os.path.realpath(shared_path) + if shared_real not in _shared_yml_cache: + with open(shared_path) as f: + _shared_yml_cache[shared_real] = yaml_load(f) or {} + shared = _shared_yml_cache[shared_real] + shared_groups = shared.get("shared_groups", {}) + for system in config.get("systems", {}).values(): + for group_name in system.get("includes", []): + if group_name in shared_groups: + existing = { + (f.get("name"), f.get("destination", f.get("name"))) + for f in system.get("files", []) + } + existing_lower = { + fold(f.get("destination", f.get("name", ""))) + for f in system.get("files", []) + } + for gf in shared_groups[group_name]: + key = (gf.get("name"), gf.get("destination", gf.get("name"))) + dest_lower = fold(gf.get("destination", gf.get("name", ""))) + if key not in existing and dest_lower not in existing_lower: + # Marked as ours: the platform's own file does not + # carry it, and an export must not count it kept. + system.setdefault("files", []).append( + {**gf, "shared_group": group_name} + ) + existing.add(key) + # Recalbox hashes a .zip by its members, so a plain md5 never answers one # of its archive declarations: the resolver reads the mark per entry. if config.get("zip_digest") == "composite": @@ -1513,6 +1520,9 @@ def group_identical_platforms( emulators = [ cores if isinstance(cores, str) else sorted(map(str, cores or [])), sorted(map(str, config.get("standalone_cores") or [])), + # One archive cannot serve a filesystem that folds case and one + # that does not: Lakka keeps bios.col beside BIOS.col. + bool(config.get("case_insensitive_fs")), ] fp = hashlib.sha1(f"{fp}|{emulators}".encode()).hexdigest() if target_cores_cache: diff --git a/scripts/split_pack.py b/scripts/split_pack.py index 55400af9..1c131809 100644 --- a/scripts/split_pack.py +++ b/scripts/split_pack.py @@ -8,7 +8,7 @@ as stored, never recompressed, so the parts depend on the pack alone. Usage: python scripts/split_pack.py dist/ - python scripts/split_pack.py dist/RetroArch_Lakka_v1.22.2_BIOS_Pack.zip + python scripts/split_pack.py dist/RetroArch_v1.22.2_BIOS_Pack.zip python scripts/split_pack.py dist/ --max-size 1900M """ diff --git a/tests/test_platform_inheritance.py b/tests/test_platform_inheritance.py index 1e4c6a0d..df2df7c3 100644 --- a/tests/test_platform_inheritance.py +++ b/tests/test_platform_inheritance.py @@ -62,5 +62,35 @@ class AChildLeavesItsParentAlone(unittest.TestCase): self.assertEqual(child["systems"]["sys-a"]["files"], [{"name": "child.bin"}]) + +class ALinuxPlatformKeepsBothCases(unittest.TestCase): + """Lakka and RetroPie inherited RetroArch's case folding although both + run on Linux: np2kai opens 2608_bd.wav and MesenCE bios.col with that + exact case, and only System.dat's 2608_BD.WAV and BIOS.col shipped.""" + + def setUp(self): + self.addCleanup(common._platform_config_cache.clear) + common._platform_config_cache.clear() + + def test_the_linux_platforms_do_not_fold(self): + platforms = str(REPO_ROOT / "platforms") + self.assertTrue(common.load_platform_config("retroarch", platforms)["case_insensitive_fs"]) + for name in ("lakka", "retropie"): + with self.subTest(platform=name): + config = common.load_platform_config(name, platforms) + self.assertFalse(config["case_insensitive_fs"]) + dests = { + f.get("destination", f.get("name")) + for system in config["systems"].values() for f in system["files"] + } + self.assertTrue({"np2kai/2608_BD.WAV", "np2kai/2608_bd.wav"} <= dests) + + def test_lakka_does_not_share_retroarchs_pack(self): + groups = common.group_identical_platforms( + ["retroarch", "lakka"], str(REPO_ROOT / "platforms") + ) + self.assertEqual(sorted(g for g, _rep in groups), [["lakka"], ["retroarch"]]) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_verify_pack_choice.py b/tests/test_verify_pack_choice.py index dd1f5c2d..f19f1765 100644 --- a/tests/test_verify_pack_choice.py +++ b/tests/test_verify_pack_choice.py @@ -39,7 +39,7 @@ class VerifyPackChoice(unittest.TestCase): self.db = self.out / "db.json" self.db.write_text(json.dumps({"files": {}})) self.stem = generate_pack._platform_pack_stem( - ["retroarch", "lakka"], "retroarch", "platforms" + ["retroarch"], "retroarch", "platforms" ) def _args(self, **overrides) -> argparse.Namespace: diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index 2c435476..bfbeb44f 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -202,9 +202,9 @@ python scripts/generate_pack.py --all --all-variants --output-dir dist/ ``` `--all-variants` builds all six combinations in one run, producing names like -`RetroArch_Lakka_v1.22.2_BIOS_Pack.zip`, -`RetroArch_Lakka_v1.22.2_Platform_BIOS_Pack.zip` and -`RetroArch_Lakka_v1.22.2_Truth_Required_BIOS_Pack.zip`. +`RetroArch_v1.22.2_BIOS_Pack.zip`, +`RetroArch_v1.22.2_Platform_BIOS_Pack.zip` and +`RetroArch_v1.22.2_Truth_Required_BIOS_Pack.zip`. Which one to pick: `full` is the one to ship, since it covers alternate cores and optional firmware. `platform` is much smaller and matches exactly what the diff --git a/wiki/architecture.md b/wiki/architecture.md index ccf648fe..2b4a1942 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -147,11 +147,13 @@ entries that are out of scope here. ## Pack grouping Platforms that produce identical packs are grouped automatically. -Two platforms share a pack when they declare the same files and run the same -emulators. RetroArch and Lakka do, so they produce one combined pack -(`RetroArch_Lakka_BIOS_Pack.zip`). RetroPie inherits RetroArch's systems but -ships its own package list, standalone emulators included, so it gets a -separate pack and a separate install manifest. +Two platforms share a pack when they declare the same files, run the same +emulators and see file names the same way. Lakka declares RetroArch's files +and cores but runs on Linux, where `bios.col` and `BIOS.col` are two files +that different cores open; RetroArch's pack folds them for Windows and macOS. +So Lakka gets its own pack. RetroPie inherits RetroArch's systems but ships its +own package list, standalone emulators included, and is case-sensitive too, so +it gets a separate pack and a separate install manifest. With `--target`, the fingerprint includes target cores so platforms with different hardware filters get separate packs. Emulator and system packs also retain system identity, `variant_group` and requested region; same-named diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 38f9a54e..85d56c9f 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -267,9 +267,11 @@ python scripts/download.py --list bash scripts/download.sh --list ``` -Some platforms share packs (Lakka uses the RetroArch pack). The installer handles -this mapping automatically, but if you're downloading manually, check which pack -name corresponds to your platform. +A pack's name lists every platform it serves. Releases built before +October 2026 ship RetroArch and Lakka as one `RetroArch_Lakka` pack; later +ones give Lakka its own, since Lakka runs on Linux where two names that differ +by case are two files. The installer and the download scripts find the right +pack either way. **A pack in several parts will not extract:**