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"
|
- "runtime-windows-manifest.json"
|
||||||
- "tools/assemble_windows.py"
|
- "tools/assemble_windows.py"
|
||||||
- "tools/assemble_runtime.py"
|
- "tools/assemble_runtime.py"
|
||||||
|
- "tools/test_runtime_archives.py"
|
||||||
- ".github/workflows/windows.yml"
|
- ".github/workflows/windows.yml"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -71,6 +72,8 @@ jobs:
|
|||||||
$env:SDL_AUDIODRIVER = "dummy"
|
$env:SDL_AUDIODRIVER = "dummy"
|
||||||
& "$package\run.cmd" --profile-root "$env:RUNNER_TEMP\bundled-profile" --smoke-frames 32 --preview-mission base:sector_1_wave_1
|
& "$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" }
|
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
|
- 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
|
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
|
- 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.
|
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
|
# Earlier 5,339-frame checkpoint — retained historical observation
|
||||||
|
|
||||||
# HD continuation checkpoint at 5,339 frames — 17 October 2026
|
# HD continuation checkpoint at 5,339 frames — 17 October 2026
|
||||||
|
|||||||
@@ -28,9 +28,16 @@ def checked_file(root, relative):
|
|||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
def iter_archive_files(package, archive, skipped, excluded_files=()):
|
def iter_archive_files(package, archive, skipped, excluded_files=(), required_files=()):
|
||||||
"""Yield the same sorted archive files without walking excluded subtrees."""
|
"""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):
|
def excluded(relative):
|
||||||
|
if relative in required or relative in required_directories:
|
||||||
|
return False
|
||||||
return (skipped.intersection(relative.parts)
|
return (skipped.intersection(relative.parts)
|
||||||
or any(part.startswith(".runtime-data-") for part in relative.parts)
|
or any(part.startswith(".runtime-data-") for part in relative.parts)
|
||||||
or (len(relative.parts) >= 4 and relative.parts[0] == "mods"
|
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()
|
if (not excluded(relative) and relative not in excluded_files and path.is_file()
|
||||||
and path.resolve() != archive):
|
and path.resolve() != archive):
|
||||||
files.append(path)
|
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)
|
yield from sorted(files)
|
||||||
|
|
||||||
|
|
||||||
@@ -165,7 +175,8 @@ def main():
|
|||||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||||
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git"}
|
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git"}
|
||||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
|
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)
|
relative = path.relative_to(package)
|
||||||
output.write(path, str(Path("RustRaptor") / relative))
|
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))
|
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"}
|
skipped = {"target", "dist", ".reference", ".local", "derived", "__pycache__", ".git", "lib"}
|
||||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as output:
|
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,
|
||||||
excluded_files=(Path("bin/raptor-rust"),)):
|
excluded_files=(Path("bin/raptor-rust"),),
|
||||||
|
required_files=data_manifest["files"]):
|
||||||
relative = path.relative_to(package)
|
relative = path.relative_to(package)
|
||||||
output.write(path, str(Path("RustRaptor") / relative))
|
output.write(path, str(Path("RustRaptor") / relative))
|
||||||
print(json.dumps(report, indent=2))
|
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