188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the optional onboard K1 integration, including a private autonomous edition.
|
|
|
|
The public build contains no application material. --private-authority-stdin
|
|
creates a mode-0600 private installer; the key never appears in arguments,
|
|
provenance, stdout or a source file. This installer itself is a private artifact.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
PACKAGING = Path(__file__).resolve().parent
|
|
REPOSITORY = PACKAGING.parents[2]
|
|
sys.path.insert(0, str(REPOSITORY / "scripts/packaging"))
|
|
from credential_install import PROFILE_ID, validate # noqa: E402
|
|
from debian import package # noqa: E402
|
|
from runtime_payload import files as runtime_files # noqa: E402
|
|
|
|
VERSION = "0.1.9"
|
|
RESOURCES = (
|
|
"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",
|
|
)
|
|
|
|
|
|
def payload(wheel_root, *, authority=None):
|
|
files = []
|
|
for name in ("k1_prepare.py", "k1_bootstrap.py", "install-k1-credential"):
|
|
files.append(
|
|
(
|
|
"usr/lib/mission-core-node/" + name,
|
|
(PACKAGING / name).read_bytes(),
|
|
0o755 if name == "install-k1-credential" else 0o644,
|
|
)
|
|
)
|
|
files.extend(
|
|
[
|
|
(
|
|
"usr/lib/mission-core-node/k1-install/credential_install.py",
|
|
(PACKAGING / "credential_install.py").read_bytes(),
|
|
0o644,
|
|
),
|
|
(
|
|
"usr/lib/mission-core-node/prepare-k1-profile",
|
|
(PACKAGING / "prepare-bundled-profile").read_bytes(),
|
|
0o755,
|
|
),
|
|
(
|
|
"usr/lib/systemd/system/mission-core-k1.service",
|
|
(PACKAGING / "mission-core-k1.service").read_bytes(),
|
|
0o644,
|
|
),
|
|
(
|
|
"usr/share/polkit-1/rules.d/50-mission-core-k1.rules",
|
|
(PACKAGING / "50-mission-core-k1.rules").read_bytes(),
|
|
0o644,
|
|
),
|
|
(
|
|
"usr/share/mission-core-node/k1/bundle.json",
|
|
(PACKAGING / "k1-bundle.json").read_bytes(),
|
|
0o644,
|
|
),
|
|
]
|
|
)
|
|
wheels = json.loads((PACKAGING / "k1-bundle.json").read_text())["wheels"]
|
|
for item in wheels:
|
|
path = wheel_root / item["name"]
|
|
if path.name != item["name"] or path.is_symlink():
|
|
raise ValueError("Unsafe K1 wheel path")
|
|
data = path.read_bytes()
|
|
if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]:
|
|
raise ValueError("K1 wheel differs from the admitted bundle")
|
|
files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644))
|
|
sources = runtime_files()
|
|
for path in sources:
|
|
files.append(
|
|
(
|
|
"usr/lib/mission-core-node/k1/" + str(path.relative_to(REPOSITORY)),
|
|
path.read_bytes(),
|
|
0o644,
|
|
)
|
|
)
|
|
for relative in RESOURCES:
|
|
files.append(
|
|
(
|
|
"usr/lib/mission-core-node/k1/" + relative,
|
|
(REPOSITORY / relative).read_bytes(),
|
|
0o644,
|
|
)
|
|
)
|
|
# Hash only code and public metadata. Never hash private material into a
|
|
# public per-file manifest or represent it as scanner identity.
|
|
provenance = {
|
|
"package": "mission-core-xgrids-k1",
|
|
"version": VERSION + ("+private.1" if authority is not None else ""),
|
|
"base_commit": subprocess.check_output(
|
|
["git", "rev-parse", "HEAD"], cwd=REPOSITORY, text=True
|
|
).strip(),
|
|
"application_profile": PROFILE_ID,
|
|
"application_material_included": authority is not None,
|
|
"acceptance_scope": "owner-controlled-k1-fw-3.0.2-ubuntu-24.04-amd64",
|
|
"files": {name: hashlib.sha256(data).hexdigest() for name, data, _ in files},
|
|
}
|
|
files.append(
|
|
(
|
|
"usr/share/doc/mission-core-xgrids-k1/provenance.json",
|
|
(json.dumps(provenance, indent=2) + "\n").encode(),
|
|
0o644,
|
|
)
|
|
)
|
|
if authority is not None:
|
|
validate(authority)
|
|
files.append(
|
|
("usr/share/mission-core-node/k1/private-application-key", bytes(authority), 0o600)
|
|
)
|
|
return files
|
|
|
|
|
|
def build(destination, wheel_root, *, authority=None):
|
|
if authority is not None:
|
|
validate(authority)
|
|
version = VERSION + ("+private.1" if authority is not None else "")
|
|
control = f"""Package: mission-core-xgrids-k1
|
|
Version: {version}
|
|
Architecture: amd64
|
|
Maintainer: NODE.DC local build <noreply@example.invalid>
|
|
Section: admin
|
|
Priority: optional
|
|
Depends: mission-core-node (>= 0.8.9), mission-core-node (<< 0.9.0),
|
|
systemd, python3, adduser, bluez, network-manager, iproute2, ffmpeg
|
|
Breaks: mission-core-node (<< 0.8.0)
|
|
Replaces: mission-core-node (<< 0.8.0)
|
|
Description: Optional XGRIDS K1 Bridge integration for Mission Core Node
|
|
Reviewed FW 3.0.2 runtime with a separate process and durable device journals.
|
|
""".encode()
|
|
controls = [("control", control, 0o644)]
|
|
controls += [
|
|
(name, (PACKAGING / name).read_bytes(), 0o755)
|
|
for name in ("preinst", "postinst", "prerm", "postrm")
|
|
]
|
|
archive = package(controls, payload(wheel_root, authority=authority))
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
# Never replace a previously published release with different bytes.
|
|
fd, staged = tempfile.mkstemp(prefix=".k1-package-", dir=destination.parent)
|
|
try:
|
|
with os.fdopen(fd, "wb") as stream:
|
|
os.fchmod(stream.fileno(), 0o600 if authority is not None else 0o644)
|
|
stream.write(archive)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.link(staged, destination)
|
|
finally:
|
|
os.unlink(staged)
|
|
return {
|
|
"package": destination.name,
|
|
"version": version,
|
|
"bytes": len(archive),
|
|
"sha256": hashlib.sha256(archive).hexdigest(),
|
|
"private": authority is not None,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--wheel-root", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--private-authority-stdin", action="store_true")
|
|
args = parser.parse_args()
|
|
secret = None
|
|
try:
|
|
if args.private_authority_stdin:
|
|
secret = bytearray(sys.stdin.buffer.read(1025).strip())
|
|
print(json.dumps(build(args.output, args.wheel_root, authority=secret)))
|
|
except Exception:
|
|
raise SystemExit("K1 package build failed; check inputs and unused output path") from None
|
|
finally:
|
|
if secret is not None:
|
|
secret[:] = b"\0" * len(secret)
|