mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Restore retail-only setup packaging and audit diagnostic release ZIPs
This commit is contained in:
@@ -67,8 +67,8 @@ offline recorder does not claim to reproduce network sessions.
|
||||
|
||||
## Replay
|
||||
|
||||
Use Python 3.11+ with the recording's original native executable, disc and game
|
||||
configuration. The tool copies the executable, runtime dependencies and
|
||||
Use Python 3.8+ (install `tomli` before Python 3.11) with the recording's original
|
||||
native executable, disc and game configuration. The tool copies the executable, runtime dependencies and
|
||||
starting state into a newly created private directory. Personal settings/cards
|
||||
and the original installation are untouched.
|
||||
|
||||
|
||||
@@ -1131,6 +1131,7 @@ if(BUILD_TESTING)
|
||||
# template, scaffold, check_generated.sh, Project Studio audit/ops).
|
||||
foreach(_t IN ITEMS
|
||||
aligned_lzss_banks
|
||||
audit_playtest_diagnostics_package
|
||||
bundled_release_layout
|
||||
collect_game_misses
|
||||
cursor_writers
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the production SDK BIOS gate with synthetic files only."""
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -28,6 +31,19 @@ class GateTests(unittest.TestCase):
|
||||
self.asset("SCPH5552.toml")
|
||||
self.assertEqual(gate.check(self.root), [str(Path("psxrecomp/bios/SCPH5552.toml"))])
|
||||
|
||||
def test_omit_requires_retail_recipe_and_no_openbios_assets(self):
|
||||
self.recipe('[runtime]\nopenbios=true\n')
|
||||
with self.assertRaisesRegex(ValueError, "requires runtime.openbios=false"):
|
||||
gate.check(self.root, omit_openbios=True)
|
||||
self.recipe('[runtime]\nopenbios=false\n')
|
||||
self.asset("SCPH1001.toml")
|
||||
self.assertEqual(len(gate.check(self.root, omit_openbios=True)), 1)
|
||||
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
|
||||
self.asset(name)
|
||||
with self.assertRaisesRegex(ValueError, "survived"):
|
||||
gate.check(self.root, omit_openbios=True)
|
||||
(self.root / "psxrecomp/bios" / name).unlink()
|
||||
|
||||
def test_retail_missing_profile_fails(self):
|
||||
self.recipe('[runtime]\nopenbios=false\n[recompiler]\nbios_config="psxrecomp/bios/SCPH5552.toml"\n')
|
||||
with self.assertRaisesRegex(ValueError, "SCPH5552"):
|
||||
@@ -118,5 +134,87 @@ class GateTests(unittest.TestCase):
|
||||
self.assertEqual(len(gate.check(self.root)), 3)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt" or not shutil.which("bash"),
|
||||
"full shell packaging fixture requires POSIX bash")
|
||||
class SetupPackageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
for path in ("build/assets/fonts/font.txt", "build/assets/img/image.txt",
|
||||
"build/mods/bundled/fixture/1.0.0/manifest.toml",
|
||||
"mods/preloaded/packages/fixture/1.0.0/manifest.toml",
|
||||
"psxrecomp/psxrecomp_cli.py", "recomp-ui/CMakeLists.txt",
|
||||
"emitters/psxrecomp-game", "emitters/psxrecomp-bios"):
|
||||
self.write(path, "synthetic fixture\n")
|
||||
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE", "SCPH1001.toml"):
|
||||
self.write("psxrecomp/bios/" + name, "synthetic BIOS fixture")
|
||||
self.write("CMakeLists.txt", "# synthetic title\n")
|
||||
self.write("VERSION", "0.0.1\n")
|
||||
self.write("build/psx_game_version.txt", "0.0.1\n")
|
||||
self.write("build/Fixture", "synthetic executable, never run\n")
|
||||
self.write("build/replay-diagnostics.py", "# staged replay helper\n")
|
||||
# Packaging tests exercise staging, not a system compression utility.
|
||||
# Ubuntu developer images may omit zip; write a real ZIP with stdlib.
|
||||
self.write("fixture-bin/zip", '''#!/usr/bin/env python3
|
||||
import pathlib, sys, zipfile
|
||||
with zipfile.ZipFile(sys.argv[-2], "w") as archive:
|
||||
for path in pathlib.Path(".").rglob("*"):
|
||||
if path.is_file():
|
||||
archive.write(path, path.as_posix())
|
||||
''')
|
||||
(self.root / "fixture-bin/zip").chmod(0o755)
|
||||
|
||||
def write(self, name, text):
|
||||
path = self.root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
def package(self, retail):
|
||||
self.write("game.toml", "[runtime]\noverlay_cache=true\nopenbios=" +
|
||||
("false" if retail else "true") + "\n")
|
||||
args = ["bash", str(ROOT / "tools/package_setup_host.sh"),
|
||||
"--root", str(self.root), "--build-dir", "build", "--artifact", "fixture",
|
||||
"--zip-prefix", "test", "--exe-name", "Fixture", "--recompiler-build", "emitters",
|
||||
"--project-file", "CMakeLists.txt", "--project-file", "game.toml"]
|
||||
if retail:
|
||||
args += ["--omit-openbios", "--bios-hint", "a legal retail BIOS dump (required)"]
|
||||
env = dict(os.environ, RELEASE_VERSION="0.0.1", EXCLUDE_DEV_MODS="0")
|
||||
env["PATH"] = str(self.root / "fixture-bin") + os.pathsep + env["PATH"]
|
||||
return subprocess.run(args, cwd=self.root, env=env, capture_output=True, text=True)
|
||||
|
||||
def test_default_and_retail_packages_use_real_sdk_gate_and_keep_helper(self):
|
||||
for retail in (False, True):
|
||||
with self.subTest(retail=retail):
|
||||
result = self.package(retail)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
stage = self.root / "dist/stage-setup-fixture"
|
||||
self.assertTrue((stage / "replay-diagnostics.py").is_file())
|
||||
self.assertTrue((stage / "psxrecomp/bios/SCPH1001.toml").is_file())
|
||||
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
|
||||
self.assertEqual((stage / "psxrecomp/bios" / name).exists(), not retail)
|
||||
readme = (stage / "README-SETUP.txt").read_text()
|
||||
if retail:
|
||||
self.assertIn("a legal retail BIOS dump (required)", readme)
|
||||
self.assertNotIn("otherwise OpenBIOS", readme)
|
||||
else:
|
||||
self.assertIn("otherwise OpenBIOS is regenerated locally", readme)
|
||||
|
||||
def test_sdk_omission_runs_after_overlay_and_rejects_openbios_recipe(self):
|
||||
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
|
||||
self.write("overlay/bios/" + name, "overlay BIOS fixture")
|
||||
self.write("game.toml", "[runtime]\nopenbios=false\n")
|
||||
args = ["bash", str(ROOT / "tools/stage_setup_sdk.sh"), "--stage", str(self.root),
|
||||
"--framework", str(self.root / "psxrecomp"), "--sdk-overlay", str(self.root / "overlay"),
|
||||
"--recompiler-build", str(self.root / "emitters"), "--allow-no-toolchain", "--omit-openbios"]
|
||||
result = subprocess.run(args, cwd=self.root, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertFalse((self.root / "psxrecomp/bios/openbios.bin").exists())
|
||||
self.write("game.toml", "[runtime]\nopenbios=true\n")
|
||||
result = subprocess.run(args, cwd=self.root, capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("requires runtime.openbios=false", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Require BIOS assets selected by the staged title recipe."""
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
try:
|
||||
@@ -8,7 +9,7 @@ except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
def check(stage):
|
||||
def check(stage, omit_openbios=False):
|
||||
stage = Path(stage).resolve()
|
||||
recipe = stage / "game.toml"
|
||||
if not recipe.is_file():
|
||||
@@ -18,6 +19,13 @@ def check(stage):
|
||||
openbios = config.get("runtime", {}).get("openbios", True)
|
||||
if not isinstance(openbios, bool):
|
||||
raise ValueError("runtime.openbios must be a boolean")
|
||||
if omit_openbios and openbios:
|
||||
raise ValueError("--omit-openbios requires runtime.openbios=false")
|
||||
if omit_openbios:
|
||||
for root in (stage / "bios", stage / "psxrecomp/bios"):
|
||||
for name in ("OpenBIOS.toml", "openbios.bin", "OpenBIOS.LICENSE"):
|
||||
if (root / name).exists():
|
||||
raise ValueError("OpenBIOS asset survived --omit-openbios: " + str(root / name))
|
||||
profile = config.get("recompiler", {}).get("bios_config")
|
||||
required = []
|
||||
if profile:
|
||||
@@ -40,7 +48,11 @@ def check(stage):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("stage")
|
||||
parser.add_argument("--omit-openbios", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print("staged BIOS policy: " + ", ".join(check(sys.argv[1])))
|
||||
print("staged BIOS policy: " + ", ".join(check(args.stage, args.omit_openbios)))
|
||||
except (ValueError, OSError) as error:
|
||||
sys.exit("error: " + str(error))
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit a flat game/setup ZIP without extracting or launching its contents."""
|
||||
import argparse
|
||||
from pathlib import PurePosixPath
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
HD_SURFACE = re.compile(r"hd[ ._-]*(?:textures?|packs?)|texture[ ._-]*packs?", re.I)
|
||||
# These checked-in synthetic cards are SDK test inputs, never player saves.
|
||||
SDK_CARD_FIXTURES = {
|
||||
"psxrecomp/runtime/tests/fixtures/memcards/card1.mcd",
|
||||
"psxrecomp/runtime/tests/fixtures/memcards/card2.mcd",
|
||||
"psxrecomp/runtime/tests/fixtures/memcards/dummy.0.mcr",
|
||||
"psxrecomp/runtime/tests/fixtures/memcards/dummy.1.mcr",
|
||||
"psxrecomp/runtime/tests/test_card_synth.mcd",
|
||||
"psxrecomp/runtime/tests/test_dir/card1.mcd",
|
||||
}
|
||||
PERSONAL_NAMES = {
|
||||
"state.toml", "state.toml.tmp", "settings.toml", "keybinds.ini",
|
||||
"disc.cfg", "bios.cfg", "overlay_captures.json", "diagnostics-location.txt",
|
||||
}
|
||||
|
||||
|
||||
def audit(path, game_id, diagnostics_id):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
files = {}
|
||||
for entry in archive.infolist():
|
||||
name = entry.filename.replace("\\", "/")
|
||||
parts = PurePosixPath(name).parts
|
||||
if name.startswith("/") or ".." in parts or (parts and ":" in parts[0]):
|
||||
raise ValueError("unsafe archive path: " + name)
|
||||
if not entry.is_dir():
|
||||
if name in files:
|
||||
raise ValueError("duplicate archive entry: " + name)
|
||||
files[name] = entry
|
||||
# Some publishers put the same flat payload inside one named folder.
|
||||
if "game.toml" not in files:
|
||||
recipes = [name for name in files if name.count("/") == 1 and name.endswith("/game.toml")]
|
||||
if len(recipes) == 1:
|
||||
prefix = recipes[0].rsplit("/", 1)[0] + "/"
|
||||
if all(name.startswith(prefix) for name in files):
|
||||
files = {name[len(prefix):]: entry for name, entry in files.items()}
|
||||
|
||||
def read(name):
|
||||
entry = files.get(name)
|
||||
if entry is None or entry.file_size == 0:
|
||||
raise ValueError("missing or empty package file: " + name)
|
||||
if entry.file_size > 4 * 1024 * 1024:
|
||||
raise ValueError("unexpectedly large package metadata: " + name)
|
||||
return archive.read(entry)
|
||||
|
||||
def executable(name, entry):
|
||||
if "/" in name:
|
||||
return False
|
||||
if name.lower().endswith(".exe"):
|
||||
return entry.file_size > 0
|
||||
with archive.open(entry) as stream:
|
||||
return stream.read(4) == b"\x7fELF"
|
||||
|
||||
recipe = tomllib.loads(read("game.toml").decode("utf-8-sig"))
|
||||
if recipe.get("game", {}).get("id") != game_id:
|
||||
raise ValueError("game.toml game ID does not match " + game_id)
|
||||
read("replay-diagnostics.py")
|
||||
if not any(executable(name, entry) for name, entry in files.items()):
|
||||
raise ValueError("no executable adjacent to replay-diagnostics.py")
|
||||
|
||||
runtime = []
|
||||
for name in sorted(files):
|
||||
lower = name.lower()
|
||||
parts = PurePosixPath(lower).parts
|
||||
leaf = parts[-1]
|
||||
if leaf in PERSONAL_NAMES or "saves" in parts or "diagnostics" in parts or (
|
||||
lower.startswith("mods/installed/")):
|
||||
raise ValueError("personal state in package: " + name)
|
||||
suffix = PurePosixPath(lower).suffix
|
||||
if recipe.get("runtime", {}).get("openbios") is False and lower in {
|
||||
root + leaf for root in ("bios/", "psxrecomp/bios/")
|
||||
for leaf in ("openbios.bin", "openbios.toml", "openbios.license")}:
|
||||
raise ValueError("OpenBIOS asset in retail-BIOS-only package: " + name)
|
||||
if suffix in {".cue", ".iso", ".img", ".chd", ".mcd", ".mcr", ".rom", ".pst"}:
|
||||
if name not in SDK_CARD_FIXTURES:
|
||||
raise ValueError("disc, BIOS or save payload in package: " + name)
|
||||
if suffix == ".bin" and lower not in {"bios/openbios.bin", "psxrecomp/bios/openbios.bin"}:
|
||||
raise ValueError("unexpected binary image in package: " + name)
|
||||
if re.match(r"scph.*\.(bin|rom)(?:\..*)?$", leaf):
|
||||
raise ValueError("retail BIOS image in package: " + name)
|
||||
# The SDK retains its framework built-in authoring catalog. The
|
||||
# title's CMake exclusion controls whether it reaches runtime.
|
||||
if lower.startswith("mods/") and suffix in {".toml", ".json"}:
|
||||
text = read(name).decode("utf-8-sig")
|
||||
if HD_SURFACE.search(text) or HD_SURFACE.search(name):
|
||||
raise ValueError("HD texture surface in title/runtime catalog: " + name)
|
||||
if leaf == "manifest.toml":
|
||||
manifest = tomllib.loads(text)
|
||||
if lower.startswith("mods/bundled/" + diagnostics_id.lower() + "/") and manifest.get("id") == diagnostics_id:
|
||||
runtime.append((name, manifest))
|
||||
if manifest.get("id") == diagnostics_id:
|
||||
validate_diagnostics(manifest, name, game_id, diagnostics_id)
|
||||
if not runtime:
|
||||
raise ValueError("runtime mods/bundled catalog is missing " + diagnostics_id)
|
||||
return len(runtime)
|
||||
|
||||
|
||||
def validate_diagnostics(manifest, name, game_id, diagnostics_id):
|
||||
if not any(target.get("game_id") == game_id for target in manifest.get("target", [])):
|
||||
raise ValueError("diagnostics manifest has the wrong game target: " + name)
|
||||
features = {feature.get("id"): feature for feature in manifest.get("feature", [])}
|
||||
for feature_id, plugin_id in (("recording", diagnostics_id), ("osd", diagnostics_id + ".osd")):
|
||||
feature = features.get(feature_id, {})
|
||||
if feature.get("default_enabled") is not True or feature.get("channel") == "developer":
|
||||
raise ValueError("diagnostics " + feature_id + " must ship enabled by default: " + name)
|
||||
if not any(plugin.get("feature") == feature_id and plugin.get("id") == plugin_id
|
||||
for plugin in manifest.get("plugin", [])):
|
||||
raise ValueError("diagnostics " + feature_id + " plugin is missing: " + name)
|
||||
if manifest.get("channel") == "developer":
|
||||
raise ValueError("diagnostics package cannot be developer-only: " + name)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("zip")
|
||||
parser.add_argument("--game-id", required=True)
|
||||
parser.add_argument("--diagnostics-id", required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
count = audit(args.zip, args.game_id, args.diagnostics_id)
|
||||
except (ValueError, OSError, zipfile.BadZipFile, KeyError, UnicodeError) as error:
|
||||
sys.exit("diagnostics package audit failed: " + str(error))
|
||||
print("Diagnostics package audit passed: {} ({} runtime manifest(s))".format(args.diagnostics_id, count))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -22,6 +22,8 @@
|
||||
# [--project-file REL]... [--project-dir REL]... \
|
||||
# [--runtime-dir NAME]... [--runtime-dir-optional NAME]... \
|
||||
# [--disc-hint "your legally owned disc"] \
|
||||
# [--bios-hint "an optional retail SCPH-1001 BIOS; otherwise OpenBIOS"] \
|
||||
# [--omit-openbios] # retail-BIOS-only titles; game.toml openbios=false
|
||||
# [--version-env BPE_RELEASE_VERSION] \
|
||||
# [--embed-toolchain] # optional: copy PSXRECOMP_TOOLCHAIN_DIR into zip
|
||||
#
|
||||
@@ -69,6 +71,8 @@ DISPLAY_NAME=""
|
||||
RECOMPILER_BUILD="build-recompiler"
|
||||
VERSION_ENV="RELEASE_VERSION"
|
||||
DISC_HINT="your legally owned game disc"
|
||||
BIOS_HINT=""
|
||||
OMIT_OPENBIOS=0
|
||||
# Wave-5 F4: a packaged game.toml without `overlay_cache = true` ships a runtime that never
|
||||
# initialises the overlay loader, so every streamed overlay runs on the interpreter (78 of
|
||||
# 105 releases measured 2026-09-16). Refuse unless the caller states why.
|
||||
@@ -113,6 +117,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--recompiler-build) RECOMPILER_BUILD="${2:?}"; shift 2 ;;
|
||||
--version-env) VERSION_ENV="${2:?}"; shift 2 ;;
|
||||
--disc-hint) DISC_HINT="${2:?}"; shift 2 ;;
|
||||
--bios-hint) BIOS_HINT="${2:?}"; shift 2 ;;
|
||||
--omit-openbios) OMIT_OPENBIOS=1; shift ;;
|
||||
--project-file) PROJECT_FILES+=("${2:?}"); shift 2 ;;
|
||||
--project-dir) PROJECT_DIRS+=("${2:?}"); shift 2 ;;
|
||||
--no-mods) STAGE_MODS=0; shift ;;
|
||||
@@ -132,6 +138,14 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${BIOS_HINT}" ]]; then
|
||||
if [[ "${OMIT_OPENBIOS}" -eq 1 ]]; then
|
||||
BIOS_HINT="a legal retail SCPH-1001 BIOS dump (required)"
|
||||
else
|
||||
BIOS_HINT="an optional retail SCPH-1001 BIOS; otherwise OpenBIOS is regenerated locally"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Default the mods catalog into the required set unless the caller already
|
||||
# named it (in either list) or opted out. Appending rather than overriding
|
||||
# keeps an explicit --runtime-dir-optional mods meaningful for a title that
|
||||
@@ -485,6 +499,9 @@ stage_args=(
|
||||
--host-exe "${STAGE}/${EXE_BASENAME}"
|
||||
--recompiler-build "${RECOMPILER_BUILD}"
|
||||
)
|
||||
if [[ "${OMIT_OPENBIOS}" -eq 1 ]]; then
|
||||
stage_args+=(--omit-openbios)
|
||||
fi
|
||||
if [[ "${EMBED_TOOLCHAIN}" -eq 1 ]]; then
|
||||
if [[ -z "${PSXRECOMP_TOOLCHAIN_DIR:-${TOOLCHAIN_DIR:-${BPE_TOOLCHAIN_DIR:-}}}" ]]; then
|
||||
echo "error: --embed-toolchain requires PSXRECOMP_TOOLCHAIN_DIR (or TOOLCHAIN_DIR)" >&2
|
||||
@@ -519,8 +536,7 @@ BIOS dumps, pre-generated game C, or a portable cmake/clang pack. Emitters
|
||||
Standalone:
|
||||
1. Install Python 3.
|
||||
2. Run ${EXE_BASENAME}.
|
||||
3. Provide ${DISC_HINT} (and optional retail SCPH-1001 BIOS; otherwise
|
||||
OpenBIOS is regenerated locally).
|
||||
3. Provide ${DISC_HINT} and ${BIOS_HINT}.
|
||||
4. Follow the Generate & rebuild wizard. On first rebuild the host downloads
|
||||
cmake-clang-v1 from RetroPortingToolKit/RetroPorting-Toolchains (or you can
|
||||
pick a local cmake-clang-v1-*.zip for offline builds). System cmake/ninja
|
||||
|
||||
@@ -29,7 +29,10 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import tomllib
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
FORMATS = (b'# psxrecomp-input-v1', b'# psxrecomp-input-v2')
|
||||
ENV_PREFIXES = ('PSX_', 'RNET_', 'LNG_', 'PSX_DIAGNOSTIC_', 'RECOMP_')
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# --runtime-bin DIR MinGW runtime DLL search dir (repeatable)
|
||||
# --search-dir DIR Extra DLL search dir (repeatable)
|
||||
# --require-cli Require psxrecomp_cli.py after overlays (default on)
|
||||
# --omit-openbios Remove OpenBIOS assets for a retail-BIOS-only recipe
|
||||
#
|
||||
# Env aliases for toolchain dir (first wins):
|
||||
# PSXRECOMP_TOOLCHAIN_DIR, TOOLCHAIN_DIR, BPE_TOOLCHAIN_DIR
|
||||
@@ -39,6 +40,7 @@ HOST_EXE=""
|
||||
RUNTIME_BINS=()
|
||||
SEARCH_DIRS=()
|
||||
REQUIRE_CLI=1
|
||||
OMIT_OPENBIOS=0
|
||||
|
||||
usage() {
|
||||
sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
|
||||
@@ -59,6 +61,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--search-dir) SEARCH_DIRS+=("${2:?}"); shift 2 ;;
|
||||
--require-cli) REQUIRE_CLI=1; shift ;;
|
||||
--no-require-cli) REQUIRE_CLI=0; shift ;;
|
||||
--omit-openbios) OMIT_OPENBIOS=1; shift ;;
|
||||
*)
|
||||
echo "error: unknown arg: $1" >&2
|
||||
usage
|
||||
@@ -209,7 +212,16 @@ else
|
||||
echo "error: Python is required to check the staged BIOS policy" >&2
|
||||
exit 1
|
||||
fi
|
||||
"${SDK_PYTHON}" "${SCRIPT_DIR}/check_setup_bios_assets.py" "${STAGE}"
|
||||
bios_args=()
|
||||
if [[ "${OMIT_OPENBIOS}" -eq 1 ]]; then
|
||||
# Run after overlays so they cannot restore an unwanted BIOS surface.
|
||||
for bios_root in "${STAGE}/bios" "${STAGE}/psxrecomp/bios"; do
|
||||
rm -f "${bios_root}/openbios.bin" "${bios_root}/OpenBIOS.toml" \
|
||||
"${bios_root}/OpenBIOS.LICENSE"
|
||||
done
|
||||
bios_args+=(--omit-openbios)
|
||||
fi
|
||||
"${SDK_PYTHON}" "${SCRIPT_DIR}/check_setup_bios_assets.py" "${STAGE}" "${bios_args[@]}"
|
||||
|
||||
if [[ "${REQUIRE_CLI}" -eq 1 && ! -f "${STAGE}/psxrecomp/psxrecomp_cli.py" ]]; then
|
||||
echo "error: missing psxrecomp/psxrecomp_cli.py in staged tree" >&2
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the release ZIP auditor against actual synthetic archives."""
|
||||
import importlib.util
|
||||
import ast
|
||||
import builtins
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import warnings
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = ROOT / "tools/ci/audit_playtest_diagnostics_package.py"
|
||||
spec = importlib.util.spec_from_file_location("package_audit", SCRIPT)
|
||||
audit = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(audit)
|
||||
DIAG = "fixture.diagnostics"
|
||||
GAME = "SLUS-00000"
|
||||
MANIFEST = "mods/bundled/{}/1.0.0/manifest.toml".format(DIAG)
|
||||
SOURCE = "mods/preloaded/packages/{}/1.0.0/manifest.toml".format(DIAG)
|
||||
TEXT = '''id = "fixture.diagnostics"
|
||||
[[target]]
|
||||
game_id = "SLUS-00000"
|
||||
[[feature]]
|
||||
id = "recording"
|
||||
default_enabled = true
|
||||
[[feature]]
|
||||
id = "osd"
|
||||
default_enabled = true
|
||||
[[plugin]]
|
||||
feature = "recording"
|
||||
id = "fixture.diagnostics"
|
||||
[[plugin]]
|
||||
feature = "osd"
|
||||
id = "fixture.diagnostics.osd"
|
||||
'''
|
||||
|
||||
|
||||
class ArchiveTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.zip = Path(self.tmp.name) / "release.zip"
|
||||
self.files = {"game.toml": '[game]\nid="SLUS-00000"\n',
|
||||
"Game.exe": b"MZfixture", "replay-diagnostics.py": "# helper\n",
|
||||
MANIFEST: TEXT}
|
||||
|
||||
def write(self, prefix=""):
|
||||
with zipfile.ZipFile(self.zip, "w") as archive:
|
||||
for name, content in self.files.items():
|
||||
archive.writestr(prefix + name, content)
|
||||
|
||||
def check(self):
|
||||
self.write()
|
||||
return audit.audit(self.zip, GAME, DIAG)
|
||||
|
||||
def test_valid_flat_and_one_folder_packages(self):
|
||||
self.assertEqual(self.check(), 1)
|
||||
self.write("Release/")
|
||||
self.assertEqual(audit.audit(self.zip, GAME, DIAG), 1)
|
||||
|
||||
def test_valid_setup_sources_and_excluded_framework_hd_catalog(self):
|
||||
self.files[SOURCE] = TEXT
|
||||
self.files["psxrecomp/mods/builtin/packages/psx.enhancement.hd-textures/1.0.0/manifest.toml"] = 'id="psx.enhancement.hd-textures"\n'
|
||||
self.files["psxrecomp/runtime/tests/fixtures/memcards/card1.mcd"] = b"synthetic"
|
||||
self.assertEqual(self.check(), 1)
|
||||
|
||||
def test_cli_and_native_linux_executable(self):
|
||||
del self.files["Game.exe"]
|
||||
self.files["Game"] = b"\x7fELFfixture"
|
||||
self.write()
|
||||
result = subprocess.run([sys.executable, str(SCRIPT), str(self.zip),
|
||||
"--game-id", GAME, "--diagnostics-id", DIAG],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("audit passed", result.stdout)
|
||||
|
||||
def test_python38_syntax_and_tomli_fallback(self):
|
||||
original_import = builtins.__import__
|
||||
|
||||
def import_without_tomllib(name, *args, **kwargs):
|
||||
if name == "tomllib":
|
||||
raise ModuleNotFoundError("simulated Python 3.8")
|
||||
if name == "tomli":
|
||||
return audit.tomllib
|
||||
return original_import(name, *args, **kwargs)
|
||||
|
||||
for script in (SCRIPT, ROOT / "tools/replay_diagnostics.py", ROOT / "tools/check_setup_bios_assets.py"):
|
||||
with self.subTest(script=script.name):
|
||||
ast.parse(script.read_text(encoding="utf-8"), feature_version=8)
|
||||
with mock.patch("builtins.__import__", side_effect=import_without_tomllib):
|
||||
namespace = runpy.run_path(str(script))
|
||||
self.assertIs(namespace["tomllib"], audit.tomllib)
|
||||
|
||||
def test_each_default_and_plugin_is_required(self):
|
||||
for bad in (TEXT.replace("default_enabled = true", "default_enabled = false", 1),
|
||||
TEXT.replace('id = "osd"\ndefault_enabled = true', 'id = "osd"\ndefault_enabled = false'),
|
||||
TEXT.replace('id = "fixture.diagnostics.osd"', 'id = "wrong.osd"'),
|
||||
'channel="developer"\n' + TEXT):
|
||||
with self.subTest(manifest=bad):
|
||||
self.files[MANIFEST] = bad
|
||||
with self.assertRaises(ValueError):
|
||||
self.check()
|
||||
|
||||
def test_source_catalog_defaults_also_required(self):
|
||||
self.files[SOURCE] = TEXT.replace("default_enabled = true", "default_enabled = false")
|
||||
with self.assertRaisesRegex(ValueError, "enabled by default"):
|
||||
self.check()
|
||||
|
||||
def test_missing_helper_executable_or_runtime_catalog(self):
|
||||
for name in ("replay-diagnostics.py", "Game.exe", MANIFEST):
|
||||
with self.subTest(file=name):
|
||||
content = self.files.pop(name)
|
||||
with self.assertRaises(ValueError):
|
||||
self.check()
|
||||
self.files[name] = content
|
||||
|
||||
def test_wrong_game_recipe_and_manifest_target(self):
|
||||
self.files["game.toml"] = '[game]\nid="WRONG"\n'
|
||||
with self.assertRaisesRegex(ValueError, "game ID"):
|
||||
self.check()
|
||||
self.files["game.toml"] = '[game]\nid="SLUS-00000"\n'
|
||||
self.files[MANIFEST] = TEXT.replace(GAME, "WRONG")
|
||||
with self.assertRaisesRegex(ValueError, "target"):
|
||||
self.check()
|
||||
|
||||
def test_hd_package_plugin_and_control_surfaces_fail(self):
|
||||
for name, content in (
|
||||
("mods/bundled/psx.enhancement.hd-textures/1.0.0/manifest.toml", 'id="psx.enhancement.hd-textures"'),
|
||||
(SOURCE, TEXT + '\n[[plugin]]\nid="psx.enhancement.hd-textures"\n'),
|
||||
("mods/preloaded/packages/fixture/1.0.0/options.toml", 'texture_pack_mode="off"\n')):
|
||||
with self.subTest(file=name):
|
||||
self.files[name] = content
|
||||
with self.assertRaisesRegex(ValueError, "HD texture"):
|
||||
self.check()
|
||||
del self.files[name]
|
||||
|
||||
def test_personal_state_save_and_retail_images_fail(self):
|
||||
for name in ("mods/state.toml", "settings.toml", "saves/card1.mcd", "saves/diagnostics/inputs.csv",
|
||||
"mods/installed/private.psxmod", "SCPH1001.BIN", "disc/game.cue",
|
||||
"psxrecomp/bios/SCPH5552.rom", "psxrecomp/runtime/tests/private.mcd"):
|
||||
with self.subTest(file=name):
|
||||
self.files[name] = "private"
|
||||
with self.assertRaises(ValueError):
|
||||
self.check()
|
||||
del self.files[name]
|
||||
|
||||
def test_retail_only_recipe_rejects_openbios(self):
|
||||
self.files["game.toml"] += '[runtime]\nopenbios=false\n'
|
||||
self.assertEqual(self.check(), 1)
|
||||
self.files["psxrecomp/bios/openbios.bin"] = "synthetic"
|
||||
with self.assertRaisesRegex(ValueError, "retail-BIOS-only"):
|
||||
self.check()
|
||||
|
||||
def test_unsafe_and_duplicate_paths_fail(self):
|
||||
self.files["../escape.txt"] = "bad"
|
||||
with self.assertRaisesRegex(ValueError, "unsafe"):
|
||||
self.check()
|
||||
del self.files["../escape.txt"]
|
||||
self.write()
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
with zipfile.ZipFile(self.zip, "a") as archive:
|
||||
archive.writestr(MANIFEST, TEXT)
|
||||
with self.assertRaisesRegex(ValueError, "duplicate"):
|
||||
audit.audit(self.zip, GAME, DIAG)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user