Keep manifest-required derived artwork in portable game archives
This commit is contained in:
1 parent
00304e25c1
commit
6b1a2629a0
5 files changed
+103
-4
No files matched your search
@@ -18,6 +18,7 @@ on:
|
||||
- "runtime-windows-manifest.json"
|
||||
- "tools/assemble_windows.py"
|
||||
- "tools/assemble_runtime.py"
|
||||
- "tools/test_runtime_archives.py"
|
||||
- ".github/workflows/windows.yml"
|
||||
|
||||
permissions:
|
||||
@@ -71,6 +72,8 @@ jobs:
|
||||
$env:SDL_AUDIODRIVER = "dummy"
|
||||
& "$package\run.cmd" --profile-root "$env:RUNNER_TEMP\bundled-profile" --smoke-frames 32 --preview-mission base:sector_1_wave_1
|
||||
if ($LASTEXITCODE -ne 0) { throw "Included Windows SDL gameplay failed" }
|
||||
- name: Test runtime archive payload selection
|
||||
run: python -m unittest discover -s tools -p test_runtime_archives.py
|
||||
- name: Build and package Windows desktop
|
||||
run: python tools/assemble_windows.py --target x86_64-pc-windows-msvc --sdl2-dir "$env:RUNNER_TEMP\sdl2\SDL2-2.32.10" --zip dist/RustRaptor-windows-x86_64.zip
|
||||
- name: Check displayed boss phases and SD HD shop artwork
|
||||
|
||||
@@ -14,6 +14,8 @@ SourceF, both binaries, Mission 2 version 1.0.3 and the nine palette opt-ins rem
|
||||
|
||||
The assembled data contains **9,504 files / 8,407,923,443 bytes**. The 307 native AGX cinematic/font records remain preserved separately. Iron Coast retains its published retune, Mission 2 version 1.0.3 and 65-test campaign evidence; see [Iron Coast retune evidence](iron-coast-retune.md). This artwork append changes no mission, weapon or shop behavior.
|
||||
|
||||
Windows package follow-up: the exact H5500 run at `00304e25` passed 401 tests (26 ignored, zero failed), then rejected the 15 new picture routes because the ZIP filter skipped directories named `derived`. Mod, PCM and Stage II checks after that HD guard did not run. Both ZIP builders now require every runtime-manifest data file to survive archive selection and fail if any required route is missing. A new synthetic ZIP/relocation check covers the failure; a fresh native Windows result remains pending. [Failed native run](https://github.com/Quintine/RustRaptor/actions/runs/39083332442).
|
||||
|
||||
# Earlier 5,339-frame checkpoint — retained historical observation
|
||||
|
||||
# HD continuation checkpoint at 5,339 frames — 17 October 2026
|
||||
|
||||
@@ -28,9 +28,16 @@ def checked_file(root, relative):
|
||||
return resolved
|
||||
|
||||
|
||||
def iter_archive_files(package, archive, skipped, excluded_files=()):
|
||||
"""Yield the same sorted archive files without walking excluded subtrees."""
|
||||
def iter_archive_files(package, archive, skipped, excluded_files=(), required_files=()):
|
||||
"""Keep manifest payloads even when a parent has a build-cache name."""
|
||||
required = {Path(name) for name in required_files}
|
||||
required_directories = {parent for path in required for parent in path.parents}
|
||||
for relative in required:
|
||||
checked_file(package, relative)
|
||||
|
||||
def excluded(relative):
|
||||
if relative in required or relative in required_directories:
|
||||
return False
|
||||
return (skipped.intersection(relative.parts)
|
||||
or any(part.startswith(".runtime-data-") for part in relative.parts)
|
||||
or (len(relative.parts) >= 4 and relative.parts[0] == "mods"
|
||||
@@ -47,6 +54,9 @@ def iter_archive_files(package, archive, skipped, excluded_files=()):
|
||||
if (not excluded(relative) and relative not in excluded_files and path.is_file()
|
||||
and path.resolve() != archive):
|
||||
files.append(path)
|
||||
missing = required - {path.relative_to(package) for path in files}
|
||||
if missing:
|
||||
raise ValueError(f"runtime archive is missing required files: {sorted(map(str, missing))}")
|
||||
yield from sorted(files)
|
||||
|
||||
|
||||
@@ -165,7 +175,8 @@ def main():
|
||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git"}
|
||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
|
||||
for path in iter_archive_files(package, archive, skipped):
|
||||
for path in iter_archive_files(package, archive, skipped,
|
||||
required_files=report["files"]):
|
||||
relative = path.relative_to(package)
|
||||
output.write(path, str(Path("RustRaptor") / relative))
|
||||
print(json.dumps({key: value for key, value in report.items() if key not in {"files", "system_dependencies"}}, indent=2))
|
||||
|
||||
@@ -174,7 +174,8 @@ def main():
|
||||
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git", "lib"}
|
||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
|
||||
for path in iter_archive_files(package, archive, skipped,
|
||||
excluded_files=(Path("bin/raptor-rust"),)):
|
||||
excluded_files=(Path("bin/raptor-rust"),),
|
||||
required_files=data_manifest["files"]):
|
||||
relative = path.relative_to(package)
|
||||
output.write(path, str(Path("RustRaptor") / relative))
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Small archive fixtures; no game build or production data copy required."""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
from assemble_runtime import iter_archive_files
|
||||
|
||||
|
||||
class RuntimeArchiveTests(unittest.TestCase):
|
||||
def test_manifest_derived_payload_survives_cache_filter_and_relocation(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
package = Path(directory) / "package"
|
||||
payload = "data/hd/jobs/reviewed-family/derived/200a2-derived.png"
|
||||
required = ["data/INDEX", "data/hd/hd.json", payload]
|
||||
fixtures = {
|
||||
"data/INDEX": b"catalog",
|
||||
"data/hd/hd.json": b"manifest",
|
||||
payload: b"accepted derived PNG bytes",
|
||||
"data/hd/jobs/reviewed-family/derived/rejected-preview.png": b"not admitted",
|
||||
"derived/preview.png": b"private cache",
|
||||
"target/build-cache": b"build",
|
||||
".reference/private": b"private",
|
||||
"mods/demo/art/source/parent.png": b"authoring reference",
|
||||
"mods/demo/art/runtime.png": b"mod runtime",
|
||||
"bin/raptor-rust": b"Linux only",
|
||||
"bin/raptor-rust.exe": b"Windows",
|
||||
}
|
||||
for name, data in fixtures.items():
|
||||
path = package / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
archive = Path(directory) / "windows.zip"
|
||||
skipped = {"target", "dist", ".reference", "derived", "lib"}
|
||||
selected = list(iter_archive_files(
|
||||
package, archive, skipped, excluded_files=(Path("bin/raptor-rust"),),
|
||||
required_files=required))
|
||||
names = {path.relative_to(package).as_posix() for path in selected}
|
||||
self.assertTrue(set(required) <= names)
|
||||
self.assertEqual(names - set(required), {"mods/demo/art/runtime.png", "bin/raptor-rust.exe"})
|
||||
with zipfile.ZipFile(archive, "w") as output:
|
||||
for path in selected:
|
||||
output.write(path, "RustRaptor/" + path.relative_to(package).as_posix())
|
||||
relocated = Path(directory) / "Portable Windows Ω folder"
|
||||
with zipfile.ZipFile(archive) as packed:
|
||||
packed.extractall(relocated)
|
||||
for name in required:
|
||||
original = package / name
|
||||
copied = relocated / "RustRaptor" / name
|
||||
self.assertEqual(hashlib.sha256(copied.read_bytes()).digest(),
|
||||
hashlib.sha256(original.read_bytes()).digest())
|
||||
|
||||
def test_missing_manifest_payload_fails_before_yielding_files(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
package = Path(directory)
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
next(iter_archive_files(package, package / "out.zip", {"derived"},
|
||||
required_files=["data/hd/missing.png"]))
|
||||
|
||||
def test_explicit_exclusion_cannot_silently_drop_manifest_payload(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
package = Path(directory)
|
||||
path = package / "data/required"
|
||||
path.parent.mkdir()
|
||||
path.write_bytes(b"required")
|
||||
with self.assertRaisesRegex(ValueError, "missing required files"):
|
||||
list(iter_archive_files(package, package / "out.zip", set(),
|
||||
excluded_files=(Path("data/required"),),
|
||||
required_files=["data/required"]))
|
||||
|
||||
def test_manifest_path_cannot_escape_package(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
package = Path(directory)
|
||||
for name in ("../outside", str(package / "absolute")):
|
||||
with self.subTest(name=name), self.assertRaises(ValueError):
|
||||
list(iter_archive_files(package, package / "out.zip", set(),
|
||||
required_files=[name]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user