From 2fb49d0658b190ae9772945e7d986ce97e71471d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:45:42 +0200 Subject: [PATCH] fix: give no second-pass copy to unsourceable files --- scripts/packextras.py | 6 ++++-- tests/test_extra_identity.py | 35 ++++++++++++++++++++++++++++++++ tests/test_game_data_homonyms.py | 6 ++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/scripts/packextras.py b/scripts/packextras.py index bfecfd16..e6070812 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -604,8 +604,10 @@ def _collect_emulator_extras( fname = f.get("name", "") if not fname: continue - # Only duplicate files already covered at another destination - if fname not in covered_names: + # Only duplicate files already covered at another destination. + # An unsourceable entry is a file no collection holds: the name + # covered elsewhere belongs to another program's file. + if fname not in covered_names or f.get("unsourceable"): continue file_mode = f.get("mode") if file_mode == "standalone" and not is_standalone: diff --git a/tests/test_extra_identity.py b/tests/test_extra_identity.py index a9df7e3a..9a52d7e3 100644 --- a/tests/test_extra_identity.py +++ b/tests/test_extra_identity.py @@ -62,5 +62,40 @@ class IdentityTravels(unittest.TestCase): self.assertIn("validation", PROFILE_IDENTITY_FIELDS) +class UnsourceableIsNoCopy(unittest.TestCase): + """The second pass copies a covered file to another core's path by name. + + zc210 ships its own default.sf2; dosbox-staging reads a default.sf2 that + the operating system links to its SoundFont, which no collection holds. + The name was enough to give dosbox-staging a slot, recorded as missing + while zc210 was in the pack and absent once it was not, so a pack built + for a target disagreed with the installer narrowing the full list. + """ + + def test_an_unsourceable_entry_gets_no_copy(self): + profiles = { + "zc210": {"emulator": "ZC", "type": "libretro", "systems": ["zc"], + "files": [{"name": "default.sf2", "path": "zc210/sf2/default.sf2"}]}, + "dosbox-staging": { + "emulator": "DOSBox Staging", "type": "libretro", "systems": ["dos"], + "files": [{"name": "default.sf2", "path": "soundfonts/default.sf2", + "required": False, "unsourceable": "the system's own link"}], + }, + } + config = {"cores": ["zc210", "dosbox-staging"], "systems": {}} + db = { + "files": {"s1": {"name": "default.sf2", "path": "bios/ZC/default.sf2", + "size": 10, "sha1": "s1", "md5": "m1", "crc32": "c1"}}, + "indexes": {"by_name": {"default.sf2": ["s1"]}, "by_md5": {"m1": "s1"}, + "by_crc32": {"c1": "s1"}, + "by_path_suffix": {"zc210/sf2/default.sf2": ["s1"]}}, + } + extras = _collect_emulator_extras(config, "emulators", db, set(), "", profiles) + self.assertEqual( + [(e["source_profile"], e["destination"]) for e in extras], + [("zc210", "zc210/sf2/default.sf2")], + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_game_data_homonyms.py b/tests/test_game_data_homonyms.py index 5e13ff0c..277618e5 100644 --- a/tests/test_game_data_homonyms.py +++ b/tests/test_game_data_homonyms.py @@ -153,7 +153,8 @@ class SecondPassKeepsIdentity(unittest.TestCase): The second extras pass copied a name the baseline already covers to the profile's own path, without its hashes or its `unsourceable:` flag, so the copy resolved on the name: RetroBat's pack carried Quake III's - baseq3/pak1.pk3 as MOHAA's mainta/pak1.pk3. + baseq3/pak1.pk3 as MOHAA's mainta/pak1.pk3. An unsourceable entry is + settled, as the first pass treats it, and gets no copy. """ def test_flag_and_hashes_reach_the_alternative_destination(self): @@ -182,7 +183,8 @@ class SecondPassKeepsIdentity(unittest.TestCase): }} extras = {e["destination"]: e for e in _collect_emulator_extras( config, "emulators", db, set(), "", profiles)} - self.assertEqual(extras["mainta/pak1.pk3"].get("unsourceable"), "retail") + # Unsourceable is settled, as in the first pass: no copy at all. + self.assertNotIn("mainta/pak1.pk3", extras) self.assertEqual(extras["maintt/pak1.pk3"].get("sha1"), "c" * 40)