206 lines
8.0 KiB
Python
206 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Twenty-five host-only guardrails for Phase 1.0A."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import sys
|
|
from types import ModuleType
|
|
from typing import Callable
|
|
|
|
|
|
def load_module(name: str, path: Path) -> ModuleType:
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError(f"could not load {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise RuntimeError(message)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
validator = load_module(
|
|
"phase10a_validator", root / "tools/validate_retroarch_phase10a.py"
|
|
)
|
|
cases: list[tuple[str, Callable[[], None]]] = []
|
|
|
|
def case(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
|
|
def register(function: Callable[[], None]) -> Callable[[], None]:
|
|
cases.append((name, function))
|
|
return function
|
|
|
|
return register
|
|
|
|
def artifact(**changes: object) -> dict[str, object]:
|
|
value: dict[str, object] = {
|
|
"name": "retroarch_ps5_headless.elf",
|
|
"execution_eligible": False,
|
|
"target_execution_performed": False,
|
|
"reproducibility": "BYTE_IDENTICAL_TWO_CLEAN_BUILDS",
|
|
"required_symbols": sorted(validator.REQUIRED_REAL_SYMBOLS),
|
|
"sha256": "a" * 64,
|
|
"size": 1,
|
|
"linker_map_sha256": "b" * 64,
|
|
"undefined_symbols": [],
|
|
"embedded_personal_paths": False,
|
|
"embedded_ip_addresses": ["127.0.0.1"],
|
|
"embedded_device_ip_addresses": False,
|
|
"forbidden_markers_found": [],
|
|
}
|
|
value.update(changes)
|
|
return value
|
|
|
|
def profile(**changes: object) -> dict[str, object]:
|
|
value: dict[str, object] = {
|
|
"disabled_features": sorted(validator.DISABLED_FEATURES),
|
|
"automatic_retry": False,
|
|
"filesystem_writes_on_default_path": False,
|
|
}
|
|
value.update(changes)
|
|
return value
|
|
|
|
@case("01 smokecore API compliance is recorded")
|
|
def _() -> None:
|
|
text = (root / "docs/retroarch/phase-1.0a-driver-status.md").read_text()
|
|
require("libretro" in text.lower(), "libretro contract absent")
|
|
|
|
@case("02 600 deterministic frames are pinned")
|
|
def _() -> None:
|
|
require("frames\") != 600" in (root / "tools/validate_retroarch_phase10a.py").read_text(), "frame guard absent")
|
|
|
|
@case("03 video hash is pinned")
|
|
def _() -> None:
|
|
require("43f920496eb5f435" in (root / "tools/validate_retroarch_phase10a.py").read_text(), "video hash absent")
|
|
|
|
@case("04 audio hash is pinned")
|
|
def _() -> None:
|
|
require("a48f47dc08c56625" in (root / "tools/validate_retroarch_phase10a.py").read_text(), "audio hash absent")
|
|
|
|
@case("05 input mapping is required")
|
|
def _() -> None:
|
|
require("input_mapping" in (root / "tools/validate_retroarch_phase10a.py").read_text(), "input guard absent")
|
|
|
|
@case("06 clean core shutdown is required")
|
|
def _() -> None:
|
|
require("clean_shutdown" in (root / "tools/validate_retroarch_phase10a.py").read_text(), "shutdown guard absent")
|
|
|
|
@case("07 real RetroArch registration is required")
|
|
def _() -> None:
|
|
require(validator.is_real_retroarch_artifact(artifact()), "real symbols rejected")
|
|
|
|
@case("08 platform feature matrix is explicit")
|
|
def _() -> None:
|
|
require(validator.profile_is_closed(profile()), "closed profile rejected")
|
|
|
|
@case("09 disabled features remain disabled")
|
|
def _() -> None:
|
|
require(not validator.profile_is_closed(profile(disabled_features=[])), "open profile accepted")
|
|
|
|
@case("10 default config write is forbidden")
|
|
def _() -> None:
|
|
require(not validator.profile_is_closed(profile(filesystem_writes_on_default_path=True)), "write path accepted")
|
|
|
|
@case("11 networking is disabled")
|
|
def _() -> None:
|
|
disabled = validator.DISABLED_FEATURES - {"networking"}
|
|
require(not validator.profile_is_closed(profile(disabled_features=sorted(disabled))), "networking accepted")
|
|
|
|
@case("12 updater is disabled")
|
|
def _() -> None:
|
|
disabled = validator.DISABLED_FEATURES - {"online_updater"}
|
|
require(not validator.profile_is_closed(profile(disabled_features=sorted(disabled))), "updater accepted")
|
|
|
|
@case("13 autoload is disabled")
|
|
def _() -> None:
|
|
disabled = validator.DISABLED_FEATURES - {"autoload"}
|
|
require(not validator.profile_is_closed(profile(disabled_features=sorted(disabled))), "autoload accepted")
|
|
|
|
@case("14 installation is disabled")
|
|
def _() -> None:
|
|
disabled = validator.DISABLED_FEATURES - {"installation"}
|
|
require(not validator.profile_is_closed(profile(disabled_features=sorted(disabled))), "installation accepted")
|
|
|
|
@case("15 lifecycle probe marker is forbidden")
|
|
def _() -> None:
|
|
require(
|
|
validator.route_is_forbidden("chimera_lifecycle_probe"),
|
|
"lifecycle probe marker accepted",
|
|
)
|
|
|
|
@case("16 GNM is disabled")
|
|
def _() -> None:
|
|
disabled = validator.DISABLED_FEATURES - {"gnm"}
|
|
require(not validator.profile_is_closed(profile(disabled_features=sorted(disabled))), "GNM accepted")
|
|
|
|
@case("17 headless artifact must be real RetroArch")
|
|
def _() -> None:
|
|
require(not validator.is_real_retroarch_artifact(artifact(required_symbols=["main"])), "sample accepted")
|
|
|
|
@case("18 static core symbols are mandatory")
|
|
def _() -> None:
|
|
symbols = validator.REQUIRED_REAL_SYMBOLS - {"retro_run"}
|
|
require(not validator.is_real_retroarch_artifact(artifact(required_symbols=sorted(symbols))), "coreless ELF accepted")
|
|
|
|
@case("19 software backend is explicit")
|
|
def _() -> None:
|
|
manifest = validator.load_json(root / "manifests/retroarch/phase-1.0a-build.json")
|
|
features = manifest["profiles"]["ps5-software-rgui-smokecore"]["enabled_features"]
|
|
require({"sdl2_software_video", "sdl2_ps5_pad", "sdl2_ps5_audio"}.issubset(features), "software backend matrix incomplete")
|
|
|
|
@case("20 unsupported features are not silent successes")
|
|
def _() -> None:
|
|
text = (root / "docs/retroarch/phase-1.0a-driver-status.md").read_text()
|
|
require("Explicitly unsupported" in text, "unsupported matrix absent")
|
|
|
|
@case("21 exact upstream commits are bound")
|
|
def _() -> None:
|
|
upstreams = validator.load_json(root / "manifests/retroarch/upstreams.json")
|
|
require(upstreams["sources"]["retroarch"]["commit"] == validator.RETROARCH_COMMIT, "RetroArch commit mismatch")
|
|
|
|
@case("22 target output is never execution eligible")
|
|
def _() -> None:
|
|
require(not validator.artifact_execution_allowed(artifact()), "artifact became eligible")
|
|
|
|
@case("23 no sender is part of build")
|
|
def _() -> None:
|
|
require(validator.route_is_forbidden("payload_sender"), "sender marker accepted")
|
|
|
|
@case("24 IP addresses are detected")
|
|
def _() -> None:
|
|
require(validator.has_ip_address(["connect 192.0.2.1"]), "IP not detected")
|
|
|
|
@case("25 personal host paths are detected")
|
|
def _() -> None:
|
|
require(validator.has_personal_path([r"C:\Users\Example\port"]), "host path not detected")
|
|
|
|
failures: list[str] = []
|
|
for name, function in cases:
|
|
try:
|
|
function()
|
|
except Exception as error: # noqa: BLE001 - isolated test reporting
|
|
failures.append(f"{name}: {error}")
|
|
if len(cases) != 25:
|
|
failures.append(f"expected 25 cases, found {len(cases)}")
|
|
if failures:
|
|
print("\n".join(failures), file=sys.stderr)
|
|
return 1
|
|
print("25 Phase 1.0A guardrails passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|