#!/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] VERSION = "0.8.5" 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'\n' + brand + b'\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 Section: admin Priority: optional Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, network-manager, iproute2 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), ]: files.append((path, (p / source).read_bytes(), mode)) 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)) 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)