Package onboard K1 separately with a private portable installer

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 11:43:24 +03:00
parent 111fe3dcfb
commit d8cc5367c4
30 changed files with 1166 additions and 178 deletions
@@ -1,6 +0,0 @@
// Only WLAN discovery. No host association/profile modification authority.
polkit.addRule(function(action, subject) {
if (subject.user === "mission-core-k1" && action.id === "org.freedesktop.NetworkManager.wifi.scan") {
return polkit.Result.YES;
}
});
-1
View File
@@ -34,7 +34,6 @@ def provenance():
"design_guideline_files": guideline_sources(),
"shared_sensor_ui_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "packages/sensor-ui/src").rglob("*")) if p.is_file()},
"k1_frontend_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "plugins/xgrids-k1/frontend/src").rglob("*")) if p.is_file()},
"k1_runtime_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "src/k1link").rglob("*")) if p.is_file() and p.suffix in (".py", ".json")},
"toolchain": json.loads((ROOT / "toolchain.json").read_text()), "files": files}
+7 -52
View File
@@ -4,16 +4,17 @@
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
import sys
ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.7.1"
VERSION = "0.8.0"
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
from debian import package
BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af"
@@ -31,29 +32,6 @@ def desktop_icon(brand):
+ 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":
@@ -65,7 +43,7 @@ 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
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()
@@ -107,35 +85,12 @@ Description: Mission Core onboard computer configuration
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))
archive = package(controls, files)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(archive)
digest = hashlib.sha256(archive).hexdigest()
@@ -11,7 +11,8 @@ root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser()
parser.add_argument("--model", choices=("realsense", "k1"), default="realsense")
model = parser.parse_args().model
manifest = json.loads((root / f"packaging/{model}-bundle.json").read_text())
manifest_path = (root.parents[1] / "plugins/xgrids-k1/packaging/k1-bundle.json" if model == "k1" else root / "packaging/realsense-bundle.json")
manifest = json.loads(manifest_path.read_text())
output = root / f"build/{model}-wheels"
output.mkdir(parents=True, exist_ok=True)
for item in manifest["wheels"]:
@@ -1,36 +0,0 @@
#!/usr/bin/python3 -I
"""Administrator-only import of the exact application key from protected stdin."""
import os
import subprocess
import sys
import tempfile
from pathlib import Path
if os.geteuid() != 0 or len(sys.argv) != 1:
raise SystemExit("Root stdin import required")
secret = bytearray(sys.stdin.buffer.read(1025).strip())
try:
if len(secret) != 36 or any(v < 33 or v > 126 for v in secret):
raise SystemExit("Credential does not match the reviewed K1 profile")
root = Path("/etc/credstore.encrypted")
root.mkdir(mode=0o700, exist_ok=True)
if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022:
raise SystemExit("Unsafe credential store")
path = root / "k1-application"
if path.is_symlink() or path.exists():
raise SystemExit("K1 credential already installed; explicit rotation required")
with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory:
staged = Path(directory) / "encrypted"
completed = subprocess.run(
["/usr/bin/systemd-creds", "encrypt", "--name=k1-application", "--with-key=host", "-", str(staged)],
input=secret, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30,
)
if completed.returncode:
raise SystemExit("K1 credential import failed")
staged.chmod(0o600)
with staged.open("rb") as stream:
os.fsync(stream.fileno())
# Atomic publication without overwriting a concurrently installed key.
os.link(staged, path)
finally:
secret[:] = b"\0" * len(secret)
-265
View File
@@ -1,265 +0,0 @@
{
"schema": "missioncore.node.driver-bundle/v1",
"model_id": "xgrids.k1",
"revision": "c7ed0bba39f757afdac176a8",
"python": "3.12",
"platform": "linux-amd64",
"lock_sha256": "551c8ccdc44bc3724328dd1e316c81d20e63d2e4dcd97148377d6cacef479246",
"wheels": [
{
"name": "aioice-0.10.2-py3-none-any.whl",
"sha256": "14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf",
"bytes": 24875
},
{
"name": "aiortc-1.14.0-py3-none-any.whl",
"sha256": "4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e",
"bytes": 93183
},
{
"name": "annotated_doc-0.0.4-py3-none-any.whl",
"sha256": "571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320",
"bytes": 5303
},
{
"name": "annotated_types-0.7.0-py3-none-any.whl",
"sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
"bytes": 13643
},
{
"name": "anyio-4.14.2-py3-none-any.whl",
"sha256": "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494",
"bytes": 125813
},
{
"name": "attrs-26.1.0-py3-none-any.whl",
"sha256": "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309",
"bytes": 67548
},
{
"name": "av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl",
"sha256": "7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2",
"bytes": 41174337
},
{
"name": "bleak-3.0.2-py3-none-any.whl",
"sha256": "39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d",
"bytes": 146490
},
{
"name": "certifi-2026.7.22-py3-none-any.whl",
"sha256": "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775",
"bytes": 136983
},
{
"name": "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"sha256": "c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf",
"bytes": 221822
},
{
"name": "click-8.4.2-py3-none-any.whl",
"sha256": "e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76",
"bytes": 119243
},
{
"name": "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl",
"sha256": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b",
"bytes": 4459756
},
{
"name": "dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9",
"bytes": 852687
},
{
"name": "dnspython-2.8.0-py3-none-any.whl",
"sha256": "01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af",
"bytes": 331094
},
{
"name": "fastapi-0.139.0-py3-none-any.whl",
"sha256": "cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189",
"bytes": 130339
},
{
"name": "foxglove_sdk-0.25.3-cp310-abi3-manylinux_2_28_x86_64.whl",
"sha256": "bcc894b88188d8169973cfbb1370300f671760adea9d6e9447e5a03b2289527d",
"bytes": 19220466
},
{
"name": "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl",
"sha256": "14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411",
"bytes": 33364
},
{
"name": "h11-0.16.0-py3-none-any.whl",
"sha256": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86",
"bytes": 37515
},
{
"name": "httpcore-1.0.9-py3-none-any.whl",
"sha256": "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55",
"bytes": 78784
},
{
"name": "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
"sha256": "b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2",
"bytes": 523851
},
{
"name": "httpx-0.28.1-py3-none-any.whl",
"sha256": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad",
"bytes": 73517
},
{
"name": "idna-3.18-py3-none-any.whl",
"sha256": "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2",
"bytes": 65455
},
{
"name": "ifaddr-0.2.0-py3-none-any.whl",
"sha256": "085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748",
"bytes": 12314
},
{
"name": "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e",
"bytes": 1368249
},
{
"name": "markdown_it_py-4.2.0-py3-none-any.whl",
"sha256": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a",
"bytes": 91687
},
{
"name": "mdurl-0.1.2-py3-none-any.whl",
"sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
"bytes": 9979
},
{
"name": "numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca",
"bytes": 16672469
},
{
"name": "paho_mqtt-2.1.0-py3-none-any.whl",
"sha256": "6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee",
"bytes": 67219
},
{
"name": "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91",
"bytes": 6940830
},
{
"name": "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9",
"bytes": 155560
},
{
"name": "pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl",
"sha256": "5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778",
"bytes": 50088993
},
{
"name": "pycparser-3.0-py3-none-any.whl",
"sha256": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992",
"bytes": 48172
},
{
"name": "pydantic-2.13.4-py3-none-any.whl",
"sha256": "45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba",
"bytes": 472262
},
{
"name": "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"sha256": "926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce",
"bytes": 2094516
},
{
"name": "pyee-14.0.0-py3-none-any.whl",
"sha256": "3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d",
"bytes": 15553
},
{
"name": "pygments-2.20.0-py3-none-any.whl",
"sha256": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176",
"bytes": 1231151
},
{
"name": "pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc",
"bytes": 2434534
},
{
"name": "pyopenssl-26.2.0-py3-none-any.whl",
"sha256": "4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70",
"bytes": 55823
},
{
"name": "python_dotenv-1.2.2-py3-none-any.whl",
"sha256": "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a",
"bytes": 22101
},
{
"name": "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
"bytes": 807870
},
{
"name": "rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl",
"sha256": "287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe",
"bytes": 163018185
},
{
"name": "rich-14.3.4-py3-none-any.whl",
"sha256": "07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952",
"bytes": 310480
},
{
"name": "shellingham-1.5.4-py2.py3-none-any.whl",
"sha256": "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
"bytes": 9755
},
{
"name": "starlette-1.3.1-py3-none-any.whl",
"sha256": "c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6",
"bytes": 73632
},
{
"name": "typer-0.26.8-py3-none-any.whl",
"sha256": "3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c",
"bytes": 122564
},
{
"name": "typing_extensions-4.16.0-py3-none-any.whl",
"sha256": "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8",
"bytes": 45571
},
{
"name": "typing_inspection-0.4.2-py3-none-any.whl",
"sha256": "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7",
"bytes": 14611
},
{
"name": "uvicorn-0.51.0-py3-none-any.whl",
"sha256": "5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b",
"bytes": 73219
},
{
"name": "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4",
"bytes": 4426307
},
{
"name": "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
"sha256": "e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5",
"bytes": 456398
},
{
"name": "websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
"sha256": "35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a",
"bytes": 187345
}
]
}
-26
View File
@@ -1,26 +0,0 @@
"""Fixed root-owned import path; no user site, environment path or import hooks."""
import os
import sys
from pathlib import Path
root = Path("/var/lib/mission-core-k1-runtime")
reference = root / "active.path"
runtime = Path(reference.read_text().strip())
if (
reference.is_symlink()
or runtime.is_symlink()
or runtime.parent != root
or not runtime.name.isalnum()
or runtime.stat().st_uid != 0
or runtime.stat().st_mode & 0o022
):
raise RuntimeError("Unsafe K1 runtime")
sys.path[:0] = [
str(runtime), str(runtime / "rerun_sdk"),
"/usr/lib/mission-core-node/k1/src", "/usr/lib/mission-core-node/sdk",
]
os.environ["MISSIONCORE_DATA_DIR"] = "/var/lib/mission-core-k1"
from k1link.device_plugins.xgrids_k1.node_bridge import main
main()
-94
View File
@@ -1,94 +0,0 @@
"""Install only the bundled, hash-pinned Ubuntu K1 runtime; no network I/O."""
import hashlib
import json
import os
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path, PurePosixPath
SHARE = Path("/usr/share/mission-core-node/k1")
ROOT = Path("/var/lib/mission-core-k1-runtime")
def members(archive):
for info in archive.infolist():
path = PurePosixPath(info.filename)
if (
path.is_absolute()
or ".." in path.parts
or (info.external_attr >> 16) & 0o170000 == 0o120000
or ".data" in path.parts
):
raise RuntimeError("Unsafe K1 runtime archive")
# Rerun's pinned wheel declares this one static package directory.
# We do not execute .pth files; bootstrap adds the exact directory.
if info.filename.endswith(".pth") and not (
info.filename == "rerun_sdk.pth" and archive.read(info) == b"rerun_sdk\n"
):
raise RuntimeError("Unreviewed K1 Python path hook")
# Distribution script/data relocation must be handled deliberately,
# never interpreted as an install hook by the operator's Python.
if any(part.endswith(".data") for part in path.parts):
raise RuntimeError("K1 wheel requires unsupported relocation")
yield info
def prepare():
if os.geteuid() != 0 or os.uname().machine != "x86_64" or sys.version_info[:2] != (3, 12):
raise RuntimeError("K1 runtime requires privileged Ubuntu amd64 Python 3.12 installation")
release = Path("/etc/os-release").read_text()
if "ID=ubuntu" not in release or 'VERSION_ID="24.04"' not in release:
raise RuntimeError("K1 runtime requires Ubuntu 24.04")
manifest = json.loads((SHARE / "bundle.json").read_text())
revision = manifest["revision"]
if not revision.isalnum():
raise RuntimeError("Invalid K1 runtime revision")
ROOT.mkdir(mode=0o755, exist_ok=True)
if ROOT.is_symlink() or ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022:
raise RuntimeError("Unsafe K1 runtime root")
target = ROOT / revision
if target.is_symlink() or (ROOT / "active.path").is_symlink():
raise RuntimeError("Unsafe K1 runtime reference")
for item in manifest["wheels"]:
path = SHARE / item["name"]
if (
path.name != item["name"]
or path.is_symlink()
or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]
):
raise RuntimeError("K1 runtime checksum mismatch")
if not target.exists():
stage = Path(tempfile.mkdtemp(prefix=".k1-", dir=ROOT))
try:
for item in manifest["wheels"]:
with zipfile.ZipFile(SHARE / item["name"]) as archive:
archive.extractall(stage, members=members(archive))
for path in stage.rglob("*"):
path.chmod(0o755 if path.is_dir() else 0o644)
stage.chmod(0o755)
stage.rename(target)
finally:
if stage.exists():
shutil.rmtree(stage)
for item in manifest["wheels"]:
with zipfile.ZipFile(SHARE / item["name"]) as archive:
for info in members(archive):
path = target / info.filename
if path.is_symlink() or (
not info.is_dir() and path.read_bytes() != archive.read(info)
):
raise RuntimeError("Installed K1 runtime differs from bundled wheel")
fd, name = tempfile.mkstemp(prefix=".active-", dir=ROOT)
with os.fdopen(fd, "w") as stream:
os.fchmod(stream.fileno(), 0o644)
stream.write(str(target))
stream.flush()
os.fsync(stream.fileno())
os.replace(name, ROOT / "active.path")
if __name__ == "__main__":
prepare()
@@ -1,38 +0,0 @@
[Unit]
Description=Mission Core Node K1 Bridge and acquisition
After=bluetooth.service NetworkManager.service
Wants=bluetooth.service NetworkManager.service
[Service]
Type=simple
User=mission-core-k1
Group=mission-core-node
SupplementaryGroups=bluetooth
ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/k1_bootstrap.py
StateDirectory=mission-core-k1
StateDirectoryMode=0700
RuntimeDirectory=mission-core-k1
RuntimeDirectoryMode=0750
LoadCredentialEncrypted=k1-application
UMask=0007
Environment=OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2
Restart=on-failure
RestartSec=3
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
CapabilityBoundingSet=
LockPersonality=yes
TasksMax=128
MemoryMax=2G
LimitNOFILE=2048
[Install]
WantedBy=multi-user.target
-6
View File
@@ -8,10 +8,6 @@ case "$1" in
if ! getent passwd mission-core-sensors >/dev/null; then
adduser --system --group --home /var/lib/mission-core-sensors --no-create-home --disabled-login mission-core-sensors
fi
if ! getent passwd mission-core-k1 >/dev/null; then
adduser --system --home /var/lib/mission-core-k1 --no-create-home --disabled-login --ingroup mission-core-node mission-core-k1
fi
/usr/bin/python3 -I /usr/lib/mission-core-node/k1_prepare.py
# Only bootstrap required to open the GUI. Operational configuration is a
# versioned job started by «Настройка окружения → Сконфигурировать».
if [ -d /run/systemd/system ]; then
@@ -19,8 +15,6 @@ case "$1" in
systemctl enable mission-core-node.service
systemctl restart mission-core-node.service
systemctl try-restart mission-core-realsense.service
systemctl enable mission-core-k1.service
systemctl restart mission-core-k1.service
fi
;;
esac
@@ -1,60 +0,0 @@
"""Resolve Linux wheel inputs from the frozen monorepo lock, without downloading."""
import hashlib
import json
import subprocess
import tomllib
from pathlib import Path
from packaging.markers import default_environment
from packaging.requirements import Requirement
from packaging.tags import compatible_tags, cpython_tags
from packaging.utils import canonicalize_name, parse_wheel_filename
ROOT = Path(__file__).resolve().parents[1]
REPOSITORY = ROOT.parents[1]
def resolve():
requirements = ROOT / "build/k1-requirements.txt"
requirements.parent.mkdir(exist_ok=True)
subprocess.run(["uv", "export", "--frozen", "--extra", "node-device-media", "--no-dev",
"--no-emit-project", "--no-emit-package", "missioncore-plugin-sdk", "--no-hashes",
"--output-file", str(requirements)], cwd=REPOSITORY, check=True, stdout=subprocess.DEVNULL)
environment = default_environment()
environment.update(sys_platform="linux", platform_system="Linux", platform_machine="x86_64",
python_version="3.12", python_full_version="3.12.3", implementation_name="cpython",
platform_python_implementation="CPython")
platforms = [f"manylinux_2_{n}_x86_64" for n in range(39, 16, -1)] + ["manylinux2014_x86_64", "linux_x86_64"]
tags = list(cpython_tags((3, 12), platforms=platforms)) + list(compatible_tags((3, 12), interpreter="cp312", platforms=platforms))
ranks = {tag: i for i, tag in enumerate(tags)}
lock_data = (REPOSITORY / "uv.lock").read_bytes()
lock = tomllib.loads(lock_data.decode())
items = []
for line in requirements.read_text().splitlines():
if not line or line.lstrip().startswith("#"):
continue
requirement = Requirement(line)
if requirement.marker and not requirement.marker.evaluate(environment):
continue
name = canonicalize_name(requirement.name)
package = next(v for v in lock["package"] if canonicalize_name(v["name"]) == name and v["version"] in requirement.specifier)
candidates = []
for wheel in package.get("wheels", []):
filename = wheel["url"].split("/")[-1]
_, _, _, wheel_tags = parse_wheel_filename(filename)
matches = [ranks[tag] for tag in wheel_tags if tag in ranks]
if matches:
candidates.append((min(matches), filename, wheel))
if not candidates:
raise RuntimeError("No reviewed Linux wheel: " + name)
_, filename, wheel = min(candidates)
items.append({"name": filename, "sha256": wheel["hash"].removeprefix("sha256:"), "bytes": wheel["size"]})
revision = hashlib.sha256(json.dumps(items, sort_keys=True).encode()).hexdigest()[:24]
manifest = {"schema": "missioncore.node.driver-bundle/v1", "model_id": "xgrids.k1", "revision": revision,
"python": "3.12", "platform": "linux-amd64", "lock_sha256": hashlib.sha256(lock_data).hexdigest(), "wheels": items}
(ROOT / "packaging/k1-bundle.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(json.dumps({"wheels": len(items), "bytes": sum(v["bytes"] for v in items), "revision": revision}))
if __name__ == "__main__":
resolve()