"""Compile a legacy export into a runtime package (stages 2 and 5 of the 3D plan). Offline only: this is the tool that bakes the coordinate transform, the unit scale, the UV sampling table and the identity hashes into one file, so the runtime never repeats that work. Stage 5 adds the three release protections the design document asks for, and each is optional: * ``--sign-key``: sign the package with an Ed25519 private key that stays on the build machine. The client carries only the matching public key. * ``--encrypt-key-id`` + ``--key-file``: additionally seal the content with AES-256-GCM. Encryption never replaces signing; it is added to it. * ``--lock-*`` (or ``--lock-from-probe``): record where the package may be displayed. A release compiled for a GPU must not silently render on llvmpipe. Units are an argument, never a default. The legacy mesh is 15.8 x 15.0 x 7.5 source units; whether one of those is a millimetre is a physical claim about the sensor, and ``validate_scene.py`` prints the measured candidates so a person can decide. Examples:: # an engineering package: plain, unsigned, unlocked python tools/scene_assets/compile_scene.py \\ --legacy-dir sensors/finger/assets/30fps-0703 \\ --map sensors/finger/assets/30fps-0703/unroll_map_uv_s0.4.npz \\ --meters-per-source-unit 0.001 \\ --output build/finger-display-1.0.0.oryscene # a release package: signed, sealed, locked to the machine we probed python tools/scene_assets/compile_scene.py \\ --legacy-dir sensors/finger/assets/30fps-0703 \\ --map sensors/finger/assets/30fps-0703/unroll_map_uv_s0.4.npz \\ --meters-per-source-unit 0.001 \\ --sign-key keys/build-ed25519.pem --sign-key-id build-2024 \\ --encrypt-key-id finger-display --key-file keys/finger-display.key \\ --lock-from-probe docs/architecture/gl-report-windows-dev.json \\ --output build/finger-display-1.0.0.oryscene """ from __future__ import annotations import argparse import json import sys from pathlib import Path def _bootstrap_paths() -> None: here = Path(__file__).resolve() repo = here.parents[2] for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): if path.is_dir() and str(path) not in sys.path: sys.path.insert(0, str(path)) def _platform_lock_from_args(args): """The lock the command line describes, from a probe report or from flags.""" from orisys_app_shared.scene3d.contracts import PlatformLock if args.lock_from_probe is not None: report = json.loads(Path(args.lock_from_probe).read_text(encoding="utf-8")) context = report.get("context") or {} host = report.get("host") or {} os_family = host.get("os_family") or host.get("system") or "" machine = host.get("machine") or "" return PlatformLock( os_family=(str(os_family).lower(),) if os_family else (), machine=(str(machine).lower(),) if machine else (), gl_profile=( str(context.get("profile_used")) if context.get("profile_used") else None ), allowed_vendors=(str(context["vendor"]),) if context.get("vendor") else (), allowed_renderers=(str(context["renderer"]),) if context.get("renderer") else (), forbid_software_renderer=not args.allow_software_renderer, min_texture_size=int(context.get("max_texture_size") or 0), note=str(args.lock_note or f"qualified from {Path(args.lock_from_probe).name}"), ) lock = PlatformLock( os_family=tuple(v.lower() for v in (args.lock_os or ())), machine=tuple(v.lower() for v in (args.lock_machine or ())), gl_profile=args.lock_gl_profile, allowed_vendors=tuple(args.lock_vendor or ()), allowed_renderers=tuple(args.lock_renderer or ()), forbid_software_renderer=not args.allow_software_renderer, min_texture_size=int(args.lock_min_texture or 0), note=str(args.lock_note or ""), ) return None if lock.is_unrestricted() and not args.lock_note else lock def _sealing_from_args(args): """The key material the command line points at, or ``None`` for a plain package.""" from orisys_app_shared.scene3d.contracts import Sealing from orisys_app_shared.scene3d import protect signing_pem = None if args.sign_key is not None: path = Path(args.sign_key) if not path.is_file(): raise SystemExit(f"the signing key is missing: {path}") signing_pem = path.read_bytes() # Prove the key is usable before spending a compile on it. protect.public_key_pem(signing_pem) provider = None if args.encrypt_key_id: if args.key_file is None: raise SystemExit("--encrypt-key-id also needs --key-file") provider = protect.load_key_file(Path(args.key_file), key_id=args.encrypt_key_id) if signing_pem is None and provider is None: return None verify = protect.public_key_pem(signing_pem) if signing_pem is not None else None if verify is not None and args.write_verify_key is not None: # The client's half of the pair. Publishing it is part of the release, and # writing it here keeps it from being re-derived differently somewhere else. target = Path(args.write_verify_key) target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(verify) return Sealing( key_id=str(args.encrypt_key_id or ""), provider=provider, signing_key_pem=signing_pem, signing_key_reference=str(args.sign_key_id or "build-key"), verify_key_pem=verify, ) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--legacy-dir", type=Path, required=True, help="legacy NPZ export") parser.add_argument("--map", type=Path, required=True, help="field map (npz)") parser.add_argument( "--meters-per-source-unit", type=float, required=True, help="stated unit scale (the importer never guesses it)", ) parser.add_argument("--output", type=Path, required=True, help="package to write") parser.add_argument("--asset-id", default="finger-display") parser.add_argument("--version", default="1.0.0") parser.add_argument("--lod-id", default="lod0") parser.add_argument("--sensor-model", default="finger") parser.add_argument( "--transform", type=Path, default=None, help="optional 4x4 rigid transform (npy) from source to sensor coordinates", ) parser.add_argument( "--housing", type=Path, default=None, help=( "optional housing/frame npz (vertices, faces) in the same source frame and " "units as the export's mesh.npz; omitted, /housing.npz is used when " "present. A housing exported in a different frame must be aligned first with " "tools/scene_assets/align_housing.py" ), ) protection = parser.add_argument_group( "release protection (stage 5)", "all optional; a plain package is the default" ) protection.add_argument("--sign-key", type=Path, default=None, help="Ed25519 private key (PEM)") protection.add_argument("--sign-key-id", default="", help="name recorded in the envelope") protection.add_argument( "--write-verify-key", type=Path, default=None, help="write the public half of --sign-key here (this is what the client ships)", ) protection.add_argument("--encrypt-key-id", default="", help="content key id to seal with") protection.add_argument("--key-file", type=Path, default=None, help="key for --encrypt-key-id") protection.add_argument( "--lock-from-probe", type=Path, default=None, help="take the platform lock from a probe_gl.py report", ) protection.add_argument("--lock-os", action="append", default=None, help="repeatable") protection.add_argument("--lock-machine", action="append", default=None, help="repeatable") protection.add_argument("--lock-gl-profile", default=None, choices=(None, "desktop33", "gles30")) protection.add_argument("--lock-vendor", action="append", default=None, help="repeatable") protection.add_argument("--lock-renderer", action="append", default=None, help="repeatable") protection.add_argument("--lock-min-texture", type=int, default=0) protection.add_argument("--lock-note", default="") protection.add_argument( "--allow-software-renderer", action="store_true", help="do not forbid a software rasteriser (the design document says do)", ) parser.add_argument("--json", action="store_true") args = parser.parse_args(argv) _bootstrap_paths() import numpy as np from orisys_app_shared.scene3d.contracts import ThreeDError from orisys_app_shared.scene3d.legacy import LegacyFingerImporter from orisys_app_shared.scene3d.package import describe_envelope, write_package transform = ( np.load(args.transform, allow_pickle=False) if args.transform is not None else np.eye(4) ) try: sealing = _sealing_from_args(args) lock = _platform_lock_from_args(args) except ThreeDError as exc: print(f"rejected: {exc.diagnostic.code}: {exc.diagnostic.message}", file=sys.stderr) return 2 try: assets = LegacyFingerImporter().convert( args.legacy_dir, args.map, transform_sensor_from_source=transform, meters_per_source_unit=args.meters_per_source_unit, asset_id=args.asset_id, asset_version=args.version, lod_id=args.lod_id, housing_path=args.housing, ) if lock is not None: from dataclasses import replace # Part of the asset, so it travels into the manifest and the signature. assets = replace(assets, platform_lock=lock) report = write_package( assets, args.output, sensor_model=args.sensor_model, hardware_revisions=("*",), sealing=sealing, ) except ThreeDError as exc: print(f"rejected: {exc.diagnostic.code}: {exc.diagnostic.message}", file=sys.stderr) return 2 envelope = describe_envelope(report.output) summary = { "status": "ok", "output": str(report.output), "asset_hash": report.asset_hash, "vertices": report.part_vertex_counts, "triangles": report.part_triangle_counts, "geometry_id": assets.geometry_id, "field_domain_id": assets.field_domain_id, "source_geometry_id": assets.source_geometry_id, "envelope": envelope, "platform_lock": ( assets.platform_lock.to_dict() if assets.platform_lock is not None else None ), } if args.json: print(json.dumps(summary, indent=2, ensure_ascii=False)) else: print( f"wrote {report.output} vertices={report.part_vertex_counts} " f"triangles={report.part_triangle_counts}" ) print(f" geometry_id={assets.geometry_id}") print( " source_geometry_id=" + (assets.source_geometry_id or "(none: no world_xyz in the map)") ) print(f" asset_hash={report.asset_hash}") print( f" envelope=v{envelope['envelope_version']} {envelope['payload']}" f"{' signed(' + str(envelope['signing_key']) + ')' if envelope['signed'] else ' unsigned'}" + (f" sealed for key {envelope['key_id']}" if envelope["sealed"] else "") ) if summary["platform_lock"]: print(f" platform_lock={json.dumps(summary['platform_lock'], ensure_ascii=False)}") return 0 if __name__ == "__main__": sys.exit(main())