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.
152 lines
9.0 KiB
Python
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)
|