chore: merge round 2 fixes (tooling)

This commit is contained in:
Abdessamad Derraz committed 2026-10-21 10:31:04 +02:00
commit 055627f39c
22 files changed
+704 -141

No files matched your search

+18 -3
View File
@@ -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
View File
@@ -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),
)
)
+10 -5
View File
@@ -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 = (
+2 -2
View File
@@ -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",
)
+1 -1
View File
@@ -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
View File
@@ -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__":
+1 -1
View File
@@ -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,
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+3 -1
View File
@@ -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
View File
@@ -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",