fix: count an uncached release asset as fetched

verify resolved offline, so a release asset absent from bios/ and the
cache read as missing while an online build fetched and packed it. verify
now reads the database record of the registered path and counts such a
declaration present, listed as fetched at build time. --offline keeps the
old count, and the offline pipeline passes it to its verify step.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 10:50:03 +02:00
1 parent b4e5e42c97
commit 9a0501d9d3
6 files changed
+251 -4

No files matched your search

+50 -3
View File
@@ -149,6 +149,11 @@ class Status:
MISSING = "missing"
# A declaration whose file is a release asset the cache does not hold yet:
# an online build fetches it, verify reads the database record instead.
RELEASE_ASSET_PENDING = "release_asset_pending"
class Severity:
CRITICAL = "critical" # required file missing or bad hash (Recalbox RED)
WARNING = "warning" # optional missing or hash mismatch (Recalbox YELLOW)
@@ -1075,7 +1080,7 @@ def _resolve_for_platform(
disk and the large-file cache: verify never downloads. A frontend that
never reads the bytes takes the filename-free core's own image.
"""
from packresolve import resolve_declaration
from packresolve import pending_release_asset, resolve_declaration, run_is_offline
override = slot_overrides.get(
sanitize_pack_path(file_entry.get("destination", file_entry.get("name", "")))
@@ -1084,6 +1089,12 @@ def _resolve_for_platform(
file_entry, db, DEFAULT_BIOS_DIR, zip_contents, data_dir_registry,
override, offline=True,
)
if status in ("not_found", "hash_mismatch") and not run_is_offline():
# Not in the cache, and the release holds a copy that answers the
# declaration: an online build fetches and packs it (resolve_file).
asset = pending_release_asset(file_entry, db)
if asset:
return asset, RELEASE_ASSET_PENDING
if status == "not_found" and not reads_file_contents(mode):
found = agnostic_substitute(file_entry, sys_id, db, platform_profiles)
if found:
@@ -1234,7 +1245,23 @@ def verify_platform(
file_entry.get("destination", file_entry.get("name", ""))
)
stored = _storage_verdict(file_entry, emulator_pack=False)
if stored is not None:
if resolve_status == RELEASE_ASSET_PENDING:
# Counted as an online build counts it, which fetches the
# asset. Under --offline the builder cannot, and neither
# does this report (_resolve_for_platform).
result = {
"name": file_entry.get("name", ""),
"required": file_entry.get("required", True),
"status": Status.OK,
"release_asset": local_path,
"reason": "release asset, fetched when the pack is built",
}
packed_order.append((
sys_id, f"{base_dest}/{destination}" if base_dest else destination, None,
))
shipped_keys.add(_dest_key(file_entry, sys_id))
local_path = None
elif stored is not None:
result = stored
local_path = None
# The builder keeps the slot: instructions for a user_provided
@@ -1744,6 +1771,12 @@ def print_platform_result(
if missing:
parts.append(f"{missing} missing")
print(f"{label}: {', '.join(parts)} [{mode}]")
fetched = sorted({d["name"] for d in result["details"] if d.get("release_asset")})
if fetched:
# Counted present: an online build downloads them. --offline counts
# them missing, as an offline build leaves them out.
shown = ", ".join(fetched[:10]) + (f" (+{len(fetched) - 10})" if len(fetched) > 10 else "")
print(f" Release assets fetched when the pack is built ({len(fetched)}): {shown}")
fallbacks = result.get("region_fallbacks") or []
if fallbacks:
from packreadme import _group_label
@@ -2440,6 +2473,7 @@ def _refuse_listing_narrowings(
("--strict", args.strict),
("--verbose", args.verbose),
("--destinations", args.destinations),
("--offline", args.offline),
):
if on:
parser.error(f"{flag} is incompatible with {listing}")
@@ -2457,6 +2491,8 @@ def _refuse_mode_flags(args: argparse.Namespace, parser: argparse.ArgumentParser
"--target is incompatible with --emulator and --system"),
(args.destinations and not (args.platform or args.all),
"--destinations requires --platform or --all"),
(args.offline and not (args.platform or args.all),
"--offline requires --platform or --all"),
(args.destinations and getattr(args, "region", None),
"--destinations describes the full pack and is incompatible with --region"),
):
@@ -2536,6 +2572,11 @@ def _main(held: contextlib.ExitStack) -> None:
help="Show emulator ground truth details",
)
parser.add_argument("--json", action="store_true", help="JSON output")
parser.add_argument(
"--offline", action="store_true",
help="Report as an offline build packs: a release asset the cache "
"lacks is missing. Without it, it counts as fetched at build time",
)
parser.add_argument(
"--destinations", metavar="FILE",
help="With --platform or --all, also write the paths each pack holds "
@@ -2552,6 +2593,10 @@ def _main(held: contextlib.ExitStack) -> None:
work_from_repo_root(args, parser, ["db", "platforms_dir", "emulators_dir"])
if args.platform:
require_platform(args.platform, args.platforms_dir)
if args.offline:
import packresolve
packresolve.set_offline(True)
requested_regions: list[str] = []
if getattr(args, "region", None):
@@ -2696,7 +2741,9 @@ def _json_details(details: list[dict], verbose: bool) -> list[dict]:
used to be accepted beside --json and dropped, the output byte for byte
the same, so a script asking for the passing entries got nothing.
"""
return list(details) if verbose else [d for d in details if d["status"] != Status.OK]
return list(details) if verbose else [
d for d in details if d["status"] != Status.OK or d.get("release_asset")
]
def _exit_if_strict(strict: bool, results: list[dict]) -> None: