Files
psxrecomp/tools/ci/audit_setup_package_platform_copy.py
T

169 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Reject setup ZIPs whose toolchain copy does not match the host platform."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
import zipfile
from pathlib import Path, PurePosixPath
PLATFORM_MARKERS = {
"windows": ("windows-x64",),
"linux": ("linux-x64",),
"macos": ("macos-x64", "macos-arm64"),
}
def sha256(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().upper()
def infer_platform(path: Path) -> str:
lower = path.name.lower()
matches = [
platform
for platform, markers in PLATFORM_MARKERS.items()
if any(marker in lower for marker in markers)
]
if len(matches) != 1:
raise ValueError(
"archive name must contain exactly one supported platform marker: "
"windows-x64, linux-x64, macos-x64, or macos-arm64"
)
return matches[0]
def root_member(names: list[str], basename: str) -> str:
matches = [
name
for name in names
if not name.endswith("/")
and len(PurePosixPath(name).parts) == 1
and PurePosixPath(name).name.lower() == basename.lower()
]
if len(matches) != 1:
raise ValueError(f"archive must contain exactly one root {basename}")
return matches[0]
def require(text: str, tokens: tuple[str, ...], where: str) -> None:
missing = [token for token in tokens if token not in text]
if missing:
raise ValueError(f"{where} is missing: {', '.join(missing)}")
def reject(text: str, tokens: tuple[str, ...], where: str) -> None:
found = [token for token in tokens if token in text]
if found:
raise ValueError(f"{where} contains forbidden copy: {', '.join(found)}")
def audit(path: Path) -> dict[str, object]:
platform = infer_platform(path)
with zipfile.ZipFile(path) as archive:
names = archive.namelist()
readme_name = root_member(names, "README-SETUP.txt")
readme = archive.read(readme_name).decode("utf-8")
run_match = re.search(r"(?m)^\s*\d+\.\s+Run\s+([^\s.]+(?:\.exe)?)\.\s*$", readme)
if not run_match:
raise ValueError("README-SETUP.txt has no numbered Run instruction")
host_name = run_match.group(1)
if platform == "windows" and not host_name.lower().endswith(".exe"):
host_name += ".exe"
host_member = root_member(names, host_name)
host_copy = archive.read(host_member).decode("latin-1")
if platform == "windows":
require(
readme,
("downloads", "cmake-clang-v1", "toolchain pack"),
"Windows README-SETUP.txt",
)
require(
host_copy,
(
"Download latest portable toolchain",
"Select toolchain zip",
"cmake-clang-v1",
),
"Windows setup host",
)
else:
require(
readme,
(
"Install CMake, Ninja, Python 3",
"C/C++ compiler",
"available on PATH",
"native build tools",
),
"POSIX README-SETUP.txt",
)
reject(
readme.lower(),
("cmake-clang-v1", "portable toolchain", "toolchain pack", "toolchain zip"),
"POSIX README-SETUP.txt",
)
require(
host_copy,
(
"1. Native build tools",
"Install CMake, Ninja, Python 3, and either Clang or GCC",
"Check tools",
),
"POSIX setup host",
)
reject(
host_copy,
("Download latest portable toolchain##tc", "1. Portable toolchain"),
"POSIX setup host",
)
return {
"archive": str(path.resolve()),
"platform": platform,
"sha256": sha256(path),
"size": path.stat().st_size,
"status": "pass",
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("archives", nargs="+", type=Path)
parser.add_argument("--json", type=Path, dest="json_path")
args = parser.parse_args()
results: list[dict[str, object]] = []
failed = False
for path in args.archives:
try:
result = audit(path)
except (OSError, ValueError, zipfile.BadZipFile) as error:
result = {"archive": str(path.resolve()), "status": "fail", "error": str(error)}
failed = True
results.append(result)
print(f"{result['status'].upper()}: {path}: {result.get('error', result.get('platform'))}")
if args.json_path:
args.json_path.parent.mkdir(parents=True, exist_ok=True)
args.json_path.write_text(
json.dumps({"schema": 1, "results": results}, indent=2) + "\n",
encoding="utf-8",
)
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())