diff --git a/scripts/launchers.py b/scripts/launchers.py index 442195d7f..b0cb6e1ab 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -955,6 +955,18 @@ class LauncherLayer: for entry in profiles[key].get("files") or [] ]) self._db = db + self._declared_elsewhere: set[tuple[str, bool, str, str]] = set() + + def declared_elsewhere(self, emulator: str, standalone: bool, entry: dict) -> None: + """Record a core entry the platform's list settles under another + name. A launcher opening the entry's own name finds nothing there: + RetroPie lists KS 1.3 as kick34005.A500, and its fs-uae launcher + passes $biosdir/kick13.rom. A name the list holds somewhere is + copied to the launcher's place from that declaration.""" + name = str(entry.get("name", "")) + if name.rsplit("/", 1)[-1].lower() in self.declared_names_folded: + return + self._declared_elsewhere.add((emulator, standalone, name, str(entry.get("path") or ""))) @staticmethod def _profiles_named(name: str, profiles: dict, relevant: set[str]) -> list[str]: @@ -1074,6 +1086,9 @@ class LauncherLayer: if "<" not in str(entry.get("name", "")): report["in_repo"] = resolve_held(report) is not None report["unoffered"] = True + if (*layout, str(entry.get("name", "")), + str(entry.get("path") or "")) in self._declared_elsewhere: + report["declared_elsewhere"] = True by_layout.setdefault(layout, []).append((entry, report)) placed: set[str] = set() for (emulator, standalone), members in sorted(by_layout.items()): @@ -1081,11 +1096,14 @@ class LauncherLayer: # An entry the report left out answers only through the # setting or the extended name: by its own name it was left # out for a reason (a region variant, a settled duplicate). + # One the platform settles under another name still answers + # by its own: the place reads a name the list does not hold. matched = [ (candidate, report) for entry, report in members if (candidate := self.match(place, entry)) is not None - and (candidate.by_setting_or_stem or not report.get("unoffered")) + and (candidate.by_setting_or_stem or not report.get("unoffered") + or report.get("declared_elsewhere")) ] # A size names one entry only when no other of the core's # entries takes that size too. diff --git a/scripts/verify.py b/scripts/verify.py index 88537fa58..73bc6a769 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -627,6 +627,9 @@ def find_undeclared_files( ) if verdict == "settled": seen_files.add(seen_key) + if (launcher_layer and not f.get("unsourceable") and not save_root + and f.get("root") not in OUTER_ROOTS and not f.get("unplaced")): + launcher_layer.declared_elsewhere(emu_name, is_standalone, f) # Answered by the platform's own declaration, which its # report covers: the set it belongs to is spoken for. set_records.append((set_keys((emu_name, is_standalone), f), f, True, None)) diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index 76f488d43..943dd8b19 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -984,6 +984,14 @@ class InstallMatchesATargetManifest(unittest.TestCase): ("batocera", "bcm2711"), ("retropie", "rpi4"), ("emudeck", "steamos"), + # A core only Batocera's x86_64 downloader brings, a missing file two + # cores read, a file another emulator installs itself, and a name a + # launcher opens that another core's copy used to fill. + ("batocera", "jh7110"), + ("knulli", "h700"), + ("rocknix", "sdm845"), + ("retropie", "x86"), + ("retropie", "rpi1"), ) @classmethod diff --git a/tests/test_launcher_placement.py b/tests/test_launcher_placement.py index 3dacf3f4e..36af88691 100644 --- a/tests/test_launcher_placement.py +++ b/tests/test_launcher_placement.py @@ -233,6 +233,35 @@ class UAE4AllOnRetroPie(unittest.TestCase): +class FsUaeOnRetroPie(unittest.TestCase): + """RetroPie lists KS 1.3 as kick34005.A500, and its fs-uae launcher + passes $biosdir/kick13.rom (fs-uae.sh:23-26). The listed copy settles + fs-uae's entry by content, and the launcher's name was filled only as + a copy of UAE4All2's amiga/kick13.rom: a board without UAE4All2 lost it.""" + + def test_the_launcher_name_is_filled_without_another_core(self) -> None: + import json + + from common import load_target_config + + db_path = REPO_ROOT / "database.json" + if not db_path.exists(): + self.skipTest("database.json is not built") + db = json.loads(db_path.read_text(encoding="utf-8")) + cores = load_target_config("retropie", "x86", str(REPO_ROOT / "platforms")) + self.assertNotIn("uae4all", cores) + with redirect_stdout(io.StringIO()): + manifest = generate_manifest( + "retropie", str(REPO_ROOT / "platforms"), db, str(REPO_ROOT / "bios"), + str(REPO_ROOT / "platforms" / "_registry.yml"), + emulators_dir=str(REPO_ROOT / "emulators"), offline=True, + target_cores=set(cores), target_name="x86", + ) + held = [(f["dest"], f["cores"]) for f in manifest["files"] + if f["dest"].rsplit("/", 1)[-1] == "kick13.rom"] + self.assertEqual(held, [("kick13.rom", ["fs-uae"])]) + + PROGRAM = b"PROGRAM BINARY" PROGRAM_DATA = b"PROGRAM DATA"