mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-30 07:37:20 -05:00
chore: merge round 2 fixes (tooling)
This commit is contained in:
commit
055627f39c
22 files changed
+704
-141
No files matched your search
@@ -1256,6 +1256,8 @@ def check_profiles(run: bool, offline: bool) -> list[Finding]:
|
||||
"--all",
|
||||
"--json",
|
||||
"--check-version",
|
||||
"--detect-new-files",
|
||||
"--watch-hashes",
|
||||
]
|
||||
if offline:
|
||||
cmd.append("--offline")
|
||||
@@ -1276,7 +1278,9 @@ def summarize_profile_sync(report: object) -> list[Finding]:
|
||||
The JSON carries the ref verdicts and the pin against the tip. A profile
|
||||
whose refs need review is stale, one the forge could not serve is unknown,
|
||||
and one whose upstream moved while every ref still anchors is reported as
|
||||
such without being counted as stale.
|
||||
such without being counted as stale. A cited file that gained a file name
|
||||
since the pin, or a hash the profile does not declare, is stale too: the
|
||||
upstream may load a file the profile does not document.
|
||||
"""
|
||||
if not isinstance(report, list):
|
||||
return [Finding("profiles", "profile_sync", ERROR, detail="report is not a list")]
|
||||
@@ -1314,7 +1318,18 @@ def summarize_profile_sync(report: object) -> list[Finding]:
|
||||
continue
|
||||
review = row.get("needs_review") or 0
|
||||
pin, head = str(row.get("pin") or ""), str(row.get("head") or "")
|
||||
if review:
|
||||
added = sorted({
|
||||
str(item.get("name"))
|
||||
for item in row.get("new_files") or ()
|
||||
if isinstance(item, dict) and item.get("since_pin")
|
||||
})
|
||||
hashes = len(row.get("new_hashes") or ())
|
||||
reasons = [f"{review} refs to review"] if review else []
|
||||
if added:
|
||||
reasons.append(f"new file names since the pin: {', '.join(added)}")
|
||||
if hashes:
|
||||
reasons.append(f"{hashes} undeclared hashes added since the pin")
|
||||
if reasons:
|
||||
findings.append(
|
||||
Finding(
|
||||
"profiles",
|
||||
@@ -1322,7 +1337,7 @@ def summarize_profile_sync(report: object) -> list[Finding]:
|
||||
STALE,
|
||||
local=pin[:8],
|
||||
remote=head[:8],
|
||||
detail=f"{review} refs to review",
|
||||
detail=", ".join(reasons),
|
||||
)
|
||||
)
|
||||
elif pin and head and pin != head:
|
||||
|
||||
+4
-1
@@ -657,8 +657,11 @@ def load_target_config(
|
||||
# through, where a platform or emulator name gets the closest ones.
|
||||
raise ValueError(
|
||||
f"Unknown target '{target}' for platform '{platform_name}'.\n"
|
||||
# Named as a full command: cross_reference.py and slots.py take
|
||||
# --target but have no --list-targets of their own.
|
||||
+ suggest_names(
|
||||
[target], sorted(alias_index), "--list-targets", len(targets)
|
||||
[target], sorted(alias_index),
|
||||
f"verify.py --platform {platform_name} --list-targets", len(targets),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -986,11 +986,16 @@ def main() -> None:
|
||||
if args.platform:
|
||||
from common import load_target_config, resolve_platform_cores
|
||||
|
||||
target_cores = (
|
||||
load_target_config(args.platform, args.target, args.platforms_dir)
|
||||
if args.target
|
||||
else None
|
||||
)
|
||||
try:
|
||||
target_cores = (
|
||||
load_target_config(args.platform, args.target, args.platforms_dir)
|
||||
if args.target
|
||||
else None
|
||||
)
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
# A typo ended in a traceback where verify and slots name the
|
||||
# closest target.
|
||||
parser.error(str(exc))
|
||||
config = load_platform_config(args.platform, args.platforms_dir)
|
||||
relevant = resolve_platform_cores(config, profiles, target_cores=target_cores)
|
||||
run_anywhere = (
|
||||
|
||||
@@ -182,7 +182,7 @@ def main() -> None:
|
||||
group.add_argument(
|
||||
"--all", action="store_true", help="diff all registered platforms"
|
||||
)
|
||||
group.add_argument("--platform", type=platform_arg, help="diff a single platform")
|
||||
group.add_argument("--platform", "-p", type=platform_arg, help="diff a single platform")
|
||||
# One output form: --json beside --format markdown printed JSON and
|
||||
# dropped the markdown without a word.
|
||||
output = parser.add_mutually_exclusive_group()
|
||||
@@ -207,7 +207,7 @@ def main() -> None:
|
||||
help="platform YAML directory (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
"--target", "-t",
|
||||
help="read the models generate_truth --target wrote, each under the "
|
||||
"target as its platform files it",
|
||||
)
|
||||
|
||||
@@ -858,7 +858,7 @@ def main() -> None:
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--all", action="store_true", help="every platform")
|
||||
group.add_argument("--platform", type=platform_arg, help="a single platform")
|
||||
group.add_argument("--platform", "-p", type=platform_arg, help="a single platform")
|
||||
parser.add_argument(
|
||||
"--output-dir", default="exports",
|
||||
help="directory the updated platform files go to (default: %(default)s)",
|
||||
|
||||
+71
-75
@@ -25,8 +25,6 @@ import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -345,68 +343,6 @@ def _platform_pack_stem(
|
||||
return "_".join(_name_part(n) for n in names) + version_tag
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def download_external(file_entry: dict, dest_path: str) -> bool:
|
||||
"""Download an external BIOS file, verify hash, save to dest_path."""
|
||||
url = file_entry.get("source_url")
|
||||
if not url:
|
||||
return False
|
||||
|
||||
sha256 = file_entry.get("sha256")
|
||||
sha1 = file_entry.get("sha1")
|
||||
md5 = file_entry.get("md5")
|
||||
|
||||
if not (sha256 or sha1 or md5):
|
||||
print(
|
||||
f" WARNING: no hash for {file_entry['name']}, skipping unverifiable download"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "retrobios-pack-gen/1.0"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
data = resp.read()
|
||||
except urllib.error.URLError as e:
|
||||
print(f" WARNING: Failed to download {url}: {e}")
|
||||
return False
|
||||
|
||||
if sha256:
|
||||
actual = hashlib.sha256(data).hexdigest()
|
||||
if actual != sha256:
|
||||
print(f" WARNING: SHA256 mismatch for {file_entry['name']}")
|
||||
return False
|
||||
elif sha1:
|
||||
actual = hashlib.sha1(data).hexdigest()
|
||||
if actual != sha1:
|
||||
print(f" WARNING: SHA1 mismatch for {file_entry['name']}")
|
||||
return False
|
||||
elif md5:
|
||||
actual = hashlib.md5(data).hexdigest()
|
||||
if actual != md5:
|
||||
print(f" WARNING: MD5 mismatch for {file_entry['name']}")
|
||||
return False
|
||||
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
with open(dest_path, "wb") as f:
|
||||
f.write(data)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# What the builder writes into a platform pack beside the files it carries.
|
||||
PACK_DOCUMENTS = ("README.txt", "manifest.json")
|
||||
|
||||
@@ -952,7 +888,7 @@ def generate_pack(
|
||||
if status == "external":
|
||||
file_ext = os.path.splitext(file_entry["name"])[1] or ""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=file_ext
|
||||
delete=False, suffix=file_ext, dir=packresolve.scratch_dir()
|
||||
) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
@@ -1707,7 +1643,7 @@ def generate_emulator_pack(
|
||||
if how == "external":
|
||||
file_ext = os.path.splitext(fe["name"])[1] or ""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=file_ext
|
||||
delete=False, suffix=file_ext, dir=packresolve.scratch_dir()
|
||||
) as tmp:
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
@@ -2152,6 +2088,11 @@ def generate_md5_pack(
|
||||
plat_file_index.setdefault(alias.lower(), []).append(fe)
|
||||
|
||||
context_name = plat_display if platform_name else (emu_display or "Custom")
|
||||
# The selection mode is part of the name, as for --system: BizHawk is
|
||||
# both a platform and a profile display name, and the two layouts
|
||||
# (Firmware/ under the platform, the profile's paths under the core)
|
||||
# wrote one file, the second run replacing the first without a word.
|
||||
mode_part = "_Emulator" if emulator_name and not platform_name else ""
|
||||
# --standalone changes the destination layout, so a run with it must not
|
||||
# take the name of a run without it.
|
||||
custom_tags = "".join(
|
||||
@@ -2166,7 +2107,8 @@ def generate_md5_pack(
|
||||
"\n".join(sorted(f"{kind}:{value.lower()}" for kind, value in hashes)).encode()
|
||||
).hexdigest()[:8]
|
||||
zip_name = (
|
||||
f"{_name_part(context_name, '_')}_Custom_{selection}{custom_tags}_BIOS_Pack.zip"
|
||||
f"{_name_part(context_name, '_')}{mode_part}_Custom_{selection}{custom_tags}"
|
||||
"_BIOS_Pack.zip"
|
||||
)
|
||||
zip_path = os.path.join(output_dir, zip_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
@@ -2370,6 +2312,39 @@ def generate_target_manifests(targets_dir: str, output_dir: str) -> None:
|
||||
print(f"Generated {count} target manifest(s) in {output_dir}")
|
||||
|
||||
|
||||
def _refuse_unknown_systems(
|
||||
system_filter: list[str],
|
||||
platforms: list[str],
|
||||
platforms_dir: str,
|
||||
parser: argparse.ArgumentParser,
|
||||
) -> None:
|
||||
"""Refuse a --system no selected platform declares, before any work.
|
||||
|
||||
The name is known once the config is loaded. Checked inside the build, a
|
||||
typo cost the lock and a refresh of every data directory first, then
|
||||
ended on a WARNING and exit 1.
|
||||
"""
|
||||
from common import _norm_system_id
|
||||
|
||||
wanted = {_norm_system_id(s) for s in system_filter}
|
||||
declared: set[str] = set()
|
||||
for name in platforms:
|
||||
systems = load_platform_config(name, platforms_dir).get("systems", {})
|
||||
if any(sid in system_filter or _norm_system_id(sid) in wanted for sid in systems):
|
||||
return
|
||||
declared.update(systems)
|
||||
where = f"--platform {platforms[0]}" if len(platforms) == 1 else "--all"
|
||||
hint = suggest_names(
|
||||
system_filter,
|
||||
sorted(declared),
|
||||
f"--platform {platforms[0]} --list-systems" if len(platforms) == 1
|
||||
else "--platform <name> --list-systems",
|
||||
)
|
||||
parser.error(
|
||||
f"--system {','.join(system_filter)} matches no system of {where}. {hint}"
|
||||
)
|
||||
|
||||
|
||||
def _refuse_for_verify_packs(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
|
||||
"""What --verify-packs cannot be combined with, refused before any lock.
|
||||
|
||||
@@ -2431,7 +2406,7 @@ def _validate_args(args: argparse.Namespace, parser: argparse.ArgumentParser) ->
|
||||
parser.error("--split requires --platform or --all")
|
||||
if args.split and has_emulator:
|
||||
parser.error("--split is incompatible with --emulator")
|
||||
if args.group_by != "system" and not args.split:
|
||||
if args.group_by is not None and not args.split:
|
||||
parser.error("--group-by requires --split")
|
||||
if args.target and not (has_platform or has_all):
|
||||
parser.error("--target requires --platform or --all")
|
||||
@@ -2448,7 +2423,9 @@ def _validate_args(args: argparse.Namespace, parser: argparse.ArgumentParser) ->
|
||||
# A mode either applies a narrowing flag or refuses it. Swallowing one
|
||||
# answers about an artifact the caller did not ask for, and the name on
|
||||
# disk then asserts a narrowing the contents do not carry.
|
||||
source_given = getattr(args, "source", "full") != "full"
|
||||
# --source and --group-by default to None so a typed default value is
|
||||
# seen: compared with "full", `--emulator X --source full` went through.
|
||||
source_given = getattr(args, "source", None) is not None
|
||||
if source_given and (has_emulator or (has_system and not has_platform)):
|
||||
parser.error("--source requires --platform or --all")
|
||||
if has_from_md5:
|
||||
@@ -2642,6 +2619,7 @@ def _run_verify_packs(args: argparse.Namespace) -> None:
|
||||
)
|
||||
|
||||
verified = 0
|
||||
failing: list[str] = []
|
||||
for platform_name in platforms:
|
||||
found = sorted(expected[platform_name] & on_disk)
|
||||
verified += len(found)
|
||||
@@ -2655,6 +2633,7 @@ def _run_verify_packs(args: argparse.Namespace) -> None:
|
||||
f"generate_pack.py --platform {platform_name} --output-dir {args.output_dir})"
|
||||
)
|
||||
all_ok = False
|
||||
failing.append(f"{platform_name} (no pack)")
|
||||
else:
|
||||
print(f" {platform_name}: SKIP (no pack in {args.output_dir})")
|
||||
continue
|
||||
@@ -2700,7 +2679,17 @@ def _run_verify_packs(args: argparse.Namespace) -> None:
|
||||
for err in errors:
|
||||
print(f" {err}")
|
||||
all_ok = False
|
||||
failing.append(f"{platform_name} ({pack_name})")
|
||||
|
||||
if failing:
|
||||
# Named at the end, where the log is read, and on stderr, as the
|
||||
# build path does: a failing pack sat among its own error lines.
|
||||
print(
|
||||
f"ERROR: packs failed verification: {', '.join(failing)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif verified:
|
||||
print(f"Verified {verified} pack{'s' if verified != 1 else ''}: all OK")
|
||||
if not verified:
|
||||
# Every platform skipped is no check at all: a mistyped or purged
|
||||
# output directory read as a passed integrity check.
|
||||
@@ -2918,7 +2907,7 @@ def _refuse_for_all_variants(args: argparse.Namespace, parser: argparse.Argument
|
||||
# or --required-only beside it would be overridden, and the modes that
|
||||
# build one pack (emulator, system, hashes) never read it.
|
||||
for flag, given in (
|
||||
("--source", args.source != "full"),
|
||||
("--source", args.source is not None),
|
||||
("--required-only", args.required_only),
|
||||
("--emulator", args.emulator),
|
||||
("--from-md5", args.from_md5 or args.from_md5_file),
|
||||
@@ -3027,7 +3016,7 @@ def _given_flags(args: argparse.Namespace) -> set[str]:
|
||||
"--system": args.system,
|
||||
"--standalone": args.standalone,
|
||||
"--split": args.split,
|
||||
"--group-by": args.group_by != "system",
|
||||
"--group-by": args.group_by is not None,
|
||||
"--manifest": args.manifest,
|
||||
"--manifest-targets": args.manifest_targets,
|
||||
"--verify-packs": args.verify_packs,
|
||||
@@ -3035,7 +3024,7 @@ def _given_flags(args: argparse.Namespace) -> set[str]:
|
||||
"--from-md5": args.from_md5,
|
||||
"--from-md5-file": args.from_md5_file,
|
||||
"--required-only": args.required_only,
|
||||
"--source": args.source != "full",
|
||||
"--source": args.source is not None,
|
||||
"--target": args.target,
|
||||
"--region": args.region,
|
||||
"--one-per-slot": args.one_per_slot,
|
||||
@@ -3140,9 +3129,9 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
choices=["platform", "truth", "full"],
|
||||
default="full",
|
||||
default=None,
|
||||
help="File source: platform (YAML only), truth (emulator profiles), full "
|
||||
"(both)",
|
||||
"(both, the default)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-variants",
|
||||
@@ -3155,7 +3144,7 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
parser.add_argument(
|
||||
"--group-by",
|
||||
choices=["system", "manufacturer"],
|
||||
default="system",
|
||||
default=None,
|
||||
help="Grouping for --split (default: system)",
|
||||
)
|
||||
parser.add_argument("--target", "-t", help="Hardware target (e.g., switch, rpi4)")
|
||||
@@ -3255,6 +3244,9 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
return
|
||||
|
||||
_validate_args(args, parser)
|
||||
# Unset until every refusal has run, so a typed default value is seen.
|
||||
args.source = args.source or "full"
|
||||
args.group_by = args.group_by or "system"
|
||||
|
||||
# Hash lookup / pack mode
|
||||
# A platform run may refresh the trees below, under the exclusive lock:
|
||||
@@ -3371,6 +3363,9 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
parser.error("Specify --platform or --all")
|
||||
return
|
||||
|
||||
if system_filter:
|
||||
_refuse_unknown_systems(system_filter, platforms, args.platforms_dir, parser)
|
||||
|
||||
data_registry = load_data_dir_registry(args.platforms_dir)
|
||||
# The output directory is claimed before the data directory refresh: a
|
||||
# refresh waits for every run that holds a cache, so a second build
|
||||
@@ -4915,6 +4910,7 @@ from packresolve import ( # noqa: E402,F401
|
||||
parse_hash_file,
|
||||
lookup_hashes,
|
||||
resolve_file,
|
||||
download_external,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -43,7 +43,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--all", action="store_true", help="all registered platforms")
|
||||
group.add_argument("--platform", type=platform_arg, help="single platform name")
|
||||
group.add_argument("--platform", "-p", type=platform_arg, help="single platform name")
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=DEFAULT_OUTPUT_DIR,
|
||||
|
||||
@@ -98,7 +98,8 @@ def asset_candidates(name: str, registered: Iterable[str]) -> list[str]:
|
||||
"""Assets that may hold *name*, a registered path or a bare file name.
|
||||
|
||||
A bare name shared by several registered paths has one asset per path,
|
||||
and the caller's hash tells them apart.
|
||||
and the caller's hash tells them apart. A bare name no registered path
|
||||
carries has none.
|
||||
"""
|
||||
registered = list(registered)
|
||||
if "/" in name:
|
||||
@@ -114,7 +115,10 @@ def asset_candidates(name: str, registered: Iterable[str]) -> list[str]:
|
||||
names[p] for p in sorted(names)
|
||||
if "/.variants/" in p and os.path.basename(p).startswith(name + ".")
|
||||
]
|
||||
return matches or [name]
|
||||
# A bare name no registered path carries is no asset: .gitignore is the
|
||||
# registry. Asking the release anyway cost a 404 per name on every
|
||||
# online build and named an upload that never existed.
|
||||
return matches
|
||||
|
||||
|
||||
def fetch_large_file(
|
||||
|
||||
+61
-2
@@ -6,9 +6,14 @@ and the offline switch that forbids reaching for the network."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from largefiles import fetch_large_file
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from largefiles import fetch_large_file
|
||||
from common import (
|
||||
declared_hash_verdict,
|
||||
resolution_is_hash_exact,
|
||||
@@ -26,6 +31,60 @@ def set_offline(value: bool) -> None:
|
||||
global _OFFLINE
|
||||
_OFFLINE = value
|
||||
|
||||
|
||||
def scratch_dir() -> str:
|
||||
"""The repository's tmp/, never the system /tmp (a 4 GB tmpfs on WSL)."""
|
||||
path = Path(__file__).resolve().parent.parent / "tmp"
|
||||
path.mkdir(exist_ok=True)
|
||||
return str(path)
|
||||
|
||||
|
||||
def download_external(
|
||||
file_entry: dict, dest_path: str, offline: bool | None = None
|
||||
) -> bool:
|
||||
"""Download a `storage: external` file, verify its hash, write dest_path.
|
||||
|
||||
The run's network policy applies: under --offline nothing is fetched and
|
||||
the entry stays missing, as verify reports it. The body is streamed to
|
||||
dest_path and hashed on the way, and removed when the hash disagrees.
|
||||
"""
|
||||
url = file_entry.get("source_url")
|
||||
if not url:
|
||||
return False
|
||||
name = file_entry.get("name", url)
|
||||
declared = next(
|
||||
(
|
||||
(algo, str(file_entry[algo]).lower())
|
||||
for algo in ("sha256", "sha1", "md5")
|
||||
if file_entry.get(algo)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if declared is None:
|
||||
print(f" WARNING: no hash for {name}, skipping unverifiable download")
|
||||
return False
|
||||
if _OFFLINE if offline is None else offline:
|
||||
print(f" WARNING: {name} is external and the run is offline, not fetched")
|
||||
return False
|
||||
algo, expected = declared
|
||||
digest = hashlib.new(algo)
|
||||
Path(dest_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "retrobios-pack-gen/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=120) as resp, open(dest_path, "wb") as out:
|
||||
for chunk in iter(lambda: resp.read(1 << 20), b""):
|
||||
digest.update(chunk)
|
||||
out.write(chunk)
|
||||
except (urllib.error.URLError, OSError) as exc:
|
||||
print(f" WARNING: Failed to download {url}: {exc}")
|
||||
Path(dest_path).unlink(missing_ok=True)
|
||||
return False
|
||||
if digest.hexdigest() != expected:
|
||||
print(f" WARNING: {algo.upper()} mismatch for {name}")
|
||||
Path(dest_path).unlink(missing_ok=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _values(raw: object) -> list[str]:
|
||||
"""A hash field's values: one, a list, or a comma-separated string."""
|
||||
items = raw if isinstance(raw, list) else [raw] if raw else []
|
||||
|
||||
+23
-9
@@ -382,7 +382,7 @@ def check_consistency(verify_output: str, pack_output: str) -> bool:
|
||||
all_ok = False
|
||||
|
||||
status = "OK" if all_ok else "FAILED"
|
||||
print(f"--- consistency check: {status} ---")
|
||||
print(f"--- {step_title(STEP_LABELS['consistency'])}, counts: {status} ---")
|
||||
return all_ok
|
||||
|
||||
|
||||
@@ -447,7 +447,7 @@ def check_pack_destinations(
|
||||
import json # noqa: PLC0415
|
||||
import zipfile # noqa: PLC0415
|
||||
|
||||
print("\n--- 5/8 consistency check: destinations ---")
|
||||
print(f"\n--- {step_title(STEP_LABELS['consistency'])}, destinations ---")
|
||||
try:
|
||||
credited_by = json.loads(Path(destinations_file).read_text())
|
||||
except (OSError, ValueError) as exc:
|
||||
@@ -506,7 +506,7 @@ def check_emulator_packs(
|
||||
select_emulator_profiles,
|
||||
)
|
||||
|
||||
print("\n--- 5/8 consistency check: emulator packs ---")
|
||||
print(f"\n--- {step_title(STEP_LABELS['consistency'])}, emulator packs ---")
|
||||
db = load_database(db_path)
|
||||
zip_contents = build_zip_contents_index(db)
|
||||
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
|
||||
@@ -561,13 +561,18 @@ def _refuse_unapplied_flags(
|
||||
--all-variants builds every source itself, so a --source beside it is
|
||||
dropped; --skip-packs builds none, so both are.
|
||||
"""
|
||||
if args.all_variants and args.source != "full":
|
||||
# --source defaults to None so a typed `--source full` is seen: compared
|
||||
# with its default value, it went through beside the modes that refuse it.
|
||||
source_given = args.source is not None
|
||||
if args.all_variants and source_given:
|
||||
parser.error("--source is incompatible with --all-variants")
|
||||
if args.skip_packs:
|
||||
for flag, on in (("--source", args.source != "full"),
|
||||
for flag, on in (("--source", source_given),
|
||||
("--all-variants", args.all_variants)):
|
||||
if on:
|
||||
parser.error(f"{flag} is incompatible with --skip-packs")
|
||||
if args.source is None:
|
||||
args.source = "full"
|
||||
|
||||
|
||||
def _truth_steps(args: argparse.Namespace, results: dict) -> bool:
|
||||
@@ -662,9 +667,9 @@ def main() -> None:
|
||||
)
|
||||
parser.add_argument("--target", "-t", help="Hardware target (e.g., switch, rpi4)")
|
||||
parser.add_argument(
|
||||
"--source", choices=["platform", "truth", "full"], default="full",
|
||||
"--source", choices=["platform", "truth", "full"], default=None,
|
||||
help="packs to build: the platform list, the profiles, or both (default: "
|
||||
"%(default)s)",
|
||||
"full)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-variants", action="store_true",
|
||||
@@ -724,7 +729,13 @@ def main() -> None:
|
||||
# `cores: [81]` unquoted passed the whole pipeline and failed CI on push.
|
||||
# jsonschema is tooling only, so its absence skips the step.
|
||||
if importlib.util.find_spec("jsonschema") is None:
|
||||
skip_notice("validate_schemas", "jsonschema not installed")
|
||||
# The skip names the install and what still runs the check, or a
|
||||
# contributor read SKIPPED, then COMPLETE, and met the failure on push.
|
||||
skip_notice(
|
||||
"validate_schemas",
|
||||
"jsonschema not installed. PR validation runs this check: "
|
||||
'pip install "jsonschema[format-nongpl]"',
|
||||
)
|
||||
results["validate_schemas"] = SKIPPED
|
||||
else:
|
||||
ok, _ = run(
|
||||
@@ -1011,7 +1022,10 @@ def main() -> None:
|
||||
# Counts can agree while the paths differ, both ways at once.
|
||||
ok = check_pack_destinations(destinations_file, pack_output) and ok
|
||||
ok = check_emulator_packs() and ok
|
||||
print(f"--- 5/8 consistency check, counts and paths: {'OK' if ok else 'FAILED'} ---")
|
||||
print(
|
||||
f"--- {step_title(STEP_LABELS['consistency'])}, counts and paths: "
|
||||
f"{'OK' if ok else 'FAILED'} ---"
|
||||
)
|
||||
results["consistency"] = ok
|
||||
all_ok = all_ok and ok
|
||||
else:
|
||||
|
||||
+100
-29
@@ -30,7 +30,13 @@ import yaml
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
import upstream
|
||||
from common import github_token, load_emulator_profiles, suggest_names
|
||||
from common import (
|
||||
_norm_system_id,
|
||||
github_token,
|
||||
load_emulator_profiles,
|
||||
suggest_names,
|
||||
system_index,
|
||||
)
|
||||
from hashing import parse_hash_list
|
||||
from sourceref import ( # noqa: F401 (re-exported for callers and tests) # noqa: F401 (the prose grammar, re-exported)
|
||||
_ANNOTATION_RE,
|
||||
@@ -488,9 +494,17 @@ def _find_all(haystack: list[str], needle: list[str]) -> list[int]:
|
||||
def _map_index(
|
||||
opcodes: list[tuple[str, int, int, int, int]], index: int
|
||||
) -> int | None:
|
||||
"""Line of HEAD that a pinned line became.
|
||||
|
||||
A replace block of equal size edits each line in place (a column added to
|
||||
every row of a table), so its lines map one to one like an equal block.
|
||||
Collapsed onto the first line, eight refs citing eight rows all landed on
|
||||
the first row.
|
||||
"""
|
||||
for tag, i1, i2, j1, j2 in opcodes:
|
||||
if i1 <= index < i2:
|
||||
return j1 + (index - i1) if tag == "equal" else j1
|
||||
in_place = tag == "equal" or (tag == "replace" and i2 - i1 == j2 - j1)
|
||||
return j1 + (index - i1) if in_place else j1
|
||||
return None
|
||||
|
||||
|
||||
@@ -958,6 +972,14 @@ class ProfileReport:
|
||||
# release, kept on the report so every output carries it.
|
||||
version: VersionReport | None = None
|
||||
version_warning: str | None = None
|
||||
# --detect-new-files and --watch-hashes, kept on the report so --json
|
||||
# carries them to check_freshness. None when not asked. A new file is
|
||||
# {path, line, name, since_pin}: since_pin when the pinned revision of
|
||||
# the same file does not carry the name. A new hash is {path, kind, value}.
|
||||
new_files: list[dict] | None = None
|
||||
new_files_elided: dict[str, int] = field(default_factory=dict)
|
||||
new_hashes: list[dict] | None = None
|
||||
detection_skipped: str | None = None
|
||||
|
||||
# Skips that say the profile cannot be checked as written: a repository
|
||||
# that no longer answers, no pin to read, a death the forge contradicts.
|
||||
@@ -3573,11 +3595,16 @@ def select_profiles(profiles: dict, args: argparse.Namespace) -> dict:
|
||||
)
|
||||
return {args.emulator: profiles[args.emulator]}
|
||||
|
||||
# --system matches as verify.py --list-systems lists and --system selects:
|
||||
# by normalized id, so `saturn` reaches the profiles declaring sega-saturn.
|
||||
wanted_system = _norm_system_id(args.system) if args.system else None
|
||||
selected = {}
|
||||
for name, profile in sorted(profiles.items()):
|
||||
if profile.get("type") in ("alias", "test"):
|
||||
continue
|
||||
if args.system and args.system not in (profile.get("systems") or []):
|
||||
if wanted_system and wanted_system not in {
|
||||
_norm_system_id(str(s)) for s in profile.get("systems") or []
|
||||
}:
|
||||
continue
|
||||
if args.type_ and profile.get("type") != args.type_:
|
||||
continue
|
||||
@@ -3599,7 +3626,8 @@ def select_profiles(profiles: dict, args: argparse.Namespace) -> dict:
|
||||
)
|
||||
hint = ""
|
||||
if args.system:
|
||||
systems = sorted({s for p in profiles.values() for s in p.get("systems") or []})
|
||||
# Suggested from the spellings the named listing prints.
|
||||
systems = sorted(spelling for spelling, _ in system_index(profiles).values())
|
||||
hint = " " + suggest_names([args.system], systems, "verify.py --list-systems")
|
||||
elif args.type_:
|
||||
kinds = sorted({str(p.get("type")) for p in profiles.values() if p.get("type")})
|
||||
@@ -3748,37 +3776,84 @@ def _cited_paths(report: ProfileReport) -> list[str]:
|
||||
)
|
||||
|
||||
|
||||
def _print_detection(
|
||||
def _attach_detection(
|
||||
args: argparse.Namespace, profile: dict, report: ProfileReport
|
||||
) -> None:
|
||||
"""Record undeclared names and hashes of the cited files on the report.
|
||||
|
||||
Kept on the report rather than printed, so --json carries them as the
|
||||
text report does. A forge that refuses is named on the report: the ref
|
||||
report already muted it, and one refusal must not end a pass over every
|
||||
profile. A quota signal still stops everything.
|
||||
"""
|
||||
if not report.head or not (args.detect_new_files or args.watch_hashes):
|
||||
return
|
||||
repo = select_repo(profile)
|
||||
if repo is None:
|
||||
return
|
||||
try:
|
||||
_detect(args, profile, report, repo)
|
||||
except upstream.RateLimitError:
|
||||
raise
|
||||
except upstream.UpstreamError as exc:
|
||||
report.detection_skipped = str(exc)
|
||||
|
||||
|
||||
def _detect(
|
||||
args: argparse.Namespace, profile: dict, report: ProfileReport,
|
||||
repo: upstream.Repo,
|
||||
) -> None:
|
||||
names, hashes = declared_names(profile), declared_hashes(profile)
|
||||
new_files: list[dict] = []
|
||||
new_hashes: list[dict] = []
|
||||
for path in _cited_paths(report):
|
||||
head_lines = upstream.fetch_file(
|
||||
repo, report.head, path, args.cache_dir, args.offline
|
||||
)
|
||||
if head_lines is None:
|
||||
continue
|
||||
pin_lines = (
|
||||
upstream.fetch_file(repo, report.pin, path, args.cache_dir, args.offline)
|
||||
or []
|
||||
)
|
||||
if args.detect_new_files:
|
||||
found, elided = detect_new_files(head_lines, names)
|
||||
for number, name in found:
|
||||
print(f" new file: {path}:{number} {name}")
|
||||
if elided:
|
||||
print(f" new file: {elided} further matches not shown")
|
||||
if args.watch_hashes:
|
||||
pin_lines = (
|
||||
upstream.fetch_file(
|
||||
repo, report.pin, path, args.cache_dir, args.offline
|
||||
)
|
||||
or []
|
||||
at_pin = {
|
||||
name.casefold()
|
||||
for _, name in detect_new_files(pin_lines, names, cap=sys.maxsize)[0]
|
||||
}
|
||||
new_files.extend(
|
||||
{"path": path, "line": number, "name": name,
|
||||
"since_pin": name.casefold() not in at_pin}
|
||||
for number, name in found
|
||||
)
|
||||
if elided:
|
||||
report.new_files_elided[path] = elided
|
||||
if args.watch_hashes:
|
||||
added = [
|
||||
line
|
||||
for line in difflib.unified_diff(pin_lines, head_lines, n=0)
|
||||
if line.startswith("+") and not line.startswith("+++")
|
||||
]
|
||||
for kind, value in watch_hashes(added, hashes):
|
||||
print(f" new {kind}: {path} {value}")
|
||||
new_hashes.extend(
|
||||
{"path": path, "kind": kind, "value": value}
|
||||
for kind, value in watch_hashes(added, hashes)
|
||||
)
|
||||
if args.detect_new_files:
|
||||
report.new_files = new_files
|
||||
if args.watch_hashes:
|
||||
report.new_hashes = new_hashes
|
||||
|
||||
|
||||
def _print_detection(report: ProfileReport) -> None:
|
||||
if report.detection_skipped:
|
||||
print(f" extras skipped: {report.detection_skipped}")
|
||||
for item in report.new_files or ():
|
||||
print(f" new file: {item['path']}:{item['line']} {item['name']}")
|
||||
for path, elided in report.new_files_elided.items():
|
||||
print(f" new file: {path}: {elided} further matches not shown")
|
||||
for item in report.new_hashes or ():
|
||||
print(f" new {item['kind']}: {item['path']} {item['value']}")
|
||||
|
||||
|
||||
def _attach_version(
|
||||
@@ -3812,13 +3887,8 @@ def _print_extras(
|
||||
instead of ending a pass over every profile. A quota signal still
|
||||
stops everything.
|
||||
"""
|
||||
wants = (
|
||||
args.detect_new_files
|
||||
or args.watch_hashes
|
||||
or args.full_diff
|
||||
or args.tree_diff
|
||||
)
|
||||
if not wants or not report.head:
|
||||
_print_detection(report)
|
||||
if not (args.full_diff or args.tree_diff) or not report.head:
|
||||
return
|
||||
repo = select_repo(profile)
|
||||
if repo is None:
|
||||
@@ -3835,8 +3905,6 @@ def _print_extras_from(
|
||||
args: argparse.Namespace, profile: dict, report: ProfileReport,
|
||||
repo: upstream.Repo,
|
||||
) -> None:
|
||||
if args.detect_new_files or args.watch_hashes:
|
||||
_print_detection(args, profile, report, repo)
|
||||
if args.full_diff:
|
||||
for path in _cited_paths(report):
|
||||
if args.ref and args.ref not in path:
|
||||
@@ -3983,13 +4051,15 @@ def _refuse_flag_combinations(args: argparse.Namespace) -> None:
|
||||
]
|
||||
# The version check lives on the report, so the outputs built from the
|
||||
# report (text, --json, --markdown) carry it. A URL list and a ranking
|
||||
# do not.
|
||||
# do not. Detection lives on the report too, and --json carries it for
|
||||
# check_freshness. The markdown table has no column for it.
|
||||
lists_only = args.fetch_plan or args.triage
|
||||
off_json = lists_only or args.markdown
|
||||
extras = [
|
||||
flag for flag, on in (
|
||||
("--check-version", args.check_version and lists_only),
|
||||
("--detect-new-files", args.detect_new_files),
|
||||
("--watch-hashes", args.watch_hashes),
|
||||
("--detect-new-files", args.detect_new_files and off_json),
|
||||
("--watch-hashes", args.watch_hashes and off_json),
|
||||
("--full-diff", args.full_diff),
|
||||
("--tree-diff", args.tree_diff),
|
||||
) if on
|
||||
@@ -4086,6 +4156,7 @@ def main() -> None:
|
||||
profile["version_source"].get("no_genuine_copy") or "") or None
|
||||
if args.check_version:
|
||||
_attach_version(args, profile, report)
|
||||
_attach_detection(args, profile, report)
|
||||
reports.append(report)
|
||||
if writes:
|
||||
try:
|
||||
|
||||
@@ -578,7 +578,9 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Preview without downloading"
|
||||
)
|
||||
parser.add_argument("--platform", type=platform_arg, help="Only refresh entries for this platform")
|
||||
parser.add_argument(
|
||||
"--platform", "-p", type=platform_arg, help="Only refresh entries for this platform"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry", default=DEFAULT_REGISTRY, help="Path to _data_dirs.yml"
|
||||
)
|
||||
|
||||
+2
-2
@@ -897,7 +897,7 @@ def main() -> int:
|
||||
"the file a platform baseline would ship.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--platform", type=platform_arg,
|
||||
"--platform", "-p", type=platform_arg,
|
||||
help="one platform instead of all",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -918,7 +918,7 @@ def main() -> int:
|
||||
help="with all platforms, the archived ones too (RetroPie)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target", help="judge only the cores a hardware target builds",
|
||||
"--target", "-t", help="judge only the cores a hardware target builds",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
|
||||
Reference in new issue
Block a user