Files
sanctuary-beta/scripts/archive_terrain_atlas.py
2026-09-12 20:29:32 +02:00

96 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Freeze a rendered native atlas with raw measurements, source revision and build hash."""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[1]
def sha(data):
return hashlib.sha256(data).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--raw", type=Path, required=True)
parser.add_argument("--rendered", type=Path, required=True)
parser.add_argument("--jar", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--extra", type=Path, action="append", default=[])
args = parser.parse_args()
if args.output.exists():
raise SystemExit("Refusing to overwrite an existing immutable atlas")
status = subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True)
if status.strip():
raise SystemExit("Commit atlas code and documentation before freezing the archive")
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
jar_bytes = args.jar.read_bytes()
with zipfile.ZipFile(args.jar) as jar:
if jar.testzip() is not None:
raise SystemExit("Invalid built JAR")
version = json.loads(jar.read("fabric.mod.json"))["version"]
files = {}
cases = []
for source in sorted(args.raw.glob("terrain-*.json")):
raw = source.read_bytes()
data = json.loads(raw)
if data["version"] != version:
raise SystemExit(f"Stale native export: {source}")
rendered = args.rendered / source.stem
provenance = json.loads((rendered / "provenance.json").read_text())
if provenance["source_sha256"] != sha(raw):
raise SystemExit(f"Rendered atlas differs from source: {source}")
if provenance["script_sha256"] != sha((ROOT / "scripts/terrain_atlas.py").read_bytes()):
raise SystemExit("Rendering script changed after rendering")
files["raw/" + source.name] = raw
for name in ("maps.png", "maps.pdf", "sections.png", "sections.pdf", "summary.json", "provenance.json"):
files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
for name in ("upper-terrain.png", "upper-terrain.pdf"):
if (rendered / name).is_file(): files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
cases.append({"seed": data["seed"], "diameter": data["diameter"], "directory": source.stem})
identities = {(c["seed"], c["diameter"]) for c in cases}
if not cases or len(identities) != len(cases):
raise SystemExit("Empty atlas or duplicate seed/size")
for name in ("scripts/terrain_atlas.py", "scripts/terrain-atlas-requirements.txt",
"scripts/archive_terrain_atlas.py", "docs/terrain-atlas.md",
"scripts/structure_atlas.py", "scripts/finishing_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
files[name] = (ROOT / name).read_bytes()
for path in args.extra:
key = "structures/" + path.name
if key in files: raise SystemExit("Duplicate extra atlas file")
files[key] = path.read_bytes()
rows = ["# Sanctuary — atlas " + version, "", f"Source : `{commit}`.", "",
"Mesures natives avant décoration ; voir docs/terrain-atlas.md pour les échelles et limites.", "",
"| Diamètre | Graine | Cartes | Coupes |", "|---|---|---|---|"]
for c in cases:
base = "figures/" + c["directory"]
rows.append(f"| {c['diameter']} | {c['seed']} | [PNG]({base}/maps.png) / [PDF]({base}/maps.pdf) | [PNG]({base}/sections.png) / [PDF]({base}/sections.pdf) |")
rows += ["", "Reproduction des figures dans un environnement Python isolé :", "", "```sh",
"python -m pip install -r scripts/terrain-atlas-requirements.txt",
"python scripts/terrain_atlas.py --output rendered raw/terrain-*.json", "```", ""]
files["README.md"] = "\n".join(rows).encode()
files["manifest.json"] = (json.dumps({"version": version, "source_commit": commit,
"jar": args.jar.name, "jar_sha256": sha(jar_bytes), "cases": cases,
"files_sha256": {name: sha(content) for name, content in sorted(files.items())}}, indent=2) + "\n").encode()
args.output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(args.output, "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, content in sorted(files.items()):
info = zipfile.ZipInfo(name, date_time=(2000, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, content)
with zipfile.ZipFile(args.output) as archive:
assert archive.testzip() is None
for name, content in files.items():
assert archive.read(name) == content
print(json.dumps({"path": str(args.output), "cases": len(cases), "source_commit": commit,
"sha256": sha(args.output.read_bytes()), "bytes": args.output.stat().st_size}))
if __name__ == "__main__":
main()