#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Validate the offline Phase-1.0A RetroArch PS5 port evidence.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path import re import subprocess import sys from typing import Any PHASE = "PHASE_1_0A_RETROARCH_PS5_PORT_BOOTSTRAP" STATUS = "RETROARCH_PS5_SOFTWARE_PORT_BUILT" BASELINE = "4c086f84ab2e7c53e6750b66b3b3e1f591ff6a06" BRANCH = "codex/chimera-gfx-phase10a-retroarch-port-bootstrap" RETROARCH_BRANCH = "codex/ps5-port-bootstrap" RETROARCH_TAG = "v1.22.2" RETROARCH_COMMIT = "69a4f0ea1e8aaf442ae4858f2e7f2b31a1776576" RETROARCH_TREE = "33babf9eb7699b5d571a3063ea21c3e488c159fe" RETROARCH_ARCHIVE_SHA256 = ( "245ef18c8fa8fbd9fbb5eb25cf43e17c6aace2f95c1ed99873cbd794012bb232" ) SDK_COMMIT = "d2e2e585740362976a39fdd5ccf390f199a7bc37" SDL_COMMIT = "0baf4ac49382b537ba449901b5b6d0d189bb1fbb" PACBREW_COMMIT = "c2abcfcb60f569128abd0e8e70ad03a67bee5ea7" DENYLIST_SHA256 = "e9603b0e3792781ad5b511afb22ef61e3d4fd4c5a16bf928f8609193bcd97783" OFFLINE_AUTHORIZATION_FIELDS = ( "offline_source_acquisition_authorized", "offline_source_modification_authorized", "offline_target_build_authorized", "offline_artifact_analysis_authorized", "private_gitea_push_authorized", ) DEVICE_AUTHORIZATION_FIELDS = ( "ps5_connection_authorized", "device_transfer_authorized", "device_execution_authorized", "installation_authorized", "lifecycle_authorized", "autoload_authorized", "device_write_authorized", "automatic_retry", ) DELIVERABLES = ( "docs/retroarch/phase-1.0a-port-plan.md", "docs/retroarch/phase-1.0a-upstream-analysis.md", "docs/retroarch/phase-1.0a-ps4-reference-delta.md", "docs/retroarch/phase-1.0a-pacbrew-sdl-analysis.md", "docs/retroarch/phase-1.0a-build-results.md", "docs/retroarch/phase-1.0a-driver-status.md", "docs/retroarch/phase-1.0a-next-device-smoke-test.md", "manifests/retroarch/upstreams.json", "manifests/retroarch/phase-1.0a-build.json", "manifests/retroarch/phase-1.0a-artifacts.json", "tests/test_retroarch_phase10a.py", "tools/validate_retroarch_phase10a.py", "packaging/retroarch/phase10a/SHA256SUMS.txt", ) REQUIRED_REAL_SYMBOLS = { "rarch_main", "retroarch_main_init", "runloop_iterate", "retro_init", "retro_deinit", "retro_run", "frontend_ctx_ps5", } DISABLED_FEATURES = { "networking", "online_updater", "dynamic_cores", "autoload", "installation", "gnm", "opengl", "vulkan", } FORBIDDEN_MARKERS = ( "chimera_lifecycle_probe", "prospero-deploy", "payload_sender", "sceSystemServiceLoadExec", ) def load_json(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as stream: value = json.load(stream) if not isinstance(value, dict): raise ValueError(f"{path} is not a JSON object") return value def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def artifact_execution_allowed(record: dict[str, Any]) -> bool: return bool(record.get("execution_eligible", False)) def route_is_forbidden(text: str) -> bool: lowered = text.lower() return any(marker.lower() in lowered for marker in FORBIDDEN_MARKERS) def profile_is_closed(profile: dict[str, Any]) -> bool: disabled = set(profile.get("disabled_features", [])) return ( DISABLED_FEATURES.issubset(disabled) and profile.get("automatic_retry") is False and profile.get("filesystem_writes_on_default_path") is False ) def is_real_retroarch_artifact(record: dict[str, Any]) -> bool: return REQUIRED_REAL_SYMBOLS.issubset(set(record.get("required_symbols", []))) def has_personal_path(strings: list[str]) -> bool: patterns = ( re.compile(r"[A-Za-z]:\\Users\\", re.IGNORECASE), re.compile(r"/(?:mnt/[a-z]/)?Users/[^/]+/", re.IGNORECASE), re.compile(r"/home/[^/]+/", re.IGNORECASE), ) return any(pattern.search(value) for value in strings for pattern in patterns) def has_ip_address(strings: list[str]) -> bool: pattern = re.compile(r"(? list[str]: errors: list[str] = [] name = record.get("name", "") if artifact_execution_allowed(record): errors.append(f"{name}: execution_eligible must be false") if record.get("target_execution_performed") is not False: errors.append(f"{name}: target execution state is not false") if record.get("reproducibility") != "BYTE_IDENTICAL_TWO_CLEAN_BUILDS": errors.append(f"{name}: reproducibility is not byte exact") if not is_real_retroarch_artifact(record): errors.append(f"{name}: real RetroArch symbols are incomplete") if len(record.get("sha256", "")) != 64 or record.get("size", 0) <= 0: errors.append(f"{name}: invalid size/hash identity") if not record.get("linker_map_sha256"): errors.append(f"{name}: linker map identity is missing") if not isinstance(record.get("undefined_symbols"), list): errors.append(f"{name}: undefined-symbol inventory is missing") if record.get("embedded_personal_paths") is not False: errors.append(f"{name}: personal path audit did not pass") if not isinstance(record.get("embedded_ip_addresses"), list): errors.append(f"{name}: IP-address inventory is missing") if record.get("embedded_device_ip_addresses") is not False: errors.append(f"{name}: a device/private address is embedded") if record.get("forbidden_markers_found"): errors.append(f"{name}: forbidden marker found") return errors def git(root: Path, *args: str) -> str: result = subprocess.run( ["git", *args], cwd=root, capture_output=True, text=True, check=False, ) if result.returncode: raise RuntimeError(result.stderr.strip() or "git command failed") return result.stdout.strip() def validate(root: Path, retroarch_root: Path | None = None) -> list[str]: errors: list[str] = [] for relative in DELIVERABLES: if not (root / relative).is_file(): errors.append(f"missing deliverable: {relative}") try: build = load_json(root / "manifests/retroarch/phase-1.0a-build.json") artifacts_doc = load_json( root / "manifests/retroarch/phase-1.0a-artifacts.json" ) upstreams = load_json(root / "manifests/retroarch/upstreams.json") except (OSError, ValueError, json.JSONDecodeError) as error: return errors + [str(error)] if build.get("phase") != PHASE or build.get("status") != STATUS: errors.append("phase/status mismatch") if build.get("baseline_commit") != BASELINE: errors.append("baseline mismatch") if build.get("branch") != BRANCH: errors.append("branch mismatch") for field in OFFLINE_AUTHORIZATION_FIELDS: if build.get(field) is not True: errors.append(f"{field} must record the explicit offline grant") for field in DEVICE_AUTHORIZATION_FIELDS: if build.get(field) is not False: errors.append(f"{field} must remain false") for field in ( "ps5_connected", "device_request_performed", "files_transferred", "device_write_performed", "target_execution_performed", "install_package_created", "execution_package_created", ): if build.get(field) is not False: errors.append(f"{field} must remain false") fork = build.get("chimera_retroarch", {}) if fork.get("branch") != RETROARCH_BRANCH or len(fork.get("commit", "")) != 40: errors.append("private fork identity is incomplete") if fork.get("working_tree_clean") is not True: errors.append("private fork was not clean at final build") profiles = build.get("profiles", {}) for name in ( "ps5-headless-smokecore", "ps5-software-rgui-smokecore", "host-smokecore-integration", ): if name not in profiles or not profile_is_closed(profiles[name]): errors.append(f"profile is not closed: {name}") host = build.get("host_tests", {}) if ( host.get("frames") != 600 or host.get("video_fnv1a64") != "43f920496eb5f435" or host.get("audio_fnv1a64") != "a48f47dc08c56625" or host.get("asan_ubsan") != "PASS" or host.get("input_mapping") != "PASS" or host.get("clean_shutdown") != "PASS" ): errors.append("host smoke-core evidence mismatch") expected_upstreams = { "retroarch": RETROARCH_COMMIT, "ps5_payload_sdk": SDK_COMMIT, "ps5_sdl": SDL_COMMIT, "pacbrew": PACBREW_COMMIT, } for name, commit in expected_upstreams.items(): if upstreams.get("sources", {}).get(name, {}).get("commit") != commit: errors.append(f"upstream mismatch: {name}") retroarch = upstreams.get("sources", {}).get("retroarch", {}) if ( retroarch.get("tag") != RETROARCH_TAG or retroarch.get("tree") != RETROARCH_TREE or retroarch.get("source_archive_sha256") != RETROARCH_ARCHIVE_SHA256 ): errors.append("RetroArch release provenance mismatch") records = artifacts_doc.get("artifacts", []) if {item.get("name") for item in records} != { "retroarch_ps5_headless.elf", "retroarch_ps5_software.elf", }: errors.append("artifact set mismatch") for record in records: errors.extend(validate_artifact(record)) denylist = root / "manifests/artifact-denylist.json" if not denylist.is_file() or sha256_file(denylist) != DENYLIST_SHA256: errors.append("permanent denylist changed") tracked = git(root, "ls-files").splitlines() forbidden_suffixes = (".elf", ".self", ".sprx", ".pkg") for relative in tracked: if relative.lower().endswith(forbidden_suffixes): errors.append(f"tracked target artifact: {relative}") new_text = "\n".join( (root / relative).read_text(encoding="utf-8", errors="replace") for relative in DELIVERABLES if (root / relative).is_file() ) unfinished_template = "{" * 2 unfinished_label = "".join(("T", "B", "D")) if unfinished_template in new_text or unfinished_label in new_text: errors.append("unfinished placeholder in deliverables") if retroarch_root is not None: for record in records: path = retroarch_root / record["local_relative_path"] if not path.is_file(): errors.append(f"local artifact missing: {path}") continue if path.stat().st_size != record["size"] or sha256_file(path) != record[ "sha256" ]: errors.append(f"local artifact identity mismatch: {path.name}") return errors def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", type=Path, required=True) parser.add_argument("--retroarch-root", type=Path) args = parser.parse_args() errors = validate( args.root.resolve(), args.retroarch_root.resolve() if args.retroarch_root else None, ) if errors: for error in errors: print(error, file=sys.stderr) return 1 print("Phase 1.0A RetroArch evidence validated") return 0 if __name__ == "__main__": raise SystemExit(main())