"""Build the optional X4 package from pinned SDK, native output and wheel bytes.""" import argparse import hashlib import io import json import os import sys import zipfile from pathlib import Path, PurePosixPath ROOT = Path(__file__).resolve().parents[1] REPOSITORY = ROOT.parents[1] PACKAGING = ROOT / "packaging" sys.path.insert(0, str(REPOSITORY / "scripts/packaging")) from debian import package # noqa: E402 from fetch_sdk import verify # noqa: E402 VERSION = "0.1.3-3" WHEELS = { "aiohappyeyeballs", "aiohttp", "aioice", "aiortc", "aiosignal", "annotated_types", "attrs", "av", "cffi", "cryptography", "dnspython", "frozenlist", "google_crc32c", "idna", "ifaddr", "multidict", "propcache", "pycparser", "pydantic", "pydantic_core", "pyee", "pylibsrtp", "pyopenssl", "typing_extensions", "typing_inspection", "yarl", } def digest(data): return hashlib.sha256(data).hexdigest() def archive_bytes(files): stream = io.BytesIO() with zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED) as archive: for name, data in sorted(files.items()): info = zipfile.ZipInfo(name, date_time=(2026, 9, 8, 0, 0, 0)) info.external_attr = 0o644 << 16 info.compress_type = zipfile.ZIP_DEFLATED archive.writestr(info, data) return stream.getvalue() def runtime_payload(): sdk = ROOT / "build/sdk" lock = json.loads((PACKAGING / "sdk-lock.json").read_text()) verify(sdk, lock) native = ROOT / "build/native" provenance = json.loads((native / "provenance.json").read_text()) for name, expected in provenance["source"].items(): if digest((ROOT / "native" / name).read_bytes()) != expected: raise ValueError("Native adapter sources changed since compilation") if provenance["sdk_lock_sha256"] != digest((PACKAGING / "sdk-lock.json").read_bytes()): raise ValueError("Native adapter SDK input changed") binary = (native / "libmissioncore_x4.so").read_bytes() if digest(binary) != provenance["binary"]["sha256"] or provenance["abi"] != 1: raise ValueError("Native adapter provenance mismatch") files = {"lib/libmissioncore_x4.so": binary} for item in lock["files"]: if item["path"].startswith(("bin/", "lib/")): files[item["path"]] = (sdk / item["path"]).read_bytes() wheels = json.loads((PACKAGING / "python-lock.json").read_text())["wheels"] for item in wheels: source = REPOSITORY / "apps/node-agent/build/realsense-wheels" / item["name"] data = source.read_bytes() if source.is_symlink() or len(data) != item["bytes"] or digest(data) != item["sha256"]: raise ValueError("Pinned Python wheel mismatch") with zipfile.ZipFile(io.BytesIO(data)) as archive: for entry in archive.infolist(): if entry.is_dir(): continue name = entry.filename path = PurePosixPath(name) if ( path.is_absolute() or ".." in path.parts or path.as_posix() != name or "\\" in name or ".data/" in name or name.endswith(".pth") or (entry.external_attr >> 16) & 0o170000 == 0o120000 ): raise ValueError("Unsupported Python wheel member") target = "python/" + name content = archive.read(entry) if target in files and files[target] != content: raise ValueError("Python runtime files collide") files[target] = content sdk_python = REPOSITORY / "packages/plugin-sdk/python/missioncore_plugin_sdk" for path in sdk_python.rglob("*.py"): files["python/missioncore_plugin_sdk/" + path.relative_to(sdk_python).as_posix()] = ( path.read_bytes() ) payload = archive_bytes(files) result = { "schema": "missioncore.insta360.runtime-bundle/v1", "version": VERSION, "platform": "ubuntu-24.04-amd64", "python": "3.12", "sdk_lock_sha256": digest((PACKAGING / "sdk-lock.json").read_bytes()), "python_lock_sha256": digest((PACKAGING / "python-lock.json").read_bytes()), "native": provenance, "runtime_source_sha256": { path.name: digest(path.read_bytes()) for path in sorted((ROOT / "runtime").glob("*.py")) }, "payload_sha256": digest(payload), "files": { name: {"bytes": len(data), "sha256": digest(data)} for name, data in sorted(files.items()) }, } result["revision"] = digest(json.dumps(result, sort_keys=True).encode())[:24] return payload, result def build(output): payload, bundle = runtime_payload() files = [ ("usr/share/mission-core-node/insta360/payload.zip", payload, 0o644), ( "usr/share/mission-core-node/insta360/bundle.json", (json.dumps(bundle, indent=2) + "\n").encode(), 0o644, ), ] for name in ("sdk-lock.json", "python-lock.json"): files.append( ( "usr/share/doc/mission-core-insta360-x4/" + name, (PACKAGING / name).read_bytes(), 0o644, ) ) for name in ("layout.py", "bootstrap.py", "supervisor.py", "prepare.py"): files.append( ("usr/lib/mission-core-node/insta360/" + name, (PACKAGING / name).read_bytes(), 0o644) ) for path in sorted((ROOT / "runtime").glob("*.py")): files.append( ("usr/lib/mission-core-node/insta360/runtime/" + path.name, path.read_bytes(), 0o644) ) for path in sorted(PACKAGING.glob("*.service")): files.append(("usr/lib/systemd/system/" + path.name, path.read_bytes(), 0o644)) files.extend( [ ( "usr/lib/udev/rules.d/70-mission-core-insta360.rules", (PACKAGING / "70-mission-core-insta360.rules").read_bytes(), 0o644, ), ( "usr/share/polkit-1/rules.d/50-mission-core-insta360.rules", (PACKAGING / "50-mission-core-insta360.rules").read_bytes(), 0o644, ), ] ) provenance = { "package": "mission-core-insta360-x4", "version": VERSION, "revision": bundle["revision"], "files": {name: digest(data) for name, data, _ in files}, "hardware_qualified": False, "clean_image_qualified": False, } files.append( ( "usr/share/doc/mission-core-insta360-x4/provenance.json", (json.dumps(provenance, indent=2) + "\n").encode(), 0o644, ) ) control = f"""Package: mission-core-insta360-x4 Version: {VERSION} Architecture: amd64 Maintainer: NODE.DC local build Section: admin Priority: optional Depends: mission-core-node (>= 0.8.16), mission-core-node (<< 0.9.0), systemd (>= 255), python3 (>= 3.12), python3 (<< 3.13), adduser, udev, polkitd, libc6 (>= 2.35), libstdc++6 (>= 12), libgcc-s1, zlib1g Description: Optional Insta360 X4 control and operator camera integration Private USB instances and pinned offline runtime for Ubuntu 24.04 amd64. """.encode() controls = [("control", control, 0o644)] + [ (name, (PACKAGING / name).read_bytes(), 0o755) for name in ("preinst", "postinst", "prerm", "postrm") ] data = package(controls, files) output.parent.mkdir(parents=True, exist_ok=True) descriptor = os.open(output, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) with os.fdopen(descriptor, "wb") as stream: stream.write(data) stream.flush() os.fsync(stream.fileno()) return { "file": str(output), "bytes": len(data), "sha256": digest(data), "revision": bundle["revision"], "version": VERSION, } if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() print(json.dumps(build(args.output)))