fix: let an alias settle only where the core reads

This commit is contained in:
Abdessamad Derraz committed 2026-10-10 09:47:50 +02:00
1 parent 836c56b3fa
commit ffa6516e09
12 files changed
+760 -222

No files matched your search

+32 -8
View File
@@ -6,6 +6,8 @@ and file resolution - eliminates DRY violations across scripts.
from __future__ import annotations
from collections import Counter
import hashlib
import functools
import json
@@ -1703,27 +1705,32 @@ def filter_systems_by_target(
def platform_declarations(
config: dict, db: dict, *, enrich: bool = True
) -> dict[str, list[tuple[int | None, str]]]:
"""Every file name a platform declares, with each declaration's size and destination.
"""Every file name a platform declares, with each declaration's size and slot.
A name alone does not say which file a declaration is: RetroArch declares
galaksija/ROM1.BIN, a 4 KiB ROM, and DOSBox reads a 32 KiB SC-55 ROM1.BIN.
The size, from the platform entry or else from the file its hash names,
tells a declaration of the same file from a same-named one; None stands
for a size nobody knows. The destination, sanitized and case-folded, is
the slot the declaration fills.
for a size nobody knows. The slot is the declared destination, sanitized
and case-folded, relative to the directory core files go to: RetroDECK
writes bios/ in front of every destination, the others put it in
base_destination.
With ``enrich``, a declaration also answers for the canonical name and
the aliases the database knows its MD5 under. This handles a platform
declaring a file under a different name than the emulator profile
(e.g. Batocera ROM1 vs gsplus ROM).
"""
prefix = "" if config.get("base_destination") else declared_root(config).lower()
declared: dict[str, list[tuple[int | None, str]]] = {}
by_md5 = db.get("indexes", {}).get("by_md5", {})
files_db = db.get("files", {})
for system in config.get("systems", {}).values():
for fe in system.get("files", []):
name = fe.get("name", "")
dest = sanitize_pack_path(fe.get("destination") or name).lower()
slot = sanitize_pack_path(fe.get("destination") or name).lower()
if prefix and slot.startswith(prefix + "/"):
slot = slot[len(prefix) + 1:]
# A zipped_file entry pins a member of the archive it names: its
# md5 and its size, when stated, are not the archive's.
by_hash: dict = {}
@@ -1738,16 +1745,33 @@ def platform_declarations(
(fe.get("sha1") or "").lower(), by_hash
).get("size")
if name:
declared.setdefault(name, []).append((size, dest))
declared.setdefault(name, []).append((size, slot))
if not enrich:
continue
known = by_hash
for other in (known.get("name", ""), *known.get("aliases", [])):
for other in (by_hash.get("name", ""), *by_hash.get("aliases", [])):
if other:
declared.setdefault(other, []).append((known.get("size"), dest))
declared.setdefault(other, []).append((by_hash.get("size"), slot))
return declared
def declared_root(config: dict) -> str:
"""The directory nearly every declared destination starts with, if any.
A platform without base_destination writes it into each destination;
core files go under the same directory.
"""
roots = [
d.split("/", 1)[0]
for system in config.get("systems", {}).values()
for f in system.get("files", [])
if "/" in (d := f.get("destination", "") or "")
]
if not roots:
return ""
root, count = Counter(roots).most_common(1)[0]
return root if count / len(roots) > 0.9 else ""
def expand_platform_declared_names(config: dict, db: dict) -> set[str]:
"""The names a platform declares, enriched as `platform_declarations`."""
return set(platform_declarations(config, db))
+5 -22
View File
@@ -7,7 +7,7 @@ this module builds so the report and the pack withdraw the same files."""
from __future__ import annotations
from common import PROFILE_IDENTITY_FIELDS
from common import expand_platform_declared_names, platform_declarations
from common import declared_root, expand_platform_declared_names, platform_declarations
from common import load_emulator_profiles
from common import resolution_is_hash_exact
from common import resolve_local_file
@@ -33,29 +33,12 @@ def _emulator_systems_index(emu_profiles: dict | None) -> dict[str, list[str]]:
return index
def _detect_extras_prefix(config: dict, base_dest: str) -> str:
"""Detect the effective BIOS prefix for core extras.
"""The prefix core-extra destinations take (may be empty).
When base_destination is empty (RetroDECK), infer the prefix from
the dominant root of YAML-declared destinations. Returns the prefix
to prepend to every core-extra destination (may be empty).
When base_destination is empty (RetroDECK), it is the dominant root of
the YAML-declared destinations.
"""
if base_dest:
return base_dest
dests: list[str] = []
for sys_data in config.get("systems", {}).values():
for f in sys_data.get("files", []):
d = f.get("destination", "")
if d and "/" in d:
dests.append(d)
if not dests:
return ""
from collections import Counter
roots = Counter(d.split("/", 1)[0] for d in dests)
most_common, count = roots.most_common(1)[0]
if count / len(dests) > 0.9:
return most_common
return ""
return base_dest or declared_root(config)
def _detect_slug_structure(config: dict) -> tuple[bool, dict[str, str]]:
"""Detect whether a platform uses per-system slug destinations.
+25 -16
View File
@@ -415,28 +415,37 @@ def _candidate_verdict(
if outside_gap_scope(file_entry, is_standalone):
return "skip"
if not include_all:
# A platform declaring any name the core answers to has met the
# requirement: quasi88 reads n88sub.rom or disk.rom, and System.dat
# names disk.rom. Without the aliases the pack carried the ROM twice.
# The archive answers by its name. A loose file answers where the
# declaration fills the entry's own slot, whose content the slot
# arbitration decides, or elsewhere at a size the core accepts:
# Galaksija's 4 KiB ROM1.BIN does not stand for DOSBox's 32 KiB
# SC-55 one.
# A platform declaring the name the core reads has met the
# requirement where the declaration fills the entry's own slot,
# whose content the slot arbitration decides, or elsewhere at a size
# the core accepts: the builder copies that file to the core's path.
# Galaksija's 4 KiB ROM1.BIN does not stand for DOSBox's 32 KiB SC-55
# one. The builder copies nothing for an alias, so an alias answers
# only where the core reads it: quasi88 reads n88sub.rom or disk.rom
# in quasi88/, and System.dat names quasi88/disk.rom. RetroDECK's
# root MSX.ROM is not CLK's MSX/MSX.ROM, and RetroBat's 16 KiB MSX
# DISK.ROM at the root is not the 256-byte Disk II ROM gsplus also
# accepts as c600.rom. The archive answers by its name.
if file_entry.get("archive") in declared_names:
return "settled"
slot = sanitize_pack_path(dest).lower()
slot = sanitize_pack_path(dest or fname).lower()
directory = slot if dest.endswith("/") else slot.rpartition("/")[0]
for name in (fname, *file_entry.get("aliases", [])):
if name not in declared_names:
continue
declarations = (
declared_names[name]
if isinstance(declared_names, Mapping)
else [(None, "")]
)
if not isinstance(declared_names, Mapping):
return "settled"
if name == fname:
if any(
where == slot or name_match_size_ok(file_entry, size)
for size, where in declared_names[name]
):
return "settled"
continue
read_at = f"{directory}/{name}".lower() if directory else name.lower()
if any(
(slot and where == slot) or name_match_size_ok(file_entry, size)
for size, where in declarations
where == read_at and name_match_size_ok(file_entry, size)
for size, where in declared_names[name]
):
return "settled"
return "keep"