fix: leave out core files the core refuses by size

A core extra resolved by name shipped on existence platforms even when
the core's own size check turns the copy away. The RetroPie pack carried
the 128 KB Apple IIGS ROM as Basilisk II's ROM (min 512 KB). The builder,
the manifest and verify now read validation.core_size_refusal, which
accepts a copy any version of the same profile slot takes.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 10:46:52 +02:00
1 parent fd1d1b7b96
commit b4e5e42c97
7 files changed
+287 -32

No files matched your search

+15 -27
View File
@@ -93,6 +93,7 @@ from validation import (
deliverable,
filter_files_by_mode,
with_archive_members,
extra_alternatives,
)
yaml = require_yaml()
@@ -592,14 +593,7 @@ def _matches_an_alternative(fe: dict, local_path: str, profiles: dict) -> bool:
of Raze's table, eduke32's demo and retail NWINTER.GRP). The extra
carries the first, and the held copy matching a sibling is no mismatch.
"""
profile = profiles.get(fe.get("source_profile") or "", {})
name = fe.get("source_name") or fe.get("name")
path = fe.get("source_path") or name
siblings = [
entry for entry in profile.get("files") or []
if isinstance(entry, dict) and entry.get("name") == name
and (entry.get("path") or name) == path
]
siblings = extra_alternatives(fe, profiles)
if len(siblings) < 2:
return False
actual = compute_hashes(local_path, frozenset({"sha1", "md5", "crc32"}))
@@ -1054,7 +1048,7 @@ def generate_pack(
core_count = 0
extra_resolver = ExtraResolver(
db, bios_dir, zip_contents, data_registry, validation_index,
validation_owners, offline,
validation_owners, offline, profiles=emu_profiles,
)
for fe in core_files:
if required_only and fe.get("required") is False:
@@ -3778,6 +3772,7 @@ def _manifest_core_entries(
validation_index: dict | None = None,
owners: dict | None = None,
data_registry: dict | None = None,
profiles: dict | None = None,
) -> int:
"""Add the files a platform's cores need but its list does not name.
@@ -3788,6 +3783,10 @@ def _manifest_core_entries(
"""
total_size = 0
extras_pfx = _detect_extras_prefix(config, base_dest)
resolver = ExtraResolver(
db, bios_dir, zip_contents, data_registry, validation_index or {},
owners, offline, profiles=profiles,
)
for fe in core_files:
if required_only and fe.get("required") is False:
continue
@@ -3833,20 +3832,12 @@ def _manifest_core_entries(
):
continue
dest_hint = fe.get("destination", "")
# Resolved with the data directories, as the builder resolves the
# same extra: without them blueMSX's MSX2P.rom named a different
# dump than the one its pack carried.
local_path, status = resolve_file(
fe,
db,
bios_dir,
zip_contents,
dest_hint=dest_hint,
data_dir_registry=data_registry,
offline=offline,
)
if status in ("not_found", "external", "user_provided") or not local_path:
# The builder's own lookup (ExtraResolver): the data directories, the
# copy the emulator accepts, and the size its code checks. Without
# the data directories blueMSX's MSX2P.rom named a different dump
# than the one its pack carried.
local_path, status, _disagreement = resolver.resolve(fe, dest)
if not local_path:
source_emu = fe.get("source_profile") or fe.get("source_emulator", "")
if not _claim_platform_omission(
omitted_by_destination.get(full_dest), source_emu
@@ -3860,10 +3851,6 @@ def _manifest_core_entries(
[source_emu] if source_emu else [],
)
continue
# The copy the emulator accepts, as the pack ships it.
local_path, _disagreement = validated_choice(
fe, local_path, db, validation_index or {}, bios_dir, None, dest, owners,
)
source_emu = fe.get("source_profile") or fe.get("source_emulator", "")
@@ -4382,6 +4369,7 @@ def generate_manifest(
seen_destinations, seen_lower, seen_parents, manifest_files,
omitted_by_destination, record_omission, pack_only_sizes, selection,
required_only, manifest_validation, manifest_owners, data_registry,
emu_profiles,
)
for key, (entry, full_dest, dest) in platform_entries.items():
declared = declared_by.get(full_dest, set())
+10 -1
View File
@@ -176,7 +176,10 @@ class ExtraResolver:
validation_index: dict,
validation_owners: dict | None,
offline: bool | None = True,
*,
profiles: dict,
) -> None:
self.profiles = profiles
self.db = db
self.bios_dir = bios_dir
self.zip_contents = zip_contents
@@ -188,7 +191,7 @@ class ExtraResolver:
def resolve(self, fe: dict, dest: str) -> tuple[str | None, str, str]:
"""(local copy or None, resolver status, the core's disagreement)."""
from packresolve import resolve_file
from validation import validated_choice
from validation import core_size_refusal, extra_alternatives, validated_choice
local_path, status = resolve_file(
fe, self.db, self.bios_dir, self.zip_contents,
@@ -201,6 +204,12 @@ class ExtraResolver:
fe, local_path, self.db, self.validation_index, self.bios_dir, None,
dest, self.validation_owners,
)
# The platform never reads the bytes, the core does: a copy it turns
# away by its own size check is not its file, on any platform.
if core_size_refusal(
fe, local_path, extra_alternatives(fe, self.profiles)
):
return None, "not_found", ""
return local_path, status, disagreement
+1 -1
View File
@@ -581,7 +581,7 @@ def _builder_extras(
resolver=ExtraResolver(
db, DEFAULT_BIOS_DIR, build_zip_contents_index(db), data_registry,
_build_validation_index(platform_profiles),
destination_owners(platform_profiles), True,
destination_owners(platform_profiles), True, profiles=emu_profiles,
),
placed_local=placed_local,
)
+51 -1
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
import os
import zipfile
from collections.abc import Callable, Iterator, Mapping
from collections.abc import Callable, Iterator, Mapping, Sequence
from common import (
adler32_of,
@@ -961,3 +961,53 @@ def existence_discrepancy(file_entry: dict, local_path: str, platform_display: s
f"packed per {platform_display} existence check: declared hash {declared}, "
f"file md5 {actual['md5']} sha1 {actual['sha1']}"
)
def same_slot_entries(files: list, name: str, path: str) -> list[dict]:
"""The entries of one profile that name the same file at the same path.
A profile lists every version its code takes under one name (each GRP
of Raze's table, eduke32's demo and retail NWINTER.GRP): any of them
fills the slot.
"""
return [
entry for entry in files or []
if isinstance(entry, dict) and entry.get("name") == name
and (entry.get("path") or name) == path
]
def extra_alternatives(fe: dict, profiles: Mapping | None) -> list[dict]:
"""The versions the extra's own profile accepts at the extra's slot."""
if not profiles:
return []
profile = profiles.get(fe.get("source_profile") or "", {})
name = fe.get("source_name") or fe.get("name", "")
return same_slot_entries(
profile.get("files") or [], name, fe.get("source_path") or name
)
def core_size_refusal(
file_entry: dict, local_path: str | None, alternatives: Sequence[dict] = (),
) -> str | None:
"""Why the core refuses this copy by the size its own code checks, or None.
The one rule the builder, the install manifest and verify read for a
core's file. An existence platform never opens the bytes, so a copy whose
hash disagrees still ships to it, but a core that measures the file turns
one of the wrong size away: Basilisk II stops on the Apple IIGS' 128 KB
ROM (main_unix.cpp:692), so packing that file for it serves nothing.
``alternatives`` are the other versions the profile accepts at the same
slot: a copy any of them takes is not refused.
"""
if not local_path or not os.path.isfile(local_path):
return None
size = os.path.getsize(local_path)
if any(name_match_size_ok(entry, size) for entry in (file_entry, *alternatives)):
return None
bounds = (
f"{file_entry['size']}" if file_entry.get("size") is not None
else f"{file_entry.get('min_size', 0)} to {file_entry.get('max_size', 'any')}"
)
return f"{size} bytes, the core checks for {bounds}"
+14 -1
View File
@@ -103,6 +103,8 @@ from validation import (
unread_on,
outside_gap_scope,
with_archive_members,
core_size_refusal,
same_slot_entries,
)
DEFAULT_DB = "database.json"
@@ -692,6 +694,17 @@ def find_undeclared_files(
in_repo = _st != "not_found" and _lp is not None
if in_repo and _st == "hash_mismatch" and shipped_on_mismatch:
in_repo = False
if in_repo and "size" in _parse_validation(f.get("validation")):
# On any platform the core measures what it opens: a
# copy it turns away by its own size check is not
# held for it, the rule the builder's lookup applies
# (packextras.ExtraResolver). The Apple IIGS' 128 KB
# ROM is not Basilisk II's ROM.
_lp, _st = resolve_local_file(f, db, dest_hint=dest)
if core_size_refusal(
f, _lp, same_slot_entries(profile.get("files"), fname, dest)
):
in_repo = False
checks = _parse_validation(f.get("validation"))
entry = {
@@ -1350,7 +1363,7 @@ def verify_platform(
region_drops, report_for_builder,
ExtraResolver(
db, DEFAULT_BIOS_DIR, zip_contents, data_dir_registry, validation_index,
validation_owners,
validation_owners, profiles=profiles,
),
placed_kind,
)