mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Restore native POSIX setup tools and platform release validation
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/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())
|
||||
@@ -530,9 +530,13 @@ ${DISPLAY_NAME} ${VERSION} — setup package
|
||||
Platform: ${ARTIFACT}
|
||||
|
||||
One zip for first install and updates. Does NOT include disc images, retail
|
||||
BIOS dumps, pre-generated game C, or a portable cmake/clang pack. Emitters
|
||||
BIOS dumps, pre-generated game C, or a bundled build-tool pack. Emitters
|
||||
(psxrecomp-game / psxrecomp-bios) and the CLI are inside psxrecomp/.
|
||||
EOF
|
||||
|
||||
case "${ARTIFACT}" in
|
||||
windows-*)
|
||||
cat >>"${STAGE}/README-SETUP.txt" <<EOF
|
||||
Standalone:
|
||||
1. Install Python 3.
|
||||
2. Run ${EXE_BASENAME}.
|
||||
@@ -546,6 +550,28 @@ Retro uses this same zip: it harvests emitters into a shared SDK cache,
|
||||
downloads the toolchain pack (or uses RETCOMM_TOOLCHAIN_DIR), and preserves
|
||||
saves/user config across updates.
|
||||
EOF
|
||||
;;
|
||||
linux-*|macos-*)
|
||||
cat >>"${STAGE}/README-SETUP.txt" <<EOF
|
||||
Standalone:
|
||||
1. Install CMake, Ninja, Python 3, and a C/C++ compiler with your system
|
||||
package manager.
|
||||
2. Make sure cmake, ninja, python3, and either clang/clang++ or gcc/g++ are
|
||||
available on PATH.
|
||||
3. Run ${EXE_BASENAME}.
|
||||
4. Provide ${DISC_HINT} and ${BIOS_HINT}.
|
||||
5. Follow the Generate & rebuild wizard. The host checks the native build
|
||||
tools before it continues.
|
||||
|
||||
Retro uses this same zip. It uses the same native build tools and preserves
|
||||
saves/user config across updates.
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "error: unsupported setup README platform: ${ARTIFACT}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Gate: the staged tree must be able to configure itself ----------------
|
||||
# The project file/dir list is an allowlist, so any path a project adds to
|
||||
|
||||
@@ -956,6 +956,30 @@ def ensure_toolchain(
|
||||
Installs always land under the shared Retro cache; the project only gets
|
||||
a stamp file pointing at that pack.
|
||||
"""
|
||||
if not sys_platform_is_windows() and from_zip is None:
|
||||
# Setup/rebuild on Linux and macOS uses native tools. Do not turn a
|
||||
# missing dependency into an unsolicited portable-pack download.
|
||||
groups = (("cmake",), ("ninja",), ("python3", "python"),
|
||||
("cc", "gcc", "clang"), ("c++", "g++", "clang++"))
|
||||
resolved = []
|
||||
for names in groups:
|
||||
command = next(filter(None, (shutil.which(name) for name in names)), None)
|
||||
if command:
|
||||
try:
|
||||
result = subprocess.run([command, "--version"], capture_output=True,
|
||||
text=True, timeout=30, check=False)
|
||||
if result.returncode == 0:
|
||||
resolved.append(command)
|
||||
continue
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
raise RuntimeError("Native build tools are missing or unusable: {}. Install CMake, "
|
||||
"Ninja, Python 3 and C/C++ compilers with your package manager."
|
||||
.format("/".join(names)))
|
||||
if log:
|
||||
log("Using native build tools on PATH")
|
||||
return Path(resolved[0]).parent
|
||||
|
||||
need = min_version or default_min_version()
|
||||
if is_windows_store_python() and log:
|
||||
log(
|
||||
|
||||
Reference in New Issue
Block a user