diff --git a/scripts/exporter/retropie_exporter.py b/scripts/exporter/retropie_exporter.py index 07ae76a6..c09b2d24 100644 --- a/scripts/exporter/retropie_exporter.py +++ b/scripts/exporter/retropie_exporter.py @@ -142,12 +142,23 @@ class Exporter(BaseExporter): @classmethod def _insertion_point(cls, help_text: str) -> int | None: - """Where a name joins the list, or None when there is no list.""" + """Where a name joins the list, or None when there is no list. + + A list of alternatives ("a or b", "a/b") is not an enumeration: an + appended ", c" reads as one more required file and the plural turns + "one of these" into "all of these". Such a list is left alone. + """ clause = _BIOS_CLAUSE.search(help_text) if clause is None: return None - names = cls._names_in(clause.group(0)) - return clause.start() + names[-1].end() if names else None + text = clause.group(0) + names = cls._names_in(text) + if not names: + return None + between = text[names[0].start():names[-1].end()] + if re.search(r"\bor\b|/", between): + return None + return clause.start() + names[-1].end() @staticmethod def _in_search_order(candidates: list[NativeFile]) -> list[str]: @@ -164,6 +175,19 @@ class Exporter(BaseExporter): names.append(fe.name) return names + @staticmethod + def _required_for(fe: NativeFile, core: str) -> bool: + """Whether the package's own core needs the file. + + The merged `required` is true when any core needs it: pcsx_rearmed + runs in HLE without a BIOS, yet four were proposed as required for + its package because other PSX cores require them. + """ + required_by = (fe.truth or {}).get("_required_by") + if required_by is None: + return fe.required + return core in required_by + @staticmethod def _join(names: list[str]) -> str: """RetroPie's own idiom: a, b and c.""" @@ -235,7 +259,7 @@ class Exporter(BaseExporter): candidates = [ fe for fe in files - if fe.required + if self._required_for(fe, core or "") and not ( {fe.name.lower()} | {str(a).lower() for a in (fe.native("aliases", []) or [])} diff --git a/scripts/truth.py b/scripts/truth.py index b2ce7a75..9ad4429d 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -123,6 +123,10 @@ def _merge_file_into_system( existing.setdefault("_source_refs", set()) if file_entry.get("required") and not existing.get("required"): existing["required"] = True + if file_entry.get("required"): + # Which cores need it: `required` above is the union, true when + # any core needs it, and a package of one core must not read it. + existing["_required_by"] = existing.get("_required_by", set()) | {emu_name} for h in ("sha1", "md5", "sha256", "crc32"): theirs = file_entry.get(h, "") ours = existing.get(h, "") @@ -214,6 +218,7 @@ def _merge_file_into_system( elif isinstance(crc, str) and crc != crc.lower(): entry["crc32"] = crc.lower() entry["_cores"] = {emu_name} + entry["_required_by"] = {emu_name} if file_entry.get("required") else set() sr = file_entry.get("source_ref") if sr is not None: sr_key = _serialize_source_ref(sr) @@ -387,6 +392,7 @@ def generate_platform_truth( for sys_id, sys_data in systems.items(): for fe in sys_data.get("files", []): fe["_cores"] = sorted(fe.get("_cores", set())) + fe["_required_by"] = sorted(fe.get("_required_by", set())) fe["_source_refs"] = sorted(fe.get("_source_refs", set())) # Add per-system coverage cov = system_cores.get(sys_id, {}) diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index a951306c..106484e4 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -177,5 +177,23 @@ class RecalboxKeepsItsOwnNotes(unittest.TestCase): truth={"md5": A, "note": "Loaded at libretro.c:120"}) self.assertNotIn("note=", Recalbox()._bios_element(fe, "psx")) +class RetroPieProposals(unittest.TestCase): + def test_required_is_read_for_the_package_core(self): + from exporter.retropie_exporter import Exporter as RetroPie + + fe = NativeFile("scph5501.bin", "scph5501.bin", "psx", + truth={"required": True, "_required_by": ["beetle_psx"], + "_cores": ["beetle_psx", "pcsx_rearmed"]}) + self.assertTrue(RetroPie._required_for(fe, "beetle_psx")) + self.assertFalse(RetroPie._required_for(fe, "pcsx_rearmed")) + + def test_a_list_of_alternatives_is_not_extended(self): + from exporter.retropie_exporter import Exporter as RetroPie + + alternatives = "Copy the required BIOS file a.rom or b.rom to $biosdir" + enumeration = "Copy the required BIOS files a.rom and b.rom to $biosdir" + self.assertIsNone(RetroPie._insertion_point(alternatives)) + self.assertIsNotNone(RetroPie._insertion_point(enumeration)) + if __name__ == "__main__": unittest.main()