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.
128 lines
7.9 KiB
Python
128 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Build a deterministic Debian package on macOS/Linux from reviewed artifacts.
|
|
|
|
No install operation, sudo, container, package-manager mutation or network I/O.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BINARY_VERSION = "0.8.19"
|
|
VERSION = "0.8.19"
|
|
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
|
|
from debian import package
|
|
|
|
BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af"
|
|
|
|
|
|
def desktop_icon(brand):
|
|
"""Give desktop loaders a square canvas without distorting the brand mark.
|
|
|
|
The canonical SVG remains an unchanged nested document. Its default
|
|
xMidYMid meet preserves the mark's aspect ratio inside this square viewport.
|
|
Explicit intrinsic dimensions also keep GTK's pixbuf square.
|
|
"""
|
|
if hashlib.sha256(brand).hexdigest() != BRAND_SHA256:
|
|
raise ValueError("Brand mark differs from the admitted Design Guideline asset")
|
|
return (b'<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" '
|
|
b'viewBox="0 0 256 256" preserveAspectRatio="xMidYMid meet">\n'
|
|
+ brand + b'</svg>\n')
|
|
|
|
|
|
def build(binary, destination):
|
|
payload = binary.read_bytes()
|
|
if payload[:4] != b"\x7fELF" or payload[4:6] != b"\x02\x01" or payload[18:20] != b"\x3e\x00":
|
|
raise ValueError("Expected a Linux amd64 ELF binary")
|
|
p = ROOT / "packaging"
|
|
control = f"""Package: mission-core-node
|
|
Version: {VERSION}
|
|
Architecture: amd64
|
|
Maintainer: NODE.DC local build <noreply@example.invalid>
|
|
Section: admin
|
|
Priority: optional
|
|
Depends: adduser, systemd (>= 255), python3 (>= 3.12), python3 (<< 3.13), python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, network-manager, iproute2, python3-psycopg2, postgresql-16 (>= 16.15), timescaledb-2-oss-postgresql-16 (= 2.29.2~ubuntu24.04-1615), udev, libc6 (>= 2.35), libstdc++6 (>= 12), libgcc-s1, zlib1g
|
|
Description: Mission Core onboard computer configuration
|
|
Local graphical setup, host inventory, SSH access and persistent node identity.
|
|
""".encode()
|
|
controls = [("control", control, 0o644)]
|
|
controls += [(name, (p / name).read_bytes(), 0o755) for name in ["preinst", "postinst", "prerm", "postrm"]]
|
|
files = [("usr/lib/mission-core-node/node-agent", payload, 0o755)]
|
|
brand = (ROOT.parents[2] / "NODEDC_DESIGN_GUIDELINE/apps/catalog/public/nodedc-mark.svg").read_bytes()
|
|
files.append(("usr/share/icons/hicolor/scalable/apps/org.nodedc.MissionCoreNode.svg", desktop_icon(brand), 0o644))
|
|
for source, path, mode in [
|
|
("launcher.py", "usr/bin/mission-core-node", 0o755),
|
|
("authorize", "usr/lib/mission-core-node/authorize", 0o755),
|
|
("mission-core-node.desktop", "usr/share/applications/org.nodedc.MissionCoreNode.desktop", 0o644),
|
|
("mission-core-node.service", "usr/lib/systemd/system/mission-core-node.service", 0o644),
|
|
("org.nodedc.mission-core-node.policy", "usr/share/polkit-1/actions/org.nodedc.mission-core-node.policy", 0o644),
|
|
("60-mission-core-node.conf", "usr/share/mission-core-node/60-mission-core-node.conf", 0o644),
|
|
("network_helper.py", "usr/lib/mission-core-node/network_helper.py", 0o644),
|
|
("install-tailscale", "usr/lib/mission-core-node/install-tailscale", 0o755),
|
|
("connect-tailscale", "usr/lib/mission-core-node/connect-tailscale", 0o755),
|
|
("tailscale-release.json", "usr/share/mission-core-node/tailscale-release.json", 0o644),
|
|
("configure-system", "usr/lib/mission-core-node/configure-system", 0o755),
|
|
("environment_helper.py", "usr/lib/mission-core-node/environment_helper.py", 0o644),
|
|
("mission-core-node-environment.service", "usr/lib/systemd/system/mission-core-node-environment.service", 0o644),
|
|
("60-environment.conf", "usr/share/mission-core-node/60-environment.conf", 0o644),
|
|
("setup-monitor", "usr/lib/mission-core-node/setup-monitor", 0o755),
|
|
("mission-core-node-monitor.service", "usr/lib/systemd/system/mission-core-node-monitor.service", 0o644),
|
|
]:
|
|
files.append((path, (p / source).read_bytes(), mode))
|
|
for path in sorted((ROOT / 'monitor').iterdir()):
|
|
if path.suffix in {'.py', '.sql'}:
|
|
files.append(('usr/lib/mission-core-node/monitor/' + path.name, path.read_bytes(), 0o644))
|
|
files.append(("usr/share/mission-core-node/environment-profile.json", (ROOT / "internal/node/environment-profile.json").read_bytes(), 0o644))
|
|
for name in ("realsense_prepare.py", "realsense_iio_access.py"):
|
|
files.append(("usr/lib/mission-core-node/" + name, (p / name).read_bytes(), 0o644))
|
|
for name in ("mission-core-realsense.service", "mission-core-node-realsense-prepare.service"):
|
|
files.append(("usr/lib/systemd/system/" + name, (p / name).read_bytes(), 0o644))
|
|
files.append(("usr/share/polkit-1/rules.d/50-mission-core-device-prepare.rules", (p / "50-mission-core-device-prepare.rules").read_bytes(), 0o644))
|
|
# Node owns the first-use bootstrap; the optional model package owns its
|
|
# SDK, worker processes, USB grants and runtime preparation. No overlapping
|
|
# dpkg file ownership and no compiler/download prerequisite at first use.
|
|
profile_raw = (p / "insta360-profile.json").read_bytes()
|
|
profile = json.loads(profile_raw)
|
|
package_name = "mission-core-insta360-x4_" + profile["version"] + "_amd64.deb"
|
|
x4 = (ROOT / "build/model-packages" / package_name).read_bytes()
|
|
if len(x4) != profile["bytes"] or hashlib.sha256(x4).hexdigest() != profile["sha256"]:
|
|
raise ValueError("Bundled X4 package differs from the admitted release")
|
|
files.extend([
|
|
("usr/lib/mission-core-node/insta360_profile.py", (p / "insta360_profile.py").read_bytes(), 0o644),
|
|
("usr/lib/systemd/system/mission-core-node-insta360-x4-profile.service", (p / "mission-core-node-insta360-x4-profile.service").read_bytes(), 0o644),
|
|
("usr/share/mission-core-node/profiles/insta360-x4/profile.json", profile_raw, 0o644),
|
|
("usr/share/mission-core-node/profiles/insta360-x4/" + package_name, x4, 0o644),
|
|
])
|
|
files.append(("usr/share/mission-core-node/realsense/70-mission-core-realsense.rules", (p / "70-mission-core-realsense.rules").read_bytes(), 0o644))
|
|
bundle = json.loads((p / "realsense-bundle.json").read_text())
|
|
files.append(("usr/share/mission-core-node/realsense/bundle.json", (p / "realsense-bundle.json").read_bytes(), 0o644))
|
|
for item in bundle["wheels"]:
|
|
data = (ROOT / "build/realsense-wheels" / item["name"]).read_bytes()
|
|
if hashlib.sha256(data).hexdigest() != item["sha256"]:
|
|
raise ValueError("Driver bundle hash mismatch")
|
|
files.append(("usr/share/mission-core-node/realsense/" + item["name"], data, 0o644))
|
|
for path in (ROOT / "sensors").glob("*.py"):
|
|
files.append(("usr/lib/mission-core-node/sensors/" + path.name, path.read_bytes(), 0o644))
|
|
sdk = ROOT.parents[1] / "packages/plugin-sdk/python/missioncore_plugin_sdk"
|
|
for path in sdk.rglob("*.py"):
|
|
files.append(("usr/lib/mission-core-node/sdk/missioncore_plugin_sdk/" + str(path.relative_to(sdk)), path.read_bytes(), 0o644))
|
|
if (ROOT / "build/provenance.json").exists():
|
|
files.append(("usr/share/doc/mission-core-node/provenance.json", (ROOT / "build/provenance.json").read_bytes(), 0o644))
|
|
archive = package(controls, files)
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_bytes(archive)
|
|
digest = hashlib.sha256(archive).hexdigest()
|
|
destination.with_suffix(destination.suffix + ".sha256").write_text(f"{digest} {destination.name}\n")
|
|
print(json.dumps({"file": str(destination), "bytes": len(archive), "sha256": digest}))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--binary", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
build(args.binary, args.output)
|