#!/usr/bin/env python3 """Generate the fixed, hash-verified built-in render lookup; offline authoring only. Source pixels, manifests and readiness flags are never rewritten. """ from __future__ import annotations import argparse import hashlib import json from pathlib import Path ROOT = Path(__file__).resolve().parents[2] OUTPUT = ROOT / "crates/engine/src/cmd/render/library_data.rs" CATALOGS = { "materials": "docs/render-materials/materials.json", "lighting": "docs/render-materials/lighting.json", "environments": "docs/render-materials/environments.json", "surfaces": "assets/render/starter/materials/source/recipes.json", "surface_manifest": "assets/render/starter/materials/manifest.json", "selene_appearances": "assets/render/selene/materials/appearance-pack.json", "selene_shots": "assets/render/selene/scenes/shots.json", "hdr": "assets/render/starter/environments/manifest.json", "rigs": "assets/render/starter/lighting/exports/rigs.json", "scenes": "docs/render-materials/scene-presets.json", "quality": "docs/render-materials/quality-presets.json", "capabilities": "docs/render-materials/capabilities.json", } ROLES = ("base_color", "normal_open_gl", "roughness", "metallic") def load(relative: str) -> dict: value = json.loads((ROOT / relative).read_text(encoding="utf-8")) if not isinstance(value, dict) or not isinstance(value.get("items"), list): raise ValueError(f"Expected catalog with items: {relative}") return value def checked_file(relative: str, expected: str) -> str: path = (ROOT / relative).resolve() path.relative_to(ROOT.resolve()) if not path.is_file() or path.stat().st_size > 16 * 1024 * 1024: raise ValueError(f"Missing or excessive asset: {relative}") actual = hashlib.sha256(path.read_bytes()).hexdigest() if actual != expected: raise ValueError(f"SHA-256 mismatch: {relative}: {actual}") return path.relative_to(ROOT).as_posix() def runtime_catalog_path(relative: str) -> str: if relative.startswith("docs/render-materials/"): return "assets/render/catalog/" + Path(relative).name return relative def catalog_exports(check: bool) -> None: for relative in CATALOGS.values(): destination = runtime_catalog_path(relative) if destination == relative: continue data = (ROOT / relative).read_bytes() target = ROOT / destination if check: if not target.is_file() or target.read_bytes() != data: raise ValueError(f"Stale runtime catalog export: {destination}") else: target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(data) def generated() -> tuple[str, dict]: assets: dict[str, tuple[str, str]] = {} for item in load(CATALOGS["surface_manifest"])["items"]: for role in ROLES: spec = item["maps"][role] if spec["bit_depth"] != 8: raise ValueError(f"Not an eight-bit runtime channel: {item['id']}/{role}") key = f"{item['id']}/{role}" if key in assets: raise ValueError(f"Duplicate runtime resource: {key}") assets[key] = (checked_file(spec["path"], spec["sha256"]), spec["sha256"]) maps = len(assets) for item in load(CATALOGS["hdr"])["items"]: relative = "assets/render/starter/environments/" + item["path"] if item["id"] in assets: raise ValueError(f"Duplicate HDR resource: {item['id']}") assets[item["id"]] = (checked_file(relative, item["sha256"]), item["sha256"]) q = json.dumps text = [ "//! Generated by tools/render-library/generate.py; do not edit by hand.", "//! Original source bytes and manifest digests remain unchanged.", "", "#[rustfmt::skip]", "pub(super) fn catalog(kind: &str) -> Option<&'static str> {", " match kind {", ] for kind, relative in sorted(CATALOGS.items()): load(relative) relative = runtime_catalog_path(relative) text.append(f' {q(kind)} => Some(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../{relative}"))),') text += [" _ => None,", " }", "}", "", "#[cfg(feature = \"bundled-render-library\")]", "#[rustfmt::skip]", "pub(super) fn asset(key: &str) -> Option<(&'static [u8], &'static str)> {", " match key {"] for key, (relative, digest) in sorted(assets.items()): text.append(f' {q(key)} => Some((include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../{relative}")), {q(digest)})),') text += [" _ => None,", " }", "}", "", "#[cfg(not(feature = \"bundled-render-library\"))]", "#[rustfmt::skip]", "pub(super) fn asset(_key: &str) -> Option<(&'static [u8], &'static str)> { None }", ""] return "\n".join(text), {"embedded_maps": maps, "embedded_hdr": len(assets)-maps, "assets_checked": len(assets), "catalogs": len(CATALOGS), "source_readiness_changed": False} def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true") args = parser.parse_args() catalog_exports(args.check) text, summary = generated() if args.check: if not OUTPUT.is_file() or OUTPUT.read_text(encoding="utf-8") != text: raise SystemExit("Generated lookup is stale; run tools/render-library/generate.py") else: OUTPUT.parent.mkdir(parents=True, exist_ok=True) OUTPUT.write_text(text, encoding="utf-8", newline="\n") print(json.dumps(summary | {"status": "PASS", "mode": "check" if args.check else "generate"})) if __name__ == "__main__": main()