Files
NODEDC_MISSION_CORE/plugins/insta360-x4/packaging/build_deb.py
T

240 lines
8.3 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-6"
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",
"ble_diagnostic.py",
"ble_options.py",
"ble_wake.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,
bluez (>= 5.72), python3-dbus, python3-gi
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)))