fix: exporters keep what the maintainer wrote

This commit is contained in:
Abdessamad Derraz committed 2026-10-09 22:45:57 +02:00
1 parent 3baec1794b
commit 8fb65b8e97
4 files changed
+133 -20

No files matched your search

+43 -10
View File
@@ -62,10 +62,29 @@ class Exporter(BaseExporter):
# The data block is a fraction of the file; the rest is the checker.
return True
def _bios_files(self, files: list[NativeFile]) -> str:
return ", ".join(self._bios_items(files))
def _bios_files(self, files: list[NativeFile], before: dict | None = None) -> str:
return ", ".join(self._bios_items(files, before))
def _bios_items(self, files: list[NativeFile]) -> list[str]:
@staticmethod
def _runner_keys(item: dict | None) -> list[str]:
"""The emulator and core checkBios reads through coreExists().
Not in our model, so they travel from the maintainer's own entry: a
rewritten item without them made the checker treat the BIOS as
wanted by every build (vectrex, tvc, macintosh, bk).
"""
return [
f'"{key}": "{item[key]}"'
for key in ("emulator", "core")
if item and isinstance(item.get(key), str)
]
def _bios_items(self, files: list[NativeFile], before: dict | None = None) -> list[str]:
originals = {
str(item.get("file", "")): item
for item in (before or {}).get("biosFiles", [])
if isinstance(item, dict)
}
parts: list[str] = []
for fe in files:
# The platform states an unhashed file as an empty md5 rather
@@ -80,14 +99,25 @@ class Exporter(BaseExporter):
zipped = fe.native("zipped_file", "")
if zipped:
item.append(f'"zippedFile": "{zipped}"')
item.extend(self._runner_keys(originals.get(path)))
parts.append("{ " + ", ".join(item) + " }")
return parts
def _entry_line(self, system: NativeSystem, files: list[NativeFile]) -> str:
@staticmethod
def _own_entry(system: NativeSystem, before: dict | None) -> dict:
"""The maintainer's entry for this system, out of the parsed pair."""
inner = (before or {}).get(system.native_id)
return inner if isinstance(inner, dict) else {}
def _entry_line(
self, system: NativeSystem, files: list[NativeFile], before: dict | None = None
) -> str:
own = self._own_entry(system, before)
runner = "".join(f"{key}, " for key in self._runner_keys(own))
return (
f'{_INDENT}"{system.native_id}": '
f'{{ "name": "{self.display_name(system)}", '
f'"biosFiles": [ {self._bios_files(files)} ] }},'
f'{{ "name": "{self.display_name(system)}", {runner}'
f'"biosFiles": [ {self._bios_files(files, own)} ] }},'
)
@staticmethod
@@ -127,14 +157,17 @@ class Exporter(BaseExporter):
) -> list[str]:
"""One entry, keeping the layout and the notes it was written with."""
trailing, leading = self._item_notes(original)
items = self._bios_items(files)
before = self._parse_entry(original)
own = self._own_entry(system, before)
items = self._bios_items(files, own)
one_line = len(original) == 1 and not trailing and not leading
if one_line:
return [self._entry_line(system, files)]
return [self._entry_line(system, files, before)]
runner = "".join(f"{key}, " for key in self._runner_keys(own))
head = (
f'{_INDENT}"{system.native_id}": '
f'{{ "name": "{self.display_name(system)}", "biosFiles": ['
f'{{ "name": "{self.display_name(system)}", {runner}"biosFiles": ['
)
pad = " " * (len(_INDENT) + 4)
lines = [head]
@@ -244,8 +277,8 @@ class Exporter(BaseExporter):
continue
written.add(key)
original_lines = body[start:end + 1]
replacement = self._entry_line(*pair)
before = self._parse_entry(original_lines)
replacement = self._entry_line(*pair, before)
after = self._parse_entry([replacement])
if before is not None and before == after:
# Nothing changed: keep the maintainer's own lines, comments
+9 -2
View File
@@ -82,8 +82,15 @@ class Exporter(BaseExporter):
for fe in system.files:
# A SHA1 BizHawk declares is BizHawk's own code, the authority
# for its cores: the truth's MSX.rom came from FBNeo and would
# have made MSXHawk refuse the dump it accepts.
if fe.hash("sha1") and not _hash_values(fe.platform or {}, "sha1"):
# have made MSXHawk refuse the dump it accepts. An entry the
# database never declared (can_add refuses it) names no call
# of its own either: by name it would rewrite the homonym of
# another system.
if (
fe.platform is not None
and fe.hash("sha1")
and not _hash_values(fe.platform, "sha1")
):
seen.setdefault(fe.name.casefold(), []).append(fe)
resolved: dict[str, NativeFile] = {}
for name, entries in seen.items():
+10 -8
View File
@@ -176,8 +176,8 @@ class Exporter(BaseExporter):
return nested, "bios"
return None
@staticmethod
def _merge(existing: object, ours: list[OrderedDict]) -> list[OrderedDict]:
@classmethod
def _merge(cls, existing: object, ours: list[OrderedDict]) -> list[OrderedDict]:
"""Correct the component's own list; never replace it.
Assigning our entries wholesale dropped every file RetroDECK declares
@@ -206,6 +206,9 @@ class Exporter(BaseExporter):
for system in _systems_of(entry):
key = (str(entry.get("filename", "")), system)
seen_keys[key] = seen_keys.get(key, 0) + 1
# A hash no revision shares is a revision the platform does not
# know: appended, not counted corrected against entries kept as
# they were (the 64DD IPL declared twice, both contradicted).
for entry in entries:
keys = _matching_keys(entry, by_key)
other_revision = (
@@ -213,18 +216,17 @@ class Exporter(BaseExporter):
and seen_keys.get(keys[0], 0) > 1
and not _md5_set(entry.get("md5")) & _md5_set(by_key[keys[0]].get("md5"))
)
if other_revision:
merged.append(OrderedDict(entry))
corrected.update(keys)
continue
if not keys:
if other_revision or not keys:
merged.append(OrderedDict(entry))
continue
declared = entry.get("system")
combined = OrderedDict(entry)
# Only what the format says it corrects: the maintainer's
# description and search paths are theirs (neogeo.zip lost its
# four roms directories to a path that does not exist).
combined.update(
(field, value) for field, value in by_key[keys[0]].items()
if not (field == "system" and declared)
if field in cls.carries() and not (field == "system" and declared)
)
merged.append(combined)
corrected.update(keys)
+71
View File
@@ -355,5 +355,76 @@ class RetroPieProposals(unittest.TestCase):
self.assertIsNone(RetroPie._insertion_point(alternatives))
self.assertIsNotNone(RetroPie._insertion_point(enumeration))
class BizHawkRewritesOnlyWhatItDeclares(unittest.TestCase):
def test_a_truth_only_homonym_does_not_rewrite_a_declared_call(self):
"""opera's panafz10-norsa.bin, absent from BizHawk's list, would have
rewritten the 3DO call of the same name by index on the name alone."""
sha_bizhawk, sha_other = "4" * 40, "e" * 40
declared = NativeFile("bios.bin", "bios.bin", "SYSB", platform={"sha1": sha_bizhawk})
truth_only = NativeFile("bios.bin", "bios.bin", "sys-a", truth={"sha1": sha_other})
exporter = BizHawk()
source = f'File("{sha_bizhawk.upper()}", 10, "bios.bin")\n'
produced = exporter.render(
{"SYSB": NativeSystem("SYSB", files=[declared]),
"sys-a": NativeSystem("sys-a", files=[truth_only])},
None, {exporter.native_filename(): source},
)
self.assertIn(sha_bizhawk.upper(), produced[exporter.native_filename()])
self.assertNotIn(sha_other.upper(), produced[exporter.native_filename()])
class RetroDeckCorrectsOnlyWhatItCarries(unittest.TestCase):
def test_paths_and_description_stay_the_maintainers(self):
"""neogeo.zip lost its four search directories to a path that does
not exist, and 26 descriptions were rewritten."""
from collections import OrderedDict # noqa: PLC0415
existing = [{"filename": "neogeo.zip", "system": "neogeo", "md5": A,
"paths": ["$roms_path/neogeo", "$roms_path/fbneo", "$bios_path"],
"description": "Neo Geo BIOS"}]
ours = [OrderedDict([("filename", "neogeo.zip"), ("md5", B), ("system", "neogeo"),
("description", "generated"), ("paths", "$bios_path/roms/neogeo")])]
merged = RetroDeck._merge(existing, ours)
self.assertEqual(merged[0]["md5"], B)
self.assertEqual(merged[0]["paths"], ["$roms_path/neogeo", "$roms_path/fbneo", "$bios_path"])
self.assertEqual(merged[0]["description"], "Neo Geo BIOS")
def test_a_hash_no_revision_shares_is_appended(self):
"""IPL.n64 declared twice, both contradicted: counted corrected,
written nowhere."""
from collections import OrderedDict # noqa: PLC0415
existing = [{"filename": "IPL.n64", "system": "n64dd", "md5": A},
{"filename": "IPL.n64", "system": "n64dd", "md5": B}]
ours = [OrderedDict([("filename", "IPL.n64"), ("md5", C), ("system", "n64dd")])]
merged = RetroDeck._merge(existing, ours)
self.assertEqual([e["md5"] for e in merged], [A, B, C])
class BatoceraKeepsTheRunnerKeys(unittest.TestCase):
def test_emulator_and_core_travel_with_a_rewritten_entry(self):
"""checkBios skips a BIOS whose emulator or core is not installed;
rewritten without the keys, vectrex's MAME BIOS was wanted by every
build."""
from exporter.batocera_exporter import Exporter as Batocera # noqa: PLC0415
fe = NativeFile("bios.bin", "bios.bin", "vectrex", platform={"md5": A},
truth={"md5": B}, corrections=["md5"])
fe.native_data = {"native_path": "bios/bios.bin"}
system = NativeSystem("vectrex", files=[fe])
original = (
' "vectrex": { "name": "Vectrex", "emulator": "libretro", "core": "mame", '
'"biosFiles": [ { "md5": "' + A + '", "file": "bios/bios.bin", '
'"emulator": "libretro", "core": "mame" } ] },'
)
exporter = Batocera()
line = exporter._entry_line(system, [fe], exporter._parse_entry([original]))
parsed = exporter._parse_entry([line])["vectrex"]
self.assertEqual((parsed["emulator"], parsed["core"]), ("libretro", "mame"))
self.assertEqual(parsed["biosFiles"][0]["core"], "mame")
self.assertEqual(parsed["biosFiles"][0]["md5"], B)
if __name__ == "__main__":
unittest.main()