mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
169 lines
5.0 KiB
Python
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())
|