239 lines
8.3 KiB
Python
239 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-4"
|
|
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",
|
|
):
|
|
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)))
|