Files
NODEDC_MISSION_CORE/apps/node-agent/packaging/build_deb.py
T
DCCONSTRUCTIONS 1c7dd29d8a chore(node): preserve pre-canonicalization experiment snapshot
Historical working copy retained for audit before consolidation into main. The canonicalized plugin architecture and later fixes already live in main; this snapshot is not a release or a request to restore obsolete source layout.
2026-09-21 08:45:34 +03:00

152 lines
9.0 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 gzip
import hashlib
import io
import json
from pathlib import Path
import tarfile
ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.7.0"
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 tarball(files):
stream = io.BytesIO()
with tarfile.open(fileobj=stream, mode="w", format=tarfile.GNU_FORMAT) as archive:
directories = {str(parent) for name, _, _ in files for parent in Path(name).parents if str(parent) != "."}
for name in sorted(directories):
item = tarfile.TarInfo(name + "/")
item.type, item.mode = tarfile.DIRTYPE, 0o755
item.uname = item.gname = "root"
archive.addfile(item)
for name, data, mode in sorted(files):
item = tarfile.TarInfo(name)
item.size, item.mode, item.uid, item.gid = len(data), mode, 0, 0
item.uname = item.gname = "root"
archive.addfile(item, io.BytesIO(data))
return gzip.compress(stream.getvalue(), mtime=0)
def ar_member(name, data):
header = f"{name + '/':<16}{0:<12}{0:<6}{0:<6}{'100644':<8}{len(data):<10}`\n".encode()
assert len(header) == 60
return header + data + (b"\n" if len(data) % 2 else b"")
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, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, bluez, network-manager, iproute2, ffmpeg
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))
for name in ("k1_prepare.py", "k1_bootstrap.py"):
files.append(("usr/lib/mission-core-node/" + name, (p / name).read_bytes(), 0o644))
files.append(("usr/lib/mission-core-node/install-k1-credential", (p / "install-k1-credential").read_bytes(), 0o755))
files.append(("usr/lib/systemd/system/mission-core-k1.service", (p / "mission-core-k1.service").read_bytes(), 0o644))
files.append(("usr/share/polkit-1/rules.d/50-mission-core-k1.rules", (p / "50-mission-core-k1.rules").read_bytes(), 0o644))
k1_bundle = json.loads((p / "k1-bundle.json").read_text())
files.append(("usr/share/mission-core-node/k1/bundle.json", (p / "k1-bundle.json").read_bytes(), 0o644))
for item in k1_bundle["wheels"]:
data = (ROOT / "build/k1-wheels" / item["name"]).read_bytes()
if hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("K1 bundle hash mismatch")
files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644))
repository = ROOT.parents[1]
# Reuse the admitted plugin runtime and the transport-neutral renderer.
# No separate Core web service is started on the board.
for path in (repository / "src/k1link").rglob("*"):
if path.is_file() and path.suffix in (".py", ".json"):
files.append(("usr/lib/mission-core-node/k1/src/k1link/" + str(path.relative_to(repository / "src/k1link")), path.read_bytes(), 0o644))
for relative in ("plugins/xgrids-k1/profile_loader.py", "plugins/xgrids-k1/plugin.manifest.json",
"config/observatory-equipment-models.json",
"config/observatory-recorded-capture-profiles.json",
"plugins/xgrids-k1/profiles/fw-3.0.2/local-network.v2.json"):
files.append(("usr/lib/mission-core-node/k1/" + relative, (repository / relative).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 = b"!<arch>\n" + ar_member("debian-binary", b"2.0\n") + ar_member("control.tar.gz", tarball(controls)) + ar_member("data.tar.gz", tarball(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)