Files
NODEDC_MISSION_CORE/plugins/insta360-x4/packaging/build_deb.py
T
DCCONSTRUCTIONS a3c15e11e9 Add packaged Insta360 X4 integration and recover paired Node channels
Discover independent camera instances and prepare their versioned runtime
from Node or remote Core. Add isolated SDK workers, camera controls, raw
dual-fisheye WebRTC preview, and shared action/region loading states.

Recover existing Node bindings over known Tailscale addresses after a Core
LAN address change. Preserve identities and trust, pin both peers, migrate
endpoints with revision checks, and require real heartbeats for online status.
Fix the Python client certificate profile for Go X509 verification.

Pin Design Guideline 8c53f73 and retain installer/build/acceptance history.
Node 0.8.19 is installed; X4 0.1.3-3 is bundled but hardware activation is pending.

Validation: qualified DG/Node builds and Go race tests; 31 fleet tests;
Python-to-Go certificate interoperability and live tailnet recovery with five
fresh heartbeats; prior 38 X4 tests and bounded remote WebRTC acceptance.
Clean-OS, replug/power autonomy, local X4 video and long-run stability remain open.
2026-09-10 09:21:24 +03:00

231 lines
8.1 KiB
Python

"""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 <noreply@example.invalid>
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)))