fix: judge a declared slot by the platform's copy

--declared-only settled each slot on its first declaration before the
cores narrowed the list, and the declared event judged the core copy
served in its place. Both now read the declaration whose systems
remain, as the platform checks it.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 20:36:53 +02:00
1 parent 5869da409b
commit ea48a54329
4 files changed
+129 -20

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "b23c6da4ee131f898c25540bd53677ea4112964fd00b209e4fe85dfdba8427d1"
$defaultInstallSha256 = "5fedfbf308ba04a174ce0682380a28150b6fe5eab514a605e3b318193ce37c7a"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+77 -18
View File
@@ -665,17 +665,45 @@ def _emit_check(
bytes_needed=sum(entry.get("size", 0) for entry in needed),
data_missing=data_missing, not_installable=not_installable,
)
own = [entry for entry in files if entry.get("cores") is None]
own_needed = [entry for entry in own if id(entry) not in present]
own_wrong = sum(1 for entry in own if id(entry) in wrong)
# The copy the platform itself checks at each of its slots, which is
# what --declared-only checks: a core's copy served in its place is
# judged against the platform's.
own_states = []
for entry in files:
if entry.get("cores") is not None:
continue
copy = declared_copy(entry)
if copy is entry or copy.get("sha1") == entry.get("sha1"):
state = ("present" if id(entry) in present
else "wrong_hash" if id(entry) in wrong else "missing")
else:
state = _declared_state(copy, entry, bios_path)
own_states.append((entry["dest"], copy.get("size", 0), state))
EVENTS.emit(
"declared", platform=plat, files=len(own), present=len(own) - len(own_needed),
missing=len(own_needed) - own_wrong, wrong_hash=own_wrong,
bytes_needed=sum(entry.get("size", 0) for entry in own_needed),
dests=[entry["dest"] for entry in own],
"declared", platform=plat, files=len(own_states),
present=sum(1 for _d, _s, state in own_states if state == "present"),
missing=sum(1 for _d, _s, state in own_states if state == "missing"),
wrong_hash=sum(1 for _d, _s, state in own_states if state == "wrong_hash"),
bytes_needed=sum(size for _d, size, state in own_states if state != "present"),
dests=[dest for dest, _s, _state in own_states],
)
def _declared_state(copy: dict, entry: dict, bios_path: Path) -> str:
"""present, missing or wrong_hash: the file at entry's place against copy."""
try:
path = _entry_path(bios_path, entry)
except ValueError:
return "missing"
if not path.is_file():
return "missing" if not path.exists() else "wrong_hash"
try:
verdict = _classify_local(copy, path)
except OSError:
return "wrong_hash"
return "present" if verdict == "up_to_date" else "wrong_hash"
def leave_out_game_data(
files: list[dict], omitted: list[dict]
) -> tuple[list[dict], list[dict], list[dict]]:
@@ -697,17 +725,42 @@ def keep_declared(files: list[dict], omitted: list[dict]) -> tuple[list[dict], l
A slot a core contradicts gets the platform's copy, and no core copy
stays behind as an alternative: narrowing by cores afterwards would
otherwise put a core's file back where a dropped system's was.
otherwise put a core's file back where a dropped system's was. Which of
the platform's declarations answers is left to that narrowing, as
without --declared-only: the first whose systems remain.
"""
def own(entries: list[dict]) -> list[dict]:
return [
{k: v for k, v in _settle_slot(entry, set()).items() if k != "alternatives"}
for entry in entries if entry.get("cores") is None
]
result = []
for entry in entries:
if entry.get("cores") is not None:
continue
kept = {k: v for k, v in _settle_slot(entry, set()).items()
if k not in ("alternatives", "declared_copy")}
declared = entry.get("declared_choices") or []
if len(declared) > 1:
kept["declared_choices"] = declared
result.append(kept)
return result
return own(files), own(omitted)
def declared_copy(entry: dict) -> dict:
"""The copy the platform's own list checks at a platform file's slot.
A narrowing that serves a core's copy there records the platform's
answer beside it, and an entry not yet settled answers by its first
declaration. Otherwise the entry is the platform's copy itself.
"""
recorded = entry.get("declared_copy")
if recorded is not None:
return recorded
declared = entry.get("declared_choices")
if declared:
return declared[0]
return entry
def _left_out_lines(left_out: list[dict], every: bool) -> list[str]:
"""What --no-game-data left out, named per category, ten each unless every."""
lines = []
@@ -3428,22 +3481,28 @@ def _settle_slot(
declared = entry.get("declared_choices")
if declared is None:
return entry
platform_copy = next(
(c for c in declared
if kept_system is None or any(kept_system(s) for s in c["systems"])),
declared[0],
)
copy = next(
(c for c in entry.get("slot_choices", []) if chosen.intersection(c["cores"])),
None,
)
if copy is None or "sha1" not in copy:
copy = next(
(c for c in declared
if kept_system is None or any(kept_system(s) for s in c["systems"])),
declared[0],
)
copy = platform_copy
kept = {
k: v for k, v in entry.items()
if k not in _COPY_FIELDS and k not in ("slot_choices", "declared_choices")
if k not in _COPY_FIELDS
and k not in ("slot_choices", "declared_choices", "declared_copy")
}
kept.update((k, copy[k]) for k in _COPY_FIELDS if k in copy and k != "cores")
kept["cores"] = None
if copy is not platform_copy and copy.get("sha1") != platform_copy.get("sha1"):
# What the platform checks here, for the `declared` event.
kept["declared_copy"] = {k: platform_copy[k] for k in _COPY_FIELDS
if k in platform_copy and k != "cores"}
return kept
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="b23c6da4ee131f898c25540bd53677ea4112964fd00b209e4fe85dfdba8427d1"
DEFAULT_INSTALL_SHA256="5fedfbf308ba04a174ce0682380a28150b6fe5eab514a605e3b318193ce37c7a"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+50
View File
@@ -288,6 +288,56 @@ class APlatformFileSettledByCoresAndSystems(unittest.TestCase):
self.assertEqual(self.served(["dsi"]), "c")
self.assertEqual(self.served(["nds"]), "b")
def test_declared_only_keeps_the_declaration_whose_system_remains(self):
"""--declared-only settled the slot on the first declaration before
the cores narrowed it: on a target without melonds it checked the
nintendo-dsi copy, the platform the nintendo-ds one."""
manifest = self.manifest()
for names, letter in ((["nds"], "b"), (["dsi"], "c"), (["melonds"], "c"),
(["desmume"], "b")):
with self.subTest(names=names):
files, omitted = install.keep_declared(manifest["files"], [])
files, _ = install.narrow_to_cores(manifest, names, files, omitted)
self.assertEqual(files[0]["sha1"][0], letter)
def test_the_declared_event_judges_the_copy_the_platform_checks(self):
"""melonds' copy is served at the slot, the platform checks its own:
the event and a --declared-only check give the same answer."""
import hashlib
tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, tmp, True)
payloads = {letter: letter.encode() * 3 for letter in "abc"}
manifest = self.manifest()
entry = manifest["files"][0]
def real(copy: dict, letter: str) -> None:
copy.update(sha1=hashlib.sha1(payloads[letter]).hexdigest(),
sha256=hashlib.sha256(payloads[letter]).hexdigest(), size=3)
real(entry, "a")
real(entry["slot_choices"][1], "a")
real(entry["declared_choices"][0], "c")
real(entry["declared_choices"][1], "b")
(tmp / "dsi_bios9.bin").write_bytes(payloads["c"])
def declared_event(files: list[dict]) -> dict:
_todo, fine, wrong = install.check_local(files, tmp)
with mock.patch.object(install.EVENTS, "emit") as emit:
install._emit_check("retroarch", manifest, tmp, files, fine, wrong, 0, 0)
return next(call.kwargs for call in emit.call_args_list
if call.args[0] == "declared")
for names in (None, ["melonds"], ["desmume", "melonds"]):
with self.subTest(names=names):
served = manifest["files"]
own, _ = install.keep_declared(manifest["files"], [])
if names is not None:
served, _ = install.narrow_to_cores(manifest, names)
own, _ = install.narrow_to_cores(manifest, names, own, [])
self.assertEqual(declared_event(served), declared_event(own))
self.assertEqual(declared_event(own)["present"], 1)
def test_the_boundary_checks_every_copy(self):
install._validate_manifest(self.manifest(), "retroarch")
bad_choices = (