fix: correct a platform list without replacing it

This commit is contained in:
Abdessamad Derraz committed 2026-09-07 06:40:18 +02:00
1 parent 23c4dc2aae
commit 1c4eb8adec
4 files changed
+139 -7

No files matched your search

+30 -4
View File
@@ -15,6 +15,7 @@ Usage:
from __future__ import annotations
import argparse
import json
import sys
import urllib.error
import urllib.request
@@ -31,9 +32,28 @@ _USER_AGENT = "retrobios-exporter/1.0"
_MAX_BYTES = 64 * 1024 * 1024
def fetch(url: str, destination: Path) -> bytes:
"""Download an original once, then read it from the cache."""
if destination.exists():
def _load_sources(index: Path | None) -> dict[str, str]:
if index is None or not index.is_file():
return {}
try:
return json.loads(index.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
def fetch(url: str, destination: Path, index: Path | None = None) -> bytes:
"""Download an original once, then read it from the cache.
The cache path carries the file's own name and nothing of the revision it
came from, so a file fetched under one pin was served under every later
one and pinned_base() stopped having any effect after the first run. The
URL that produced each cached file is recorded beside the cache, and a
different URL refetches. A cache written before this index existed keeps
being served: nothing recorded means nothing contradicted.
"""
recorded = _load_sources(index)
key = str(destination)
if destination.exists() and recorded.get(key, url) == url:
return destination.read_bytes()
request = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
with urllib.request.urlopen(request, timeout=60) as response:
@@ -42,6 +62,12 @@ def fetch(url: str, destination: Path) -> bytes:
raise ValueError(f"{url}: response larger than {_MAX_BYTES} bytes")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(payload)
if index is not None:
recorded[key] = url
index.parent.mkdir(parents=True, exist_ok=True)
index.write_text(
json.dumps(recorded, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return payload
@@ -98,7 +124,7 @@ def collect_originals(
payload = path.read_bytes()
elif allow_fetch:
try:
payload = fetch(url, path)
payload = fetch(url, path, upstream_dir / ".sources.json")
except (urllib.error.URLError, urllib.error.HTTPError, OSError) as exc:
missing.append(f"{relative}: {exc}")
continue
+10 -1
View File
@@ -113,6 +113,7 @@ class Exporter(BaseExporter):
"the checks are code, not data"
)
self._withdrawn: dict[str, set[str]] = {}
pieces: list[str] = []
cursor = 0
for name, start, end in self._function_spans(script):
@@ -124,7 +125,15 @@ class Exporter(BaseExporter):
if md5s and match:
# An array compared by membership says nothing about order,
# so the same set is left as the maintainer wrote it.
if set(md5s) != set(match.group(1).split()):
theirs = set(match.group(1).split())
# The array was replaced wholesale, so any value EmuDeck holds
# and our model does not was deleted: a user whose dump matched
# it would stop passing the check. A rewrite that withdraws one
# is refused and reported instead.
withdrawn = theirs - set(md5s)
if withdrawn:
self._withdrawn.setdefault(name, set()).update(withdrawn)
elif set(md5s) != theirs:
body = (
body[: match.start(1)]
+ " ".join(md5s)
+38 -2
View File
@@ -106,6 +106,42 @@ class Exporter(BaseExporter):
return nested, "bios"
return None
@staticmethod
def _merge(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
that our model does not carry -- 177 of them across the components.
An entry the platform declares is kept, its fields corrected where the
truth has something to say and left alone where it does not, and what
the platform does not declare is appended.
"""
by_name: OrderedDict[str, OrderedDict] = OrderedDict()
for entry in ours:
name = str(entry.get("filename", ""))
if name and name not in by_name:
by_name[name] = entry
merged: list[OrderedDict] = []
corrected: set[str] = set()
for entry in existing if isinstance(existing, list) else []:
if not isinstance(entry, dict):
continue
name = str(entry.get("filename", ""))
ours_entry = by_name.get(name)
if ours_entry is None:
merged.append(OrderedDict(entry))
continue
combined = OrderedDict(entry)
combined.update(ours_entry)
merged.append(combined)
corrected.add(name)
merged.extend(
entry for name, entry in by_name.items() if name not in corrected
)
return merged
def render(
self,
systems: dict[str, NativeSystem],
@@ -132,10 +168,10 @@ class Exporter(BaseExporter):
continue
holder = self._bios_holder(component_value)
if holder is None:
component_value["bios"] = entries
component_value["bios"] = self._merge(None, entries)
else:
container, key = holder
container[key] = entries
container[key] = self._merge(container.get(key), entries)
break
produced[path] = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"