fix: let target installs match their target pack

This commit is contained in:
Abdessamad Derraz committed 2026-10-17 00:39:57 +02:00
1 parent 0a5e15789f
commit e659228777
12 files changed
+1326 -70

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 = "e0d8fad4e7e55afa57c412e98726992d82b92286b752c9a8cce0978f0a355127"
$defaultInstallSha256 = "953240dab33c25e52005155ecc8cd694269234697ca2c3f55939994fc6b61951"
$maximumInstallerBytes = 2MB
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
$temporary = $null
+81 -7
View File
@@ -1154,6 +1154,47 @@ def _check_download(entry: dict, where: str, dest: str) -> int:
return size
def _check_slot(entry: dict, where: str, dest: str) -> int:
"""The copies a platform file takes as cores and systems drop; the largest size."""
choices = entry.get("slot_choices", [])
declared = entry.get("declared_choices")
if declared is None and not choices:
return 0
if (
entry.get("cores") is not None
or not isinstance(choices, list)
or not isinstance(declared, list)
or not declared
or len(choices) > MAX_SELECTION_TAGS
or len(declared) > MAX_SELECTION_TAGS
):
raise ValueError(f"invalid slot choices for {dest}")
size = 0
for index, choice in enumerate(declared):
systems = choice.get("systems") if isinstance(choice, dict) else None
if (
not isinstance(systems, list)
or not systems
or not all(isinstance(s, str) for s in systems)
or not set(choice) <= (set(_COPY_FIELDS) - {"cores"}) | {"systems"}
):
raise ValueError(f"invalid declared choice for {dest}")
size = max(size, _check_download(choice, f"{where}.declared_choices[{index}]", dest))
for index, choice in enumerate(choices):
if (
not isinstance(choice, dict)
or not choice.get("cores")
or not set(choice) <= set(_COPY_FIELDS)
):
raise ValueError(f"invalid slot choice for {dest}")
# A choice naming only cores leaves the platform's own answer.
if set(choice) != {"cores"}:
size = max(size, _check_download(
choice, f"{where}.slot_choices[{index}]", dest
))
return size
def _validate_manifest(data: object, plat: str) -> dict:
"""Validate the untrusted install-manifest boundary using stdlib only."""
if not isinstance(data, dict):
@@ -1192,6 +1233,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
raise ValueError(f"invalid alternative for {dest}")
# 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))
largest_total += size
if largest_total > MAX_TOTAL_DOWNLOAD_SIZE:
raise ValueError("manifest total size exceeds safety limit")
@@ -1397,13 +1439,44 @@ def _for_cores(entry: dict, chosen: set[str]) -> dict | None:
if chosen.intersection(alternative["cores"]):
kept = {
k: v for k, v in entry.items()
if k not in _COPY_FIELDS and k != "alternatives"
if k not in _COPY_FIELDS
and k not in ("alternatives", "slot_choices", "declared_choices")
}
kept.update((k, alternative[k]) for k in _COPY_FIELDS if k in alternative)
return kept
return None
def _settle_slot(entry: dict, chosen: set[str], kept_system=None) -> dict:
"""A platform file as a pack built for the chosen cores carries it.
Where a core's own source contradicts the platform and the frontend only
checks the path, the pack serves the core's copy: the first of
`slot_choices` naming a chosen core decides. Otherwise the platform
answers, through the first of `declared_choices` whose systems remain.
"""
declared = entry.get("declared_choices")
if declared is None:
return entry
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],
)
kept = {
k: v for k, v in entry.items()
if k not in _COPY_FIELDS and k not in ("slot_choices", "declared_choices")
}
kept.update((k, copy[k]) for k in _COPY_FIELDS if k in copy and k != "cores")
kept["cores"] = None
return kept
def _filter_by_target(
files: list[dict], target_cores: list[str]
) -> list[dict]:
@@ -1412,7 +1485,7 @@ def _filter_by_target(
result: list[dict] = []
for f in files:
if f.get("cores") is None:
result.append(f)
result.append(_settle_slot(f, chosen))
continue
kept = _for_cores(f, chosen)
if kept is not None:
@@ -1453,6 +1526,9 @@ def narrow_to_cores(
cores = designated_cores(manifest, names)
reach = manifest.get("system_cores") or {}
def kept_system(system: str) -> bool:
return system not in reach or bool(cores.intersection(reach[system]))
def kept(entries: list[dict]) -> list[dict]:
result: list[dict] = []
for entry in entries:
@@ -1464,10 +1540,8 @@ def narrow_to_cores(
# The systems declaring the file, where they are fewer than the
# ones reading it.
systems = entry.get("declared_in") or entry_systems(entry)
if not systems or any(
s not in reach or cores.intersection(reach[s]) for s in systems
):
result.append(entry)
if not systems or any(kept_system(s) for s in systems):
result.append(_settle_slot(entry, cores, kept_system))
continue
# The system is dropped, which frees its destination for the
# first named core claiming it, as in a pack built for them.
@@ -1622,7 +1696,7 @@ def available_choices(files: list[dict], omitted: list[dict]) -> dict[str, list[
ordered.append(OTHER_SYSTEMS)
cores = {
c for f in files
for copy in [f, *f.get("alternatives", [])]
for copy in [f, *f.get("alternatives", []), *f.get("slot_choices", [])]
for c in (copy.get("cores") or [])
}
regions = {
+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="e0d8fad4e7e55afa57c412e98726992d82b92286b752c9a8cce0978f0a355127"
DEFAULT_INSTALL_SHA256="953240dab33c25e52005155ecc8cd694269234697ca2c3f55939994fc6b61951"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+226
View File
@@ -1536,6 +1536,28 @@
"systems": [
"arcade"
],
"slot_choices": [
{
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
"sha256": "13d503ea6c7e7b9a2249837cf915bff6df862b7035f5c99b18240088ec10854a",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/.variants/nmk004.zip.f70b6562",
"cores": [
"fbneo"
]
}
],
"declared_choices": [
{
"sha1": "489256f5e2001070d2ad94c90d255282c71ed274",
"sha256": "6ca3142ebe127f5d2cb755c53740a92d9bf1574bde8daa32642de2991af89aaa",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/nmk004.zip",
"systems": [
"arcade"
]
}
],
"alternatives": [
{
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
@@ -1850,6 +1872,29 @@
"arcade",
"snk-neogeo-cd"
],
"slot_choices": [
{
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": [
"fbneo",
"fbneo_neogeo"
]
}
],
"declared_choices": [
{
"sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc",
"sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667",
"size": 214681,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31",
"systems": [
"arcade"
]
}
],
"declared_in": [
"arcade"
],
@@ -2044,6 +2089,28 @@
"atari-400-800",
"atari-5200"
],
"slot_choices": [
{
"sha1": "db1031585968cfc6ec2ecda5c9a5a52f61444a3b",
"sha256": "63973d5e86b1ae5b6fca5b119a7f7589e817e19acc3ca54e54462c9dfb619e73",
"size": 10240,
"repo_path": "bios/Atari/400-800/ROM_OS_B_NTSC",
"cores": [
"atari800"
]
}
],
"declared_choices": [
{
"sha1": "f1f0741b1d34fb4350cf7cb8ab3b6ea11cdd8174",
"sha256": "bc6012f8ca2f7dde21ae895899b1d2af572ffaa28f819b84911ddb6a0bd8a5c7",
"size": 10240,
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
"systems": [
"atari-400-800"
]
}
],
"declared_in": [
"atari-400-800"
],
@@ -5224,6 +5291,38 @@
"nintendo-ds",
"nintendo-dsi"
],
"slot_choices": [
{
"sha1": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53",
"sha256": "12a03016ba07cfd874fc002df49a2c6eb2414eb7cfa395c2043035ec5e9193e8",
"size": 65536,
"repo_path": "bios/Nintendo/DS/NDS_Bios7i.bin",
"cores": [
"melonds",
"melonds_ds"
]
}
],
"declared_choices": [
{
"sha1": "a3aa751eb6bdaaf8a827ba9e03576a6f1ab0f547",
"sha256": "2946281e730e71f7cafdb125f5cb60fed944ca5d610ee1e082c441b602b5f4e2",
"size": 65536,
"repo_path": "bios/Nintendo/Nintendo DS/biosdsi7.bin",
"systems": [
"nintendo-dsi"
]
},
{
"sha1": "d2b17224e156ee5e55b5d8d9f323b1c9b86dd951",
"sha256": "28a13952901b4065ef3f08c3252b49a00b6525e301b7ddaa3a390b4c7cbef901",
"size": 65536,
"repo_path": "bios/Nintendo/DS/dsi_bios7.bin",
"systems": [
"nintendo-ds"
]
}
],
"alternatives": [
{
"sha1": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53",
@@ -5248,6 +5347,38 @@
"nintendo-ds",
"nintendo-dsi"
],
"slot_choices": [
{
"sha1": "719b9eef33692406d7170ff526069615759c4dfc",
"sha256": "6e9f70b51dc4f26289b597f14e2513620c2af964374a95f4c5a62b5cad199afc",
"size": 65536,
"repo_path": "bios/Nintendo/DS/NDS_Bios9i.bin",
"cores": [
"melonds",
"melonds_ds"
]
}
],
"declared_choices": [
{
"sha1": "7bf549b8be9e48ab0cdc9b0fdadd49a5131f97eb",
"sha256": "47538922a8e8a8e79b922ff1203863ef5c40d9c54656a8d2c89c56ece52029ce",
"size": 65536,
"repo_path": "bios/Nintendo/Nintendo DS/biosdsi9.bin",
"systems": [
"nintendo-dsi"
]
},
{
"sha1": "db61fa39ddbc5f5ed71fb19cda47609ef0201723",
"sha256": "664c608bd6ccfe583e87b3b924afa00f8697dc7ca92eeb6d206d68abb03d2e10",
"size": 65536,
"repo_path": "bios/Nintendo/DS/dsi_bios9.bin",
"systems": [
"nintendo-ds"
]
}
],
"alternatives": [
{
"sha1": "719b9eef33692406d7170ff526069615759c4dfc",
@@ -9539,6 +9670,38 @@
"systems": [
"snk-neogeo-cd"
],
"slot_choices": [
{
"sha1": "712816a3c41baf4129e23d645305a536e03d69de",
"sha256": "f885f2cc79b5c0120ca316b0db360183c2b9a60bc727cbe211b7518e12bd5548",
"size": 955352,
"repo_path": "bios/SNK/Neo Geo CD/.variants/neocdz.zip.712816a3",
"cores": [
"geolith"
]
},
{
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": [
"fbneo",
"fbneo_neogeo"
]
}
],
"declared_choices": [
{
"sha1": "9cab31cfe7eacb6871d36cee105d87b90fd85b64",
"sha256": "d828241905d390c5de4867646296f615c95bca2f5d8f7e81d50e08717373a821",
"size": 1181333,
"repo_path": "bios/Arcade/Arcade/neocdz.zip",
"systems": [
"snk-neogeo-cd"
]
}
],
"alternatives": [
{
"sha1": "712816a3c41baf4129e23d645305a536e03d69de",
@@ -9831,6 +9994,29 @@
"region_system_groups": [
"sony-playstation"
],
"slot_choices": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
"sha256": "0c8359870cbac0ea091f1c87f188cd332dcc709753b91cafd9fd44a4a6188197",
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5000.bin",
"cores": [
"duckstation",
"rustation"
]
}
],
"declared_choices": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
"sha256": "0c8359870cbac0ea091f1c87f188cd332dcc709753b91cafd9fd44a4a6188197",
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5000.bin",
"systems": [
"sony-playstation"
]
}
],
"alternatives": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
@@ -11675,6 +11861,46 @@
"systems": [
"sony-playstation-2"
],
"slot_choices": [
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
"sha256": "61c77b789ccde856096f024e4a3eef6891e15ca59c127e0dd13e762a7d338a7e",
"size": 2690355,
"repo_path": "bios/Other/armsx2/pcsx2/resources/GameIndex.yaml",
"cores": [
"armsx2"
]
},
{
"sha1": "7efb2ed721cf0c0f0677324e09be311a8337e793",
"sha256": "3bf3c54d6f16d88d3669be698179bb8c2fb981ee5ce71bcb952719429a838cc1",
"size": 2693579,
"repo_path": "bios/Other/yaps2/GameIndex.yaml",
"cores": [
"yaps2"
]
},
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
"sha256": "61c77b789ccde856096f024e4a3eef6891e15ca59c127e0dd13e762a7d338a7e",
"size": 2690355,
"repo_path": "bios/Other/armsx2/pcsx2/resources/GameIndex.yaml",
"cores": [
"lrps2"
]
}
],
"declared_choices": [
{
"sha1": "57369df9ca82a460e596cea7835c5a0797dee668",
"sha256": "efa94cb7b9208dc6812bb9407dc986560e523cce57f511eff30942f76b7288bf",
"size": 1737799,
"repo_path": "bios/Sony/PlayStation 2/.variants/GameIndex.yaml.57369df9",
"systems": [
"sony-playstation-2"
]
}
],
"alternatives": [
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
+226
View File
@@ -1775,6 +1775,28 @@
"systems": [
"arcade"
],
"slot_choices": [
{
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
"sha256": "13d503ea6c7e7b9a2249837cf915bff6df862b7035f5c99b18240088ec10854a",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/.variants/nmk004.zip.f70b6562",
"cores": [
"fbneo"
]
}
],
"declared_choices": [
{
"sha1": "489256f5e2001070d2ad94c90d255282c71ed274",
"sha256": "6ca3142ebe127f5d2cb755c53740a92d9bf1574bde8daa32642de2991af89aaa",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/nmk004.zip",
"systems": [
"arcade"
]
}
],
"alternatives": [
{
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
@@ -2089,6 +2111,29 @@
"arcade",
"snk-neogeo-cd"
],
"slot_choices": [
{
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": [
"fbneo",
"fbneo_neogeo"
]
}
],
"declared_choices": [
{
"sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc",
"sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667",
"size": 214681,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31",
"systems": [
"arcade"
]
}
],
"declared_in": [
"arcade"
],
@@ -2283,6 +2328,28 @@
"atari-400-800",
"atari-5200"
],
"slot_choices": [
{
"sha1": "db1031585968cfc6ec2ecda5c9a5a52f61444a3b",
"sha256": "63973d5e86b1ae5b6fca5b119a7f7589e817e19acc3ca54e54462c9dfb619e73",
"size": 10240,
"repo_path": "bios/Atari/400-800/ROM_OS_B_NTSC",
"cores": [
"atari800"
]
}
],
"declared_choices": [
{
"sha1": "f1f0741b1d34fb4350cf7cb8ab3b6ea11cdd8174",
"sha256": "bc6012f8ca2f7dde21ae895899b1d2af572ffaa28f819b84911ddb6a0bd8a5c7",
"size": 10240,
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
"systems": [
"atari-400-800"
]
}
],
"declared_in": [
"atari-400-800"
],
@@ -5463,6 +5530,38 @@
"nintendo-ds",
"nintendo-dsi"
],
"slot_choices": [
{
"sha1": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53",
"sha256": "12a03016ba07cfd874fc002df49a2c6eb2414eb7cfa395c2043035ec5e9193e8",
"size": 65536,
"repo_path": "bios/Nintendo/DS/NDS_Bios7i.bin",
"cores": [
"melonds",
"melonds_ds"
]
}
],
"declared_choices": [
{
"sha1": "a3aa751eb6bdaaf8a827ba9e03576a6f1ab0f547",
"sha256": "2946281e730e71f7cafdb125f5cb60fed944ca5d610ee1e082c441b602b5f4e2",
"size": 65536,
"repo_path": "bios/Nintendo/Nintendo DS/biosdsi7.bin",
"systems": [
"nintendo-dsi"
]
},
{
"sha1": "d2b17224e156ee5e55b5d8d9f323b1c9b86dd951",
"sha256": "28a13952901b4065ef3f08c3252b49a00b6525e301b7ddaa3a390b4c7cbef901",
"size": 65536,
"repo_path": "bios/Nintendo/DS/dsi_bios7.bin",
"systems": [
"nintendo-ds"
]
}
],
"alternatives": [
{
"sha1": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53",
@@ -5487,6 +5586,38 @@
"nintendo-ds",
"nintendo-dsi"
],
"slot_choices": [
{
"sha1": "719b9eef33692406d7170ff526069615759c4dfc",
"sha256": "6e9f70b51dc4f26289b597f14e2513620c2af964374a95f4c5a62b5cad199afc",
"size": 65536,
"repo_path": "bios/Nintendo/DS/NDS_Bios9i.bin",
"cores": [
"melonds",
"melonds_ds"
]
}
],
"declared_choices": [
{
"sha1": "7bf549b8be9e48ab0cdc9b0fdadd49a5131f97eb",
"sha256": "47538922a8e8a8e79b922ff1203863ef5c40d9c54656a8d2c89c56ece52029ce",
"size": 65536,
"repo_path": "bios/Nintendo/Nintendo DS/biosdsi9.bin",
"systems": [
"nintendo-dsi"
]
},
{
"sha1": "db61fa39ddbc5f5ed71fb19cda47609ef0201723",
"sha256": "664c608bd6ccfe583e87b3b924afa00f8697dc7ca92eeb6d206d68abb03d2e10",
"size": 65536,
"repo_path": "bios/Nintendo/DS/dsi_bios9.bin",
"systems": [
"nintendo-ds"
]
}
],
"alternatives": [
{
"sha1": "719b9eef33692406d7170ff526069615759c4dfc",
@@ -9778,6 +9909,38 @@
"systems": [
"snk-neogeo-cd"
],
"slot_choices": [
{
"sha1": "712816a3c41baf4129e23d645305a536e03d69de",
"sha256": "f885f2cc79b5c0120ca316b0db360183c2b9a60bc727cbe211b7518e12bd5548",
"size": 955352,
"repo_path": "bios/SNK/Neo Geo CD/.variants/neocdz.zip.712816a3",
"cores": [
"geolith"
]
},
{
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": [
"fbneo",
"fbneo_neogeo"
]
}
],
"declared_choices": [
{
"sha1": "9cab31cfe7eacb6871d36cee105d87b90fd85b64",
"sha256": "d828241905d390c5de4867646296f615c95bca2f5d8f7e81d50e08717373a821",
"size": 1181333,
"repo_path": "bios/Arcade/Arcade/neocdz.zip",
"systems": [
"snk-neogeo-cd"
]
}
],
"alternatives": [
{
"sha1": "712816a3c41baf4129e23d645305a536e03d69de",
@@ -10070,6 +10233,29 @@
"region_system_groups": [
"sony-playstation"
],
"slot_choices": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
"sha256": "0c8359870cbac0ea091f1c87f188cd332dcc709753b91cafd9fd44a4a6188197",
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5000.bin",
"cores": [
"duckstation",
"rustation"
]
}
],
"declared_choices": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
"sha256": "0c8359870cbac0ea091f1c87f188cd332dcc709753b91cafd9fd44a4a6188197",
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5000.bin",
"systems": [
"sony-playstation"
]
}
],
"alternatives": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
@@ -11914,6 +12100,46 @@
"systems": [
"sony-playstation-2"
],
"slot_choices": [
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
"sha256": "61c77b789ccde856096f024e4a3eef6891e15ca59c127e0dd13e762a7d338a7e",
"size": 2690355,
"repo_path": "bios/Other/armsx2/pcsx2/resources/GameIndex.yaml",
"cores": [
"armsx2"
]
},
{
"sha1": "7efb2ed721cf0c0f0677324e09be311a8337e793",
"sha256": "3bf3c54d6f16d88d3669be698179bb8c2fb981ee5ce71bcb952719429a838cc1",
"size": 2693579,
"repo_path": "bios/Other/yaps2/GameIndex.yaml",
"cores": [
"yaps2"
]
},
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
"sha256": "61c77b789ccde856096f024e4a3eef6891e15ca59c127e0dd13e762a7d338a7e",
"size": 2690355,
"repo_path": "bios/Other/armsx2/pcsx2/resources/GameIndex.yaml",
"cores": [
"lrps2"
]
}
],
"declared_choices": [
{
"sha1": "57369df9ca82a460e596cea7835c5a0797dee668",
"sha256": "efa94cb7b9208dc6812bb9407dc986560e523cce57f511eff30942f76b7288bf",
"size": 1737799,
"repo_path": "bios/Sony/PlayStation 2/.variants/GameIndex.yaml.57369df9",
"systems": [
"sony-playstation-2"
]
}
],
"alternatives": [
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
+80
View File
@@ -6570,6 +6570,26 @@
"arcade",
"philips-cdi"
],
"declared_choices": [
{
"sha1": "e7d2a0dad62d6f75bc10f48a376da0a99b764571",
"sha256": "aa8a33813531795de9739b06b65960e0f86cdec504c8452a956d184eac3cdcd4",
"size": 468660,
"repo_path": "bios/Arcade/Arcade/.variants/cdibios.zip.e7d2a0da",
"systems": [
"arcade"
]
},
{
"sha1": "16072afaa65d1b059346616ac5b5a600c63ff1d1",
"sha256": "b53d206ae81607bf6866411b720ad82ce75122423058ce08c6df84cdc1ca3496",
"size": 472798,
"repo_path": "bios/Philips/CD-i/cdibios.zip",
"systems": [
"philips-cdi"
]
}
],
"alternatives": [
{
"sha1": "16072afaa65d1b059346616ac5b5a600c63ff1d1",
@@ -17884,6 +17904,26 @@
"arcade",
"sega-saturn"
],
"declared_choices": [
{
"sha1": "330a2ab5ce2e2c29987b2086e021de7aa04aa102",
"sha256": "bbdf6a9ef4bad6bd9ad16f9f7669071dd4ee3bb2387d49124f024ea8dd750f88",
"size": 2699375,
"repo_path": "bios/Arcade/MAME/.variants/stvbios.zip.330a2ab5",
"systems": [
"arcade"
]
},
{
"sha1": "7783ab663bb1a8d62ff60d583674355c2af556b1",
"sha256": "38ceb2792c72770f56aadb1865cf66dc33a3017db1ee05e11c2b2dff397ad39e",
"size": 2955630,
"repo_path": "bios/Arcade/MAME/stvbios.zip",
"systems": [
"sega-saturn"
]
}
],
"alternatives": [
{
"sha1": "b14bbed4bf459439cf7e96d2f79d7adea5d31448",
@@ -20260,6 +20300,26 @@
"atari-400-800",
"atari-5200"
],
"declared_choices": [
{
"sha1": "f1f0741b1d34fb4350cf7cb8ab3b6ea11cdd8174",
"sha256": "bc6012f8ca2f7dde21ae895899b1d2af572ffaa28f819b84911ddb6a0bd8a5c7",
"size": 10240,
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
"systems": [
"atari-400-800"
]
},
{
"sha1": "db1031585968cfc6ec2ecda5c9a5a52f61444a3b",
"sha256": "63973d5e86b1ae5b6fca5b119a7f7589e817e19acc3ca54e54462c9dfb619e73",
"size": 10240,
"repo_path": "bios/Atari/400-800/ROM_OS_B_NTSC",
"systems": [
"atari-5200"
]
}
],
"alternatives": [
{
"sha1": "db1031585968cfc6ec2ecda5c9a5a52f61444a3b",
@@ -23088,6 +23148,26 @@
"systems": [
"nec-pc-88",
"tandy-coco"
],
"declared_choices": [
{
"sha1": "bb7103a0818850a039c67ff666a31ce49a8d516f",
"sha256": "9dc63118728a171ce2ed437ad38ef280b8b7bcf4a5a95fedf18946501b010456",
"size": 2048,
"repo_path": "bios/NEC/PC-88/N88SUB.ROM",
"systems": [
"nec-pc-88"
]
},
{
"sha1": "892b85cc96f0ff31cae8ad8d409b9f6c04b32e5e",
"sha256": "b9b4bfe42b4957d4c6b532ceb5a1946c9e4d9d4f55a6e3eb600cc7f236e09ca9",
"size": 8194,
"repo_path": "bios/Tandy/CoCo/disk.rom",
"systems": [
"tandy-coco"
]
}
]
},
{
+308
View File
@@ -1553,6 +1553,28 @@
"systems": [
"arcade"
],
"slot_choices": [
{
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
"sha256": "13d503ea6c7e7b9a2249837cf915bff6df862b7035f5c99b18240088ec10854a",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/.variants/nmk004.zip.f70b6562",
"cores": [
"fbneo"
]
}
],
"declared_choices": [
{
"sha1": "489256f5e2001070d2ad94c90d255282c71ed274",
"sha256": "6ca3142ebe127f5d2cb755c53740a92d9bf1574bde8daa32642de2991af89aaa",
"size": 3556,
"repo_path": "bios/Arcade/Arcade/nmk004.zip",
"systems": [
"arcade"
]
}
],
"alternatives": [
{
"sha1": "9f84f563df14ea1a1bcaaf05b720c3f5545e5a83",
@@ -1867,6 +1889,29 @@
"arcade",
"snk-neogeo-cd"
],
"slot_choices": [
{
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": [
"fbneo",
"fbneo_neogeo"
]
}
],
"declared_choices": [
{
"sha1": "4f28af31ca0defdd73d80edec2fa296908e624dc",
"sha256": "6aa507a5dc33c45d2592bf2bc77eb3e497762221ddd9225f18583a5c55d38667",
"size": 214681,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.4f28af31",
"systems": [
"arcade"
]
}
],
"declared_in": [
"arcade"
],
@@ -2061,6 +2106,28 @@
"atari-400-800",
"atari-5200"
],
"slot_choices": [
{
"sha1": "db1031585968cfc6ec2ecda5c9a5a52f61444a3b",
"sha256": "63973d5e86b1ae5b6fca5b119a7f7589e817e19acc3ca54e54462c9dfb619e73",
"size": 10240,
"repo_path": "bios/Atari/400-800/ROM_OS_B_NTSC",
"cores": [
"atari800"
]
}
],
"declared_choices": [
{
"sha1": "f1f0741b1d34fb4350cf7cb8ab3b6ea11cdd8174",
"sha256": "bc6012f8ca2f7dde21ae895899b1d2af572ffaa28f819b84911ddb6a0bd8a5c7",
"size": 10240,
"repo_path": "bios/Atari/400-800/ATARIOSB.ROM",
"systems": [
"atari-400-800"
]
}
],
"declared_in": [
"atari-400-800"
],
@@ -2346,6 +2413,34 @@
"systems": [
"coleco-colecovision"
],
"slot_choices": [
{
"cores": [
"jollycv",
"openmsx"
]
},
{
"sha1": "99ba9be24ada3e86e5c17aeecb7a2d68c5edfe59",
"sha256": "25cdb7ff4ad675ac25e11663e9e9472afc1c7cf59ba40c330081abda9881a968",
"size": 8192,
"repo_path": "bios/Coleco/ColecoVision/colecoa.rom",
"cores": [
"zesarux"
]
}
],
"declared_choices": [
{
"sha1": "45bedc4cbdeac66c7df59e9e599195c778d86a92",
"sha256": "990bf1956f10207d8781b619eb74f89b00d921c8d45c95c334c16c8cceca09ad",
"size": 8192,
"repo_path": "bios/Coleco/ColecoVision/BIOS.col",
"systems": [
"coleco-colecovision"
]
}
],
"alternatives": [
{
"sha1": "45bedc4cbdeac66c7df59e9e599195c778d86a92",
@@ -4113,6 +4208,33 @@
"systems": [
"microsoft-msx"
],
"slot_choices": [
{
"cores": [
"fmsx"
]
},
{
"sha1": "fec451b9256570a0e4869355a40329c57f40705f",
"sha256": "a3a6a325935d031cee662b56e2c810c73d35a8375a2c5ffa03f6d27405bc569e",
"size": 65536,
"repo_path": "bios/Microsoft/MSX/openmsx/fmpac.rom",
"cores": [
"openmsx"
]
}
],
"declared_choices": [
{
"sha1": "9d789166e3caf28e4742fe933d962e99618c633d",
"sha256": "3cfe44646b69a622656b96305f1b975f48ca018839f7946895f7a1352d9720aa",
"size": 65536,
"repo_path": "bios/Microsoft/MSX/FMPAC.ROM",
"systems": [
"microsoft-msx"
]
}
],
"alternatives": [
{
"sha1": "9d789166e3caf28e4742fe933d962e99618c633d",
@@ -4190,6 +4312,33 @@
"systems": [
"microsoft-msx"
],
"slot_choices": [
{
"cores": [
"fmsx"
]
},
{
"sha1": "e998f0c441f4f1800ef44e42cd1659150206cf79",
"sha256": "1c85dac5536fa3ba6f2cb70deba02ff680b34ac6cc787d2977258bd663a99555",
"size": 32768,
"repo_path": "bios/Microsoft/MSX/MSX_8020-20bios.rom",
"cores": [
"zesarux"
]
}
],
"declared_choices": [
{
"sha1": "409e82adac40f6bdd18eb6c84e8b2fbdc7fb5498",
"sha256": "999564a371dd2fdf7fbe8d853e82a68d557c27b7d87417639b2fa17704b83f78",
"size": 32768,
"repo_path": "bios/Microsoft/MSX/MSX.ROM",
"systems": [
"microsoft-msx"
]
}
],
"alternatives": [
{
"sha1": "409e82adac40f6bdd18eb6c84e8b2fbdc7fb5498",
@@ -5250,6 +5399,38 @@
"nintendo-ds",
"nintendo-dsi"
],
"slot_choices": [
{
"sha1": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53",
"sha256": "12a03016ba07cfd874fc002df49a2c6eb2414eb7cfa395c2043035ec5e9193e8",
"size": 65536,
"repo_path": "bios/Nintendo/DS/NDS_Bios7i.bin",
"cores": [
"melonds",
"melonds_ds"
]
}
],
"declared_choices": [
{
"sha1": "a3aa751eb6bdaaf8a827ba9e03576a6f1ab0f547",
"sha256": "2946281e730e71f7cafdb125f5cb60fed944ca5d610ee1e082c441b602b5f4e2",
"size": 65536,
"repo_path": "bios/Nintendo/Nintendo DS/biosdsi7.bin",
"systems": [
"nintendo-dsi"
]
},
{
"sha1": "d2b17224e156ee5e55b5d8d9f323b1c9b86dd951",
"sha256": "28a13952901b4065ef3f08c3252b49a00b6525e301b7ddaa3a390b4c7cbef901",
"size": 65536,
"repo_path": "bios/Nintendo/DS/dsi_bios7.bin",
"systems": [
"nintendo-ds"
]
}
],
"alternatives": [
{
"sha1": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53",
@@ -5274,6 +5455,38 @@
"nintendo-ds",
"nintendo-dsi"
],
"slot_choices": [
{
"sha1": "719b9eef33692406d7170ff526069615759c4dfc",
"sha256": "6e9f70b51dc4f26289b597f14e2513620c2af964374a95f4c5a62b5cad199afc",
"size": 65536,
"repo_path": "bios/Nintendo/DS/NDS_Bios9i.bin",
"cores": [
"melonds",
"melonds_ds"
]
}
],
"declared_choices": [
{
"sha1": "7bf549b8be9e48ab0cdc9b0fdadd49a5131f97eb",
"sha256": "47538922a8e8a8e79b922ff1203863ef5c40d9c54656a8d2c89c56ece52029ce",
"size": 65536,
"repo_path": "bios/Nintendo/Nintendo DS/biosdsi9.bin",
"systems": [
"nintendo-dsi"
]
},
{
"sha1": "db61fa39ddbc5f5ed71fb19cda47609ef0201723",
"sha256": "664c608bd6ccfe583e87b3b924afa00f8697dc7ca92eeb6d206d68abb03d2e10",
"size": 65536,
"repo_path": "bios/Nintendo/DS/dsi_bios9.bin",
"systems": [
"nintendo-ds"
]
}
],
"alternatives": [
{
"sha1": "719b9eef33692406d7170ff526069615759c4dfc",
@@ -9587,6 +9800,38 @@
"systems": [
"snk-neogeo-cd"
],
"slot_choices": [
{
"sha1": "712816a3c41baf4129e23d645305a536e03d69de",
"sha256": "f885f2cc79b5c0120ca316b0db360183c2b9a60bc727cbe211b7518e12bd5548",
"size": 955352,
"repo_path": "bios/SNK/Neo Geo CD/.variants/neocdz.zip.712816a3",
"cores": [
"geolith"
]
},
{
"sha1": "985d935c06fe278ce39d7690aa6c53df88954cd6",
"sha256": "4919ba2a7650b24d1bf2c9107c89a80d93b50703a0a371fa5113c791bd03e114",
"size": 1184219,
"repo_path": "bios/Arcade/Arcade/.variants/neocdz.zip.ba20a7be",
"cores": [
"fbneo",
"fbneo_neogeo"
]
}
],
"declared_choices": [
{
"sha1": "9cab31cfe7eacb6871d36cee105d87b90fd85b64",
"sha256": "d828241905d390c5de4867646296f615c95bca2f5d8f7e81d50e08717373a821",
"size": 1181333,
"repo_path": "bios/Arcade/Arcade/neocdz.zip",
"systems": [
"snk-neogeo-cd"
]
}
],
"alternatives": [
{
"sha1": "712816a3c41baf4129e23d645305a536e03d69de",
@@ -9879,6 +10124,29 @@
"region_system_groups": [
"sony-playstation"
],
"slot_choices": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
"sha256": "0c8359870cbac0ea091f1c87f188cd332dcc709753b91cafd9fd44a4a6188197",
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5000.bin",
"cores": [
"duckstation",
"rustation"
]
}
],
"declared_choices": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
"sha256": "0c8359870cbac0ea091f1c87f188cd332dcc709753b91cafd9fd44a4a6188197",
"size": 524288,
"repo_path": "bios/Sony/PlayStation/scph5000.bin",
"systems": [
"sony-playstation"
]
}
],
"alternatives": [
{
"sha1": "ffa7f9a7fb19d773a0c3985a541c8e5623d2c30d",
@@ -11723,6 +11991,46 @@
"systems": [
"sony-playstation-2"
],
"slot_choices": [
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
"sha256": "61c77b789ccde856096f024e4a3eef6891e15ca59c127e0dd13e762a7d338a7e",
"size": 2690355,
"repo_path": "bios/Other/armsx2/pcsx2/resources/GameIndex.yaml",
"cores": [
"armsx2"
]
},
{
"sha1": "7efb2ed721cf0c0f0677324e09be311a8337e793",
"sha256": "3bf3c54d6f16d88d3669be698179bb8c2fb981ee5ce71bcb952719429a838cc1",
"size": 2693579,
"repo_path": "bios/Other/yaps2/GameIndex.yaml",
"cores": [
"yaps2"
]
},
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
"sha256": "61c77b789ccde856096f024e4a3eef6891e15ca59c127e0dd13e762a7d338a7e",
"size": 2690355,
"repo_path": "bios/Other/armsx2/pcsx2/resources/GameIndex.yaml",
"cores": [
"lrps2"
]
}
],
"declared_choices": [
{
"sha1": "57369df9ca82a460e596cea7835c5a0797dee668",
"sha256": "efa94cb7b9208dc6812bb9407dc986560e523cce57f511eff30942f76b7288bf",
"size": 1737799,
"repo_path": "bios/Sony/PlayStation 2/.variants/GameIndex.yaml.57369df9",
"systems": [
"sony-playstation-2"
]
}
],
"alternatives": [
{
"sha1": "72bfdf91fa11a86e2810a7c167cdcf11e20418a8",
+98
View File
@@ -482,6 +482,104 @@
}
}
},
"slot_choices": {
"description": "On a platform file a core's own source contradicts while the frontend only checks the path: the copy each core's presence puts there, in the order the pack builder settles the slot. A narrowing serves the first whose cores remain; one naming only cores, or none remaining, leaves `declared_choices`.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"cores"
],
"properties": {
"sha1": {
"type": "string",
"pattern": "^[a-f0-9]{40}$"
},
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"size": {
"type": "integer",
"minimum": 0
},
"repo_path": {
"type": "string"
},
"storage": {
"const": "release"
},
"release_asset": {
"type": "string",
"pattern": "^[^/\\\\]+$"
},
"cores": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
},
"executable": {
"type": "boolean"
}
}
}
},
"declared_choices": {
"description": "The platform's own answer at the destination by the declaring systems that remain, in the order the pack builder takes the declarations; a narrowing serves the first whose systems remain.",
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"sha1",
"sha256",
"size",
"repo_path",
"systems"
],
"properties": {
"sha1": {
"type": "string",
"pattern": "^[a-f0-9]{40}$"
},
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"size": {
"type": "integer",
"minimum": 0
},
"repo_path": {
"type": "string"
},
"storage": {
"const": "release"
},
"release_asset": {
"type": "string",
"pattern": "^[^/\\\\]+$"
},
"executable": {
"type": "boolean"
},
"systems": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"minItems": 1,
"uniqueItems": true
}
}
}
},
"executable": {
"description": "The repository keeps the file executable; the installer gives it the execute bits the umask allows.",
"type": "boolean"
+153 -36
View File
@@ -3160,6 +3160,66 @@ def _attach_claims(
entry.pop("alternatives", None)
def _record_slot_choices(
entry: dict,
choices: list[tuple[str, str | None]],
declarations: list[tuple[list[str], dict, str]],
settle,
dest: str,
db: dict,
repo_root: str,
) -> None:
"""Record what a pack built for fewer cores serves at a platform destination.
`slot_choices` lists, in the order slots decides, the cores whose claim
settles the slot and the copy it serves, a choice without a copy leaving
the platform's answer. `declared_choices` is the platform's answer by the
declaring systems that remain, in the order the builder takes them.
Nothing is recorded where every narrowing gets the entry's own copy.
"""
def download(file_entry: dict, sys_id: str, override: str | None) -> dict | None:
local_path, _status = settle(file_entry, sys_id, dest, override)
if local_path is None:
return None
copy, _size = _manifest_entry(local_path, dest, None, db, repo_root, {})
if copy is None:
return None
return {k: v for k, v in copy.items() if k not in ("dest", "cores")}
declared: list[dict] = []
for systems, file_entry, sys_id in declarations:
copy = download(file_entry, sys_id, None)
if copy is None:
# An omission is not a copy the installer can switch to.
return
if declared and declared[-1]["sha1"] == copy["sha1"]:
declared[-1]["systems"].extend(systems)
else:
declared.append({**copy, "systems": list(systems)})
_systems, first, first_sys = declarations[0]
recorded: list[dict] = []
seen: set[str] = set()
for emulator, path in choices:
if emulator in seen:
continue
seen.add(emulator)
copy = {} if path is None else download(first, first_sys, path)
if copy is None:
return
if recorded and recorded[-1].get("sha1") == copy.get("sha1"):
recorded[-1]["cores"].append(emulator)
else:
recorded.append({**copy, "cores": [emulator]})
while recorded and "sha1" not in recorded[-1]:
recorded.pop()
if not recorded and len(declared) == 1:
return
if recorded:
entry["slot_choices"] = recorded
entry["declared_choices"] = declared
def _manifest_core_entries(
core_files: list,
config: dict,
@@ -3441,6 +3501,13 @@ def generate_manifest(
config, emu_profiles, db, zip_contents, data_registry,
target_cores=target_cores, source=source,
)
# The full manifest records every core's answer at a contested slot, so a
# target narrowed from it gets what that target's own pack serves.
slot_choices = (
slots.slot_choices(config, emu_profiles, db, zip_contents, data_registry)
if target_cores is None and source != "platform" and emu_profiles
else {}
)
def manifest_destination(full_destination: str) -> str:
if base_dest and full_destination.startswith(f"{base_dest}/"):
@@ -3489,6 +3556,80 @@ def generate_manifest(
pack_systems, db, bios_dir, base_dest, required_only,
zip_contents, offline,
)
declarations: dict[str, dict[str, list[dict]]] = {}
for sys_id, system in sorted(pack_systems.items()):
for file_entry in system.get("files", []):
if required_only and file_entry.get("required") is False:
continue
dest = sanitize_pack_path(file_entry.get("destination", file_entry["name"]))
if dest and not (region_drops and dest in region_drops):
full_dest = f"{base_dest}/{dest}" if base_dest else dest
declarations.setdefault(full_dest, {}).setdefault(sys_id, []).append(
file_entry
)
def declaration_order(full_dest: str) -> list[tuple[list[str], dict, str]]:
"""The declarations answering a destination as its systems drop.
A pack built for a target loses the systems no available core
reads, and the next declaration takes the destination by the
same rule: RetroArch's bare nintendo-ds dsi_bios9.bin once the
pinned nintendo-dsi one is gone.
"""
remaining = dict(declarations.get(full_dest, {}))
order: list[tuple[list[str], dict, str]] = []
while remaining:
winner = _preferred_entries(
{sid: {"files": fes} for sid, fes in remaining.items()},
db, bios_dir, base_dest, required_only, zip_contents, offline,
).get(full_dest)
first_sys, first = next(
(sid, fe) for sid, fes in sorted(remaining.items()) for fe in fes
if winner is None or id(fe) == winner
)
systems = sorted(
sid for sid, fes in remaining.items()
if any(fe is first for fe in fes)
)
order.append((systems, first, first_sys))
for sid in systems:
del remaining[sid]
return order
def settle(file_entry: dict, sys_id: str, dest: str, override: str | None):
"""The file served once the slot is settled, or None and why not."""
local_path, status = slots.apply_override(override, *resolve_file(
file_entry,
db,
bios_dir,
zip_contents,
data_dir_registry=data_registry,
offline=offline,
))
if status == "not_found" and not reads_file_contents(verification_mode):
found = agnostic_substitute(file_entry, sys_id, db, manifest_profiles)
if found:
local_path, status = found[0], "agnostic_fallback"
if os.path.basename(local_path) != file_entry.get("name", ""):
# The pack explains the rename in a note of its own.
rename_notes.add(file_entry.get("name", ""))
# The builder's verdict: a hash platform omits what its frontend
# would reject, a zipped_file archive included when the member is
# missing whatever the outer resolution said. An existence
# platform never reads the bytes.
status, refusal = baseline_verdict(
file_entry, local_path, status, verification_mode
)
if refusal or status in ("not_found", "external"):
return None, "hash_mismatch" if refusal else status
# The variant the pack ships when an emulator rejects the
# resolved file: the installer must place the same bytes.
local_path, _disagreement = validated_choice(
file_entry, local_path, db, manifest_validation, bios_dir,
digest_algorithm(verification_mode), dest, manifest_owners,
)
return local_path, status
for sys_id, system in sorted(pack_systems.items()):
for file_entry in system.get("files", []):
if required_only and file_entry.get("required") is False:
@@ -3524,48 +3665,24 @@ def generate_manifest(
)
continue
local_path, status = resolve_file(
file_entry,
db,
bios_dir,
zip_contents,
data_dir_registry=data_registry,
offline=offline,
local_path, status = settle(
file_entry, sys_id, dest, slot_overrides.get(full_dest)
)
local_path, status = slots.apply_override(
slot_overrides.get(full_dest), local_path, status
)
if status == "not_found" and not reads_file_contents(verification_mode):
found = agnostic_substitute(file_entry, sys_id, db, manifest_profiles)
if found:
local_path, status = found[0], "agnostic_fallback"
if os.path.basename(local_path) != file_entry.get("name", ""):
# The pack explains the rename in a note of its own.
rename_notes.add(file_entry.get("name", ""))
# The builder's verdict: a hash platform omits what its
# frontend would reject, a zipped_file archive included when
# the member is missing whatever the outer resolution said.
# An existence platform never reads the bytes.
status, refusal = baseline_verdict(
file_entry, local_path, status, verification_mode
)
if refusal or status in ("not_found", "external"):
record_omission(
full_dest, file_entry, sys_id,
"hash_mismatch" if refusal else status, None,
)
if local_path is None:
record_omission(full_dest, file_entry, sys_id, status, None)
continue
# The variant the pack ships when an emulator rejects the
# resolved file: the installer must place the same bytes.
local_path, _disagreement = validated_choice(
file_entry, local_path, db, manifest_validation, bios_dir,
digest_algorithm(verification_mode), dest, manifest_owners,
)
entry, file_size = _manifest_entry(
local_path, dest, None, db, repo_root,
selection.fields(dest, file_entry["name"]),
)
if entry is not None and target_cores is None:
order = declaration_order(full_dest)
if full_dest in slot_choices or len(order) > 1:
_record_slot_choices(
entry, slot_choices.get(full_dest, []), order,
settle, dest, db, repo_root,
)
if entry is None:
record_omission(full_dest, file_entry, sys_id, "not_found", None)
if file_size and full_dest not in seen_destinations:
@@ -3706,7 +3823,7 @@ def _manifest_core_index(
}
named = {
c for entry in [*files, *omitted]
for copy in [entry, *entry.get("alternatives", [])]
for copy in [entry, *entry.get("alternatives", []), *entry.get("slot_choices", [])]
for c in copy.get("cores") or []
}
named |= {c for cores in system_cores.values() for c in cores}
+79 -24
View File
@@ -28,6 +28,7 @@ from common import (
parse_md5_list,
resolution_is_hash_exact,
resolve_local_file,
resolve_platform_cores,
runs_standalone,
)
from packresolve import preferred_entries
@@ -302,20 +303,16 @@ def _claim_rank(claim: Claim) -> int:
return 1 if claim.is_proven else 0
def find_conflicts(
def _slot_claims(
config: dict,
profiles: dict,
db: dict,
base_dest: str = "",
standalone_cores: set[str] | None = None,
zip_contents: dict | None = None,
data_dir_registry: dict | None = None,
) -> list[Conflict]:
"""Destinations where a proven profile claim contradicts what ships.
Only proven claims are compared. A claim resolved by filename alone
asserts nothing about content and cannot contradict anything.
"""
base_dest: str,
standalone_cores: set[str] | None,
zip_contents: dict | None,
data_dir_registry: dict | None,
) -> tuple[dict[str, Claim], dict[str, list[Claim]]]:
"""The platform claim and the proven profile claims at each destination."""
fold = bool(config.get("case_insensitive_fs", False))
# The builder's own choice among declarations sharing a destination,
# read from the same function: the most accepting archive for zipped_file
@@ -369,6 +366,27 @@ def find_conflicts(
continue
by_slot.setdefault(key, []).append(claim)
return by_dest, by_slot
def find_conflicts(
config: dict,
profiles: dict,
db: dict,
base_dest: str = "",
standalone_cores: set[str] | None = None,
zip_contents: dict | None = None,
data_dir_registry: dict | None = None,
) -> list[Conflict]:
"""Destinations where a proven profile claim contradicts what ships.
Only proven claims are compared. A claim resolved by filename alone
asserts nothing about content and cannot contradict anything.
"""
by_dest, by_slot = _slot_claims(
config, profiles, db, base_dest, standalone_cores, zip_contents,
data_dir_registry,
)
# A declaration that lists several accepted hashes is satisfied by any of
# them: Recalbox names both scph1001.bin revisions, and mesence names
# four BS-X dumps among which System.dat pins one. The resolver stops on
@@ -428,20 +446,45 @@ def pack_overrides(
verify all read this: two of them deciding alone gave the one-line
installer different bytes than the ZIP.
"""
from common import resolve_platform_cores
# A baseline-only pack serves what the platform declares, and a target's
# pack answers to the cores that hardware builds: DuckStation does not
# exist on any RetroArch target and still replaced its scph5000.bin.
if not profiles or source == "platform":
return {}
platform_profiles = {
name: profiles[name]
for name in resolve_platform_cores(config, profiles, target_cores=target_cores)
}
mode = config.get("verification_mode", "existence")
cores = set(resolve_platform_cores(config, profiles, target_cores=target_cores))
overrides: dict[str, str] = {}
for conflict in find_conflicts(
for destination, choices in slot_choices(
config, profiles, db, zip_contents, data_dir_registry
).items():
path = next((path for emulator, path in choices if emulator in cores), None)
if path:
overrides[destination] = path
return overrides
def slot_choices(
config: dict,
profiles: dict,
db: dict,
zip_contents: dict | None = None,
data_dir_registry: dict | None = None,
) -> dict[str, list[tuple[str, str | None]]]:
"""Destination -> (core, the file it has served there) in decision order.
A pack built for a set of cores serves the first choice whose core is in
it, None standing for the platform's own answer; no core in it leaves
the platform's answer too. The order is arbitrate's: a core agreeing with
the platform settles the slot whatever else is present, then the
strongest proof decides. Read for one core set by pack_overrides and
recorded whole in the install manifest, so a target narrowed from the
full manifest gets the bytes the target's own pack carries.
"""
if nativemode.reads_file_contents(config.get("verification_mode", "existence")):
return {}
platform_profiles = {
name: profiles[name] for name in resolve_platform_cores(config, profiles)
}
by_dest, by_slot = _slot_claims(
config,
platform_profiles,
db,
@@ -449,11 +492,23 @@ def pack_overrides(
{str(c) for c in config.get("standalone_cores", [])},
zip_contents,
data_dir_registry,
):
decision = arbitrate(conflict, mode)
if decision.serves_both and decision.winner.local_path:
overrides[conflict.destination] = decision.winner.local_path
return overrides
)
choices: dict[str, list[tuple[str, str | None]]] = {}
for key, claims in sorted(by_slot.items()):
platform = by_dest[key]
agree = [_satisfies(c, platform, db) for c in claims]
agreeing = [c for c, ok in zip(claims, agree) if ok]
others = sorted(
(c for c, ok in zip(claims, agree) if not ok),
key=lambda claim: (-_claim_rank(claim), claims.index(claim)),
)
if not others:
continue
choices[platform.destination] = [
*((c.emulator, None) for c in agreeing),
*((c.emulator, c.local_path) for c in others),
]
return choices
def apply_override(
+68
View File
@@ -244,6 +244,74 @@ class APlatformOmissionKeepsItsClaim(unittest.TestCase):
)
class APlatformFileSettledByCoresAndSystems(unittest.TestCase):
"""RetroArch's dsi_bios9.bin ships melonds' copy where melonds runs; on
a target without it the pack serves the platform's answer, and the
pinned nintendo-dsi declaration gives way to the bare nintendo-ds one
once its system drops. The installer kept melonds' copy on rpi4."""
def copy(self, letter: str) -> dict:
return {"sha1": letter * 40, "size": 1, "repo_path": f"bios/{letter}.bin"}
def manifest(self) -> dict:
entry = {
"dest": "dsi_bios9.bin", **self.copy("a"), "cores": None,
"systems": ["nintendo-ds", "nintendo-dsi"],
"slot_choices": [{"cores": ["desmume"]}, {**self.copy("a"), "cores": ["melonds"]}],
"declared_choices": [
{**self.copy("c"), "systems": ["nintendo-dsi"]},
{**self.copy("b"), "systems": ["nintendo-ds"]},
],
}
return {
"manifest_version": 2, "platform": "retroarch",
"files": [entry], "omitted_files": [],
"core_names": {},
"system_cores": {"nintendo-ds": ["desmume", "melonds", "nds"],
"nintendo-dsi": ["melonds", "dsi"]},
}
def served(self, names: list[str]) -> str:
files, _ = install.narrow_to_cores(self.manifest(), names)
self.assertEqual(len(files), 1)
self.assertNotIn("declared_choices", files[0])
return files[0]["sha1"][0]
def test_the_first_chosen_core_decides(self):
self.assertEqual(self.served(["melonds"]), "a")
def test_a_core_agreeing_with_the_platform_leaves_its_answer(self):
self.assertEqual(self.served(["desmume", "melonds"]), "c")
self.assertEqual(self.served(["desmume"]), "b")
def test_the_remaining_system_answers_without_the_cores(self):
self.assertEqual(self.served(["dsi"]), "c")
self.assertEqual(self.served(["nds"]), "b")
def test_the_boundary_checks_every_copy(self):
install._validate_manifest(self.manifest(), "retroarch")
bad_choices = (
[{"cores": []}],
[{"cores": ["x"], "repo_path": "../x", "sha1": "a" * 40, "size": 1}],
[{"cores": ["x"], "dest": "../evil"}],
)
for choices in bad_choices:
manifest = self.manifest()
manifest["files"][0]["slot_choices"] = choices
with self.subTest(choices=choices), self.assertRaises(ValueError):
install._validate_manifest(manifest, "retroarch")
for declared in ([], [self.copy("c")], [{**self.copy("c"), "systems": []}],
[{**self.copy("c"), "systems": ["s"], "cores": ["x"]}]):
manifest = self.manifest()
manifest["files"][0]["declared_choices"] = declared
with self.subTest(declared=declared), self.assertRaises(ValueError):
install._validate_manifest(manifest, "retroarch")
manifest = self.manifest()
del manifest["files"][0]["declared_choices"]
with self.assertRaises(ValueError):
install._validate_manifest(manifest, "retroarch")
class RefusedBeforeAnyInstall(unittest.TestCase):
def test_a_platform_shipping_its_cores_stops_the_run_first(self):
tmp = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
+5 -1
View File
@@ -173,7 +173,11 @@ What stays is what a pack built for exactly those cores carries, the rule
emulates it or reads its files, or when no core of the platform does; a
file a core loads stays when that core is installed. Where two cores read
one path and want different bytes, the copy of the first installed core in
the builder's order is the one installed.
the builder's order is the one installed. The same holds for a file of the
platform's own list: where a core's source contradicts the platform and the
frontend only checks the path, the core's copy is installed while that core
is, and otherwise the platform's answer, read from the first declaring
system that stays.
Batocera, Recalbox and the other distributions ship their cores with the
system, so the option is refused there, as it is when no core is found or