fix: replay absent core claims when narrowing

This commit is contained in:
Abdessamad Derraz committed 2026-10-17 22:21:50 +02:00
1 parent 20bdd381ec
commit 0bac022c2b
5 files changed
+88 -18

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ param(
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "61ffed8fddf816c02c1b45268dfd9bfdff9daeb1da2adedf2e13d14472f8654d"
$defaultInstallSha256 = "2e9e93d9809d9276a84209cfd421ad9b0d55853ce9c9d6761beb2ef21044ef2f"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+43 -15
View File
@@ -1151,6 +1151,27 @@ _COPY_FIELDS = (
)
def _check_omission(entry: dict, dest: str) -> None:
"""The fields of a file left out: an omitted entry, or the claim of a core
with nothing held at a destination another core's copy serves."""
if not isinstance(entry.get("name"), str) or not entry["name"]:
raise ValueError(f"invalid omitted file name for {dest}")
if not isinstance(entry.get("system", ""), str):
raise ValueError(f"invalid omitted system for {dest}")
if not isinstance(entry.get("required"), bool):
raise ValueError(f"invalid omitted required flag for {dest}")
if entry.get("reason") not in OMISSION_REASONS:
raise ValueError(f"invalid omission reason for {dest}")
cores = entry.get("cores")
if cores is not None and (
not isinstance(cores, list)
or not all(isinstance(core, str) for core in cores)
):
raise ValueError(f"invalid omitted cores list for {dest}")
for field in SELECTION_FIELDS:
_check_tags(entry, field, dest)
def _check_download(entry: dict, where: str, dest: str) -> int:
"""The size, hashes, source and cores of one downloadable copy; its size."""
size = entry.get("size")
@@ -1271,6 +1292,13 @@ def _validate_manifest(data: object, plat: str) -> dict:
# Narrowing can swap in any copy: the cap holds for the largest.
size = max(size, _check_download(alternative, where, dest))
size = max(size, _check_slot(entry, f"files[{index}]", dest))
absent = entry.get("absent_claims", [])
if not isinstance(absent, list) or len(absent) > MAX_SELECTION_TAGS:
raise ValueError(f"invalid absent claims for {dest}")
for claim in absent:
if not isinstance(claim, dict):
raise ValueError(f"invalid absent claim for {dest}")
_check_omission(claim, dest)
largest_total += size
if largest_total > MAX_TOTAL_DOWNLOAD_SIZE:
raise ValueError("manifest total size exceeds safety limit")
@@ -1299,20 +1327,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
if dest in seen_destinations or dest in seen_omitted:
raise ValueError(f"duplicate or conflicting omitted destination: {dest}")
seen_omitted.add(dest)
if not isinstance(entry.get("name"), str) or not entry["name"]:
raise ValueError(f"invalid omitted file name for {dest}")
if not isinstance(entry.get("system", ""), str):
raise ValueError(f"invalid omitted system for {dest}")
if not isinstance(entry.get("required"), bool):
raise ValueError(f"invalid omitted required flag for {dest}")
if entry.get("reason") not in OMISSION_REASONS:
raise ValueError(f"invalid omission reason for {dest}")
cores = entry.get("cores")
if cores is not None and (
not isinstance(cores, list)
or not all(isinstance(core, str) for core in cores)
):
raise ValueError(f"invalid omitted cores list for {dest}")
_check_omission(entry, dest)
claims = entry.get("alternatives", [])
if not isinstance(claims, list) or len(claims) > MAX_SELECTION_TAGS:
raise ValueError(f"invalid omitted alternatives for {dest}")
@@ -1566,6 +1581,8 @@ def narrow_to_cores(
def kept_system(system: str) -> bool:
return system not in reach or bool(cores.intersection(reach[system]))
displaced: list[dict] = []
def kept(entries: list[dict]) -> list[dict]:
result: list[dict] = []
for entry in entries:
@@ -1573,6 +1590,16 @@ def narrow_to_cores(
copy = _for_cores(entry, cores)
if copy is not None:
result.append(copy)
continue
# None of the cores served here is named: a named core that
# wants the slot and has nothing held is a file left out.
claim = next(
(c for c in entry.get("absent_claims", [])
if cores.intersection(c.get("cores") or [])),
None,
)
if claim is not None:
displaced.append({"dest": entry["dest"], **claim})
continue
# The systems declaring the file, where they are fewer than the
# ones reading it.
@@ -1589,7 +1616,8 @@ def narrow_to_cores(
files = manifest.get("files", []) if files is None else files
omitted = manifest.get("omitted_files", []) if omitted is None else omitted
return kept(files), kept(omitted)
kept_files = kept(files)
return kept_files, kept(omitted) + displaced
def canonical_region(raw: str) -> str:
+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="61ffed8fddf816c02c1b45268dfd9bfdff9daeb1da2adedf2e13d14472f8654d"
DEFAULT_INSTALL_SHA256="2e9e93d9809d9276a84209cfd421ad9b0d55853ce9c9d6761beb2ef21044ef2f"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+22 -1
View File
@@ -3299,6 +3299,20 @@ def _attach_claims(
dest_hint=claim.get("destination", ""), offline=offline,
)
if claim_status in ("not_found", "external", "user_provided") or not claim_path:
# Nothing held for this core: narrowed to it without the cores
# served above, the slot is a file left out (installer replays it).
systems = _extra_system_ids(claim)
entry.setdefault("absent_claims", []).append({
"name": str(claim.get("name") or ""),
"system": systems[0] if systems else "",
"required": bool(claim.get("required", True)),
"reason": (
claim_status
if claim_status in ("not_found", "external", "user_provided")
else "not_found"
),
"cores": [claim_emu],
})
continue
claim_path, _ = validated_choice(
claim, claim_path, db, validation_index or {}, bios_dir, None, dest, owners,
@@ -3495,7 +3509,14 @@ def _manifest_core_entries(
)
manifest_files.append(entry)
omitted_by_destination.pop(full_dest, None)
displaced = omitted_by_destination.pop(full_dest, None)
if displaced and displaced.get("cores"):
# A core that wants this slot and has nothing held: a pack built
# without the cores served here leaves the slot to it, missing,
# and the installer narrowing to those cores has to say so too.
entry.setdefault("absent_claims", []).append(
{k: v for k, v in displaced.items() if k != "dest"}
)
total_size += file_size
seen_destinations.add(full_dest)
_register_path(full_dest, seen_destinations, seen_parents)
+21
View File
@@ -630,5 +630,26 @@ class InstallMatchesATargetManifest(unittest.TestCase):
)
class ACoreWithNothingHeldIsAnOmission(unittest.TestCase):
"""DOSBox serves SC-55/WAVEROM3.BIN, DOSBox Pure wants it and nothing is
held for it. A pack built for a target without DOSBox lists it as left
out; narrowed from the full manifest, it had vanished."""
MANIFEST = {"files": [{
"dest": "SC-55/WAVEROM3.BIN", "cores": ["dosbox"], "sha1": "1" * 40,
"absent_claims": [{"name": "WAVEROM3.BIN", "system": "dos", "required": False,
"reason": "not_found", "cores": ["dosbox_pure"]}],
}], "omitted_files": []}
def test_narrowing_away_the_holder_leaves_the_omission(self):
files, omitted = install.narrow_to_cores(self.MANIFEST, ["dosbox_pure"])
self.assertEqual(files, [])
self.assertEqual([o["dest"] for o in omitted], ["SC-55/WAVEROM3.BIN"])
files, omitted = install.narrow_to_cores(self.MANIFEST, ["dosbox", "dosbox_pure"])
self.assertEqual(len(files), 1)
self.assertEqual(omitted, [])
if __name__ == "__main__":
unittest.main()