From 540374b40d90541c4cc6ebeabb7ad6e4d74f26ea Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:20:42 +0200 Subject: [PATCH] fix: count a launcher's names as read The removal candidates looked for a reading in the profiles only, so a name the platform's own launch code opens, links or copies was listed as read by no core: Batocera's DuckStation generator walks 22 names (duckstationGenerator.py:501-515). Every name a launcher record names is now a reading on that platform. Recalbox's unexplained entries drop from 20 to 13, RetroBat's from 24 to 20, EmuDeck's from 25 to 22. --- scripts/exporter/candidates.py | 17 +++++++++++++++++ tests/test_export_report.py | 15 +++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/scripts/exporter/candidates.py b/scripts/exporter/candidates.py index 99016c1a..d7f080d4 100644 --- a/scripts/exporter/candidates.py +++ b/scripts/exporter/candidates.py @@ -19,6 +19,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from common import _norm_system_id, resolve_platform_cores, unresolved_platform_cores from exporter.baseline import HASH_FIELDS, NativeSystem, _hash_values +from launchers import _names as launcher_names # Batocera cuts some md5 to 29 characters: the prefix still names the dump. _FULL_MD5 = 32 @@ -49,6 +50,19 @@ class ReadEvidence: directories: set[str] = field(default_factory=set) trees: set[str] = field(default_factory=set) tree_members: set[str] = field(default_factory=set) + launched: set[str] = field(default_factory=set) + + def add_launchers(self, records: list[dict]) -> None: + """Every name the platform's own launch code opens, links or copies. + + Batocera's DuckStation generator walks 22 names (bios_lists, + duckstationGenerator.py:501-515): each is read on Batocera whatever + the profiles call it. + """ + for record in records: + if isinstance(record, dict): + for name in launcher_names(record): + self.launched.update(_tails(name)) def add_profile(self, profile: dict, data_registry: dict) -> None: for entry in profile.get("files") or []: @@ -100,6 +114,8 @@ class ReadEvidence: tails = _tails(destination) if any(t in self.tails for t in tails[:-1]): return "path" + if names & self.launched or (tails and tails[-1] in self.launched): + return "launcher" if names & self.names or (tails and tails[-1] in self.names): return "name" hashes = { @@ -164,6 +180,7 @@ def removal_candidates( evidence = ReadEvidence() for key in sorted(examined): evidence.add_profile(profiles[key], data_registry or {}) + evidence.add_launchers(scraped.get("launchers") or []) system_of = { id(file_entry): sys_key diff --git a/tests/test_export_report.py b/tests/test_export_report.py index d6486af1..600e8982 100644 --- a/tests/test_export_report.py +++ b/tests/test_export_report.py @@ -371,6 +371,21 @@ class RemovalCandidatesNeedAReadingNoCoreAnswers(unittest.TestCase): self.assertEqual(kept, [f["name"] for f in files]) return found + def test_a_name_the_platform_launcher_opens_is_a_reading(self): + """Batocera's DuckStation generator walks 22 names (bios_lists, + duckstationGenerator.py:501-515): a name it opens is read on + Batocera whatever the profiles call it.""" + from exporter.baseline import build_native_model + from exporter.candidates import removal_candidates + + scraped = self._scraped([{"name": "scph7502.bin"}], ["corea"]) + scraped["launchers"] = [{ + "emulator": "corea", "build": "libretro", "kind": "names_searched", + "names": ["scph1001.bin", "scph7502.bin"], "source_ref": "gen.py:1-2", + }] + systems, _ = build_native_model({"systems": {}}, scraped) + self.assertEqual(removal_candidates(systems, scraped, self.PROFILES), []) + def test_an_alias_is_a_reading(self): self.assertEqual(self._candidates([{"name": "alt.rom"}]), [])