feat(vesc): integrate native calibration diagnostics and configuration archives
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Exact observed candidate descriptor. Firmware identity is verified by the reader.
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", ATTRS{product}=="ChibiOS/RT Virtual COM Port", GROUP="mission-core-vesc", MODE="0660", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1"
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Create the deterministic, self-contained native-engine qualification job."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
|
||||
def build(output):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
files = {"__main__.py": (root / "packaging/native_probe.py").read_bytes(),
|
||||
"offline_main.cpp": (root / "native/offline_main.cpp").read_bytes(),
|
||||
"config_export.h": (root / "native/config_export.h").read_bytes(),
|
||||
"engine_main.cpp": (root / "native/engine_main.cpp").read_bytes(),
|
||||
"native_bundle.py": (root / "packaging/native_bundle.py").read_bytes()}
|
||||
identity = hashlib.sha256(b"".join(k.encode() + v for k, v in sorted(files.items()))).hexdigest()[:24]
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
path = output / ("mission-core-vesc-native-probe-" + identity + ".pyz")
|
||||
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as bundle:
|
||||
for name, data in files.items():
|
||||
entry = zipfile.ZipInfo(name, (2026, 9, 23, 0, 0, 0))
|
||||
entry.external_attr = 0o600 << 16
|
||||
bundle.writestr(entry, data)
|
||||
result = {"id": identity, "artifact": str(path.resolve()),
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "bytes": path.stat().st_size}
|
||||
(output / "current-artifact.json").write_text(json.dumps(result, indent=2) + "\n")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
print(json.dumps(build(parser.parse_args().output)))
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Discard only generated VESC bytecode before starting an upgraded profile.
|
||||
|
||||
Deterministic packages reuse file mtimes; a same-size Python source update
|
||||
can otherwise validate an old timestamp-based pyc, even with python -B.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
|
||||
def clear(root):
|
||||
for cache in root.rglob("__pycache__"):
|
||||
if cache.is_symlink():
|
||||
cache.unlink()
|
||||
elif cache.is_dir():
|
||||
shutil.rmtree(cache)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1:]:
|
||||
raise ValueError("This installer step accepts no paths or arguments")
|
||||
clear(Path("/usr/lib/mission-core-vesc"))
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Prepare the versioned VESC read profile
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 -I -B /usr/lib/mission-core-vesc/prepare.py
|
||||
TimeoutStartSec=90
|
||||
UMask=0022
|
||||
@@ -0,0 +1,37 @@
|
||||
[Unit]
|
||||
Description=Mission Core VESC Tool service
|
||||
Wants=modprobe@cdc_acm.service
|
||||
After=systemd-udev-settle.service modprobe@cdc_acm.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mission-core-vesc
|
||||
Group=mission-core-node
|
||||
SupplementaryGroups=mission-core-vesc
|
||||
WorkingDirectory=/usr/lib/mission-core-vesc
|
||||
ExecStart=/usr/bin/python3 -B -m runtime.server
|
||||
RuntimeDirectory=mission-core-vesc
|
||||
RuntimeDirectoryMode=0750
|
||||
StateDirectory=mission-core-vesc
|
||||
StateDirectoryMode=0700
|
||||
UMask=0007
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=yes
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
PrivateNetwork=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
DevicePolicy=closed
|
||||
DeviceAllow=char-ttyACM rw
|
||||
MemoryMax=512M
|
||||
TasksMax=64
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,676 @@
|
||||
{
|
||||
"schema": "missioncore.vesc.native-runtime/v1",
|
||||
"upstream_version": "7.00",
|
||||
"upstream_commit": "01d5f10901116c311e3fb84d5a1541f663d3ce20",
|
||||
"os": "ubuntu-24.04-amd64",
|
||||
"file": "mission-core-vesc-native-runtime.tar.gz",
|
||||
"bytes": 50354000,
|
||||
"sha256": "4473819387682089a814a6c558065e3e6a4c2fe67266c0ed82ad49cea1444aad",
|
||||
"engine_sha256": "10cfe14cd0eff561a06f0da33c940861b314de077de489908c90204904e4e590",
|
||||
"files": {
|
||||
"bin/mission-core-vesc-engine": {
|
||||
"sha256": "10cfe14cd0eff561a06f0da33c940861b314de077de489908c90204904e4e590",
|
||||
"bytes": 24801840
|
||||
},
|
||||
"lib/libGL.so.1": {
|
||||
"sha256": "67f471213576d225d38347a0b6d2a08a231980685301ff6461bd74d3994e5027",
|
||||
"bytes": 547136
|
||||
},
|
||||
"lib/libGLX.so.0": {
|
||||
"sha256": "16fc8a37eea9210dc83c57eeff5aedc10ab4c6673f2f97e8bb6ee103df657b40",
|
||||
"bytes": 137792
|
||||
},
|
||||
"lib/libGLdispatch.so.0": {
|
||||
"sha256": "ca01a91104c8887b3d8e59499b58cbb8f604cc285666b50d9ec888eb0c915182",
|
||||
"bytes": 719304
|
||||
},
|
||||
"lib/libQt5Bluetooth.so.5": {
|
||||
"sha256": "7e409c8ba1153801671343a454a3aee4943504f2d7a9d1077c67ff46dc7a9831",
|
||||
"bytes": 915320
|
||||
},
|
||||
"lib/libQt5Core.so.5": {
|
||||
"sha256": "f3a46b3517fdcd82d70b9a2c01ba707499860e2eab328bb98445d8afce59ca37",
|
||||
"bytes": 5699976
|
||||
},
|
||||
"lib/libQt5DBus.so.5": {
|
||||
"sha256": "1122d2c92c03fca880e2e8e7de4eaf15fc577f75012362e8f3a76a38a3ce07ab",
|
||||
"bytes": 595528
|
||||
},
|
||||
"lib/libQt5Gamepad.so.5": {
|
||||
"sha256": "364e27bb1a7dd64ff974ab6442a844beb729006da53912581674c478ac5dc4d7",
|
||||
"bytes": 130144
|
||||
},
|
||||
"lib/libQt5Gui.so.5": {
|
||||
"sha256": "75b66a3edcbc7f3013b4180b76919b49c22ce652372eba45ead8aac9121625e0",
|
||||
"bytes": 7256408
|
||||
},
|
||||
"lib/libQt5Network.so.5": {
|
||||
"sha256": "8b3062321c31ddb2c0d96f2c7a7761e086fc4ef2d0d52dbf1119a03d0079ec64",
|
||||
"bytes": 1749216
|
||||
},
|
||||
"lib/libQt5Positioning.so.5": {
|
||||
"sha256": "267929ff49f2ecfbfed7951f3a2cd50985dc2d96f1d8f5ca689615bb6ee5f2b6",
|
||||
"bytes": 590448
|
||||
},
|
||||
"lib/libQt5PrintSupport.so.5": {
|
||||
"sha256": "818b07d6b46ede7e935f97c523231c97b79b44004723b8c1a17e40739337767d",
|
||||
"bytes": 486696
|
||||
},
|
||||
"lib/libQt5Qml.so.5": {
|
||||
"sha256": "e57364d9d0d366824064539a49f4cb3d8fdd284a15db9e3d164ca1ac9a1f64f3",
|
||||
"bytes": 4729856
|
||||
},
|
||||
"lib/libQt5QmlModels.so.5": {
|
||||
"sha256": "68bebbb6db95909aa2262aa2fa6cfd23547565faaa5bb72c49cfe027ac9caf0b",
|
||||
"bytes": 567600
|
||||
},
|
||||
"lib/libQt5Quick.so.5": {
|
||||
"sha256": "a2d2e45f415baa2c9a251844dd45f67d1d10cf6f15c6270dd49c0d348a431260",
|
||||
"bytes": 5737928
|
||||
},
|
||||
"lib/libQt5QuickWidgets.so.5": {
|
||||
"sha256": "8e3947729ac17448ed59a0a0a74960d9f09244a2d084c976b1c4af342f87d82e",
|
||||
"bytes": 92384
|
||||
},
|
||||
"lib/libQt5SerialPort.so.5": {
|
||||
"sha256": "67305251921c0a1d737b90d72b365cc937ca665c6ee218575e2555e85c63dcdb",
|
||||
"bytes": 97680
|
||||
},
|
||||
"lib/libQt5Widgets.so.5": {
|
||||
"sha256": "8e03815f581781fc8b0ef931335493f8f2fcb48ee855f0be2258bbfbd06e62ee",
|
||||
"bytes": 7057536
|
||||
},
|
||||
"lib/libX11.so.6": {
|
||||
"sha256": "c5b5d782bd9cab3420a62df88f5c991507edf3331a89f98464ddbc538c37b879",
|
||||
"bytes": 1298088
|
||||
},
|
||||
"lib/libXau.so.6": {
|
||||
"sha256": "8040da3f8516c1acfe39f04ff022480cdb273705ba5e7a4ca70bbdb1527cd67a",
|
||||
"bytes": 18696
|
||||
},
|
||||
"lib/libXdmcp.so.6": {
|
||||
"sha256": "667d97d6da16016400ab10de9f83ef4ab209ceb6adf68ff9650225ec13e723b0",
|
||||
"bytes": 26776
|
||||
},
|
||||
"lib/libbrotlicommon.so.1": {
|
||||
"sha256": "a91ead095d2c80520c55a89057bbe10b031a075340442e63f44b310f93883a1b",
|
||||
"bytes": 141640
|
||||
},
|
||||
"lib/libbrotlidec.so.1": {
|
||||
"sha256": "64d8a5019d4c294b89fde1193343ea324bbd8603652554e5545f0a01595fa2c5",
|
||||
"bytes": 51512
|
||||
},
|
||||
"lib/libbsd.so.0": {
|
||||
"sha256": "e86cd4f0019f42c2ba5e60602e0edc8694d2948377d98c3654d0f6bddb5254bb",
|
||||
"bytes": 80888
|
||||
},
|
||||
"lib/libbz2.so.1.0": {
|
||||
"sha256": "cc08c9f50a8009ffd6391e0116a100369b11ca9238fa52392c019e6645b122a1",
|
||||
"bytes": 78944
|
||||
},
|
||||
"lib/libcap.so.2": {
|
||||
"sha256": "6ac6abc86ac891c6e13486470e26f1d939f47fda9e6b5d5508a7f5ec881adc84",
|
||||
"bytes": 51536
|
||||
},
|
||||
"lib/libcom_err.so.2": {
|
||||
"sha256": "022943b3b11c860b049bce41342f1c2594941b7b401d95dfdf235521099fee08",
|
||||
"bytes": 18504
|
||||
},
|
||||
"lib/libdbus-1.so.3": {
|
||||
"sha256": "a6ae7b4ef48562b40d7b9ba8efd2e49f6528b7cc6364bea382dcee4b473b5413",
|
||||
"bytes": 317752
|
||||
},
|
||||
"lib/libdouble-conversion.so.3": {
|
||||
"sha256": "d1c9583dc7c1fce6f0a0701dd4356448425e45afe15c0946c23edeaf93d9397c",
|
||||
"bytes": 79952
|
||||
},
|
||||
"lib/libexpat.so.1": {
|
||||
"sha256": "ec6c12d33bb8f9d0e90804121adf19930f36b1b2a4aeb6e1a454b89c7a50c801",
|
||||
"bytes": 186624
|
||||
},
|
||||
"lib/libfontconfig.so.1": {
|
||||
"sha256": "a94b4059b27766f563894c8f7e61762b6f6b2e25c59ef36b730164d4b75c6c98",
|
||||
"bytes": 325712
|
||||
},
|
||||
"lib/libfreetype.so.6": {
|
||||
"sha256": "c14c53c5baff12afafb610c6312fb879e9bb77e80dd42e27504d52d6d8bcd059",
|
||||
"bytes": 833608
|
||||
},
|
||||
"lib/libgcc_s.so.1": {
|
||||
"sha256": "d93224d2b0dab4247598be683adca02f5cf00586f99c187579cd7e92058fb7cb",
|
||||
"bytes": 183024
|
||||
},
|
||||
"lib/libgcrypt.so.20": {
|
||||
"sha256": "6ad6d7007ee1ad8319eb18ba9a512cf09dda49396a303774c68b47477020bad4",
|
||||
"bytes": 1345072
|
||||
},
|
||||
"lib/libglib-2.0.so.0": {
|
||||
"sha256": "96ef9163aee942bdc09e6f4a1acd2fd6b178c03af824c569741440d63ac9f4f4",
|
||||
"bytes": 1343056
|
||||
},
|
||||
"lib/libgpg-error.so.0": {
|
||||
"sha256": "6cb18a007bfcb623029f4528a36b46578fea7dc34c2b505b8e6e0d99e6348cd1",
|
||||
"bytes": 149760
|
||||
},
|
||||
"lib/libgraphite2.so.3": {
|
||||
"sha256": "fcfaf843b25b58b88319ced52f826fc8213a661f3693c916f88641eddffe05b8",
|
||||
"bytes": 149776
|
||||
},
|
||||
"lib/libgssapi_krb5.so.2": {
|
||||
"sha256": "6c1b81696044d79a47d6f0f494ee60aaf641c525c46a11da0c6e4a041a782d3c",
|
||||
"bytes": 338696
|
||||
},
|
||||
"lib/libharfbuzz.so.0": {
|
||||
"sha256": "4562cfcfd18935324ba3ac74a944898b867ab87a56e925ab3e351ba773f1513e",
|
||||
"bytes": 1101752
|
||||
},
|
||||
"lib/libicudata.so.74": {
|
||||
"sha256": "ddbb3718b8bd9cbd780e5ab08b4503c30a6c4fa0706ebe5d074ed6b596c1714e",
|
||||
"bytes": 30795392
|
||||
},
|
||||
"lib/libicui18n.so.74": {
|
||||
"sha256": "3550b194eb2cf2e6f798f033eb9ca279d498c21296b4a18790ce158d2023e47b",
|
||||
"bytes": 3455304
|
||||
},
|
||||
"lib/libicuuc.so.74": {
|
||||
"sha256": "7560aadde38e5f4237a47a1ddd5891f9b36768a77a60faae30beee003ac01901",
|
||||
"bytes": 2140336
|
||||
},
|
||||
"lib/libk5crypto.so.3": {
|
||||
"sha256": "73bc9d72c0c684d6149a3c38f96aab891d178375415c05954fdda587685936f5",
|
||||
"bytes": 178648
|
||||
},
|
||||
"lib/libkeyutils.so.1": {
|
||||
"sha256": "f48214417757f18793ed6e180cc14ee1d6f04252a518fc7270e8ca1d0b4260fe",
|
||||
"bytes": 22600
|
||||
},
|
||||
"lib/libkrb5.so.3": {
|
||||
"sha256": "9615a2841f0783c410eec7fae005a951282551cfedd377737ad8431c8c8cde64",
|
||||
"bytes": 823488
|
||||
},
|
||||
"lib/libkrb5support.so.0": {
|
||||
"sha256": "0aa43578471faecbd642ed2ee6ab92b6682a2b676644193e956fc59d18ca24bb",
|
||||
"bytes": 47904
|
||||
},
|
||||
"lib/liblz4.so.1": {
|
||||
"sha256": "40bffd0a098387368b16b992abd5f7cf43c0fa2f05cabe5a6d483719554adfda",
|
||||
"bytes": 137440
|
||||
},
|
||||
"lib/liblzma.so.5": {
|
||||
"sha256": "696e868dd0700a19a6d65fc01608ec2d70d3cb91f65710e89180cd2e688f30cb",
|
||||
"bytes": 202904
|
||||
},
|
||||
"lib/libmd.so.0": {
|
||||
"sha256": "423e18586b6ea740f4465afd64f7a9a4cb7264ed979c8e7256f9085af5345fef",
|
||||
"bytes": 55536
|
||||
},
|
||||
"lib/libmd4c.so.0": {
|
||||
"sha256": "d5f418d0ea9aec6b41efbb924580a1fb61e9113bcae36d5ad3f322269822d1b3",
|
||||
"bytes": 67656
|
||||
},
|
||||
"lib/libpcre2-16.so.0": {
|
||||
"sha256": "4dfa8a4023270763b8ca1654dc4f59f835393b1ae78085a19369228f8109a671",
|
||||
"bytes": 572064
|
||||
},
|
||||
"lib/libpcre2-8.so.0": {
|
||||
"sha256": "e00576d71d81d3ba0cfa4903c835a44a8723aac96f72f79ff75200b4cff9071b",
|
||||
"bytes": 625344
|
||||
},
|
||||
"lib/libpng16.so.16": {
|
||||
"sha256": "eac265b3506df0d9110dd9143e1d0503e2daabafe1d98b23fac8ca17b71d1f6b",
|
||||
"bytes": 223304
|
||||
},
|
||||
"lib/libstdc++.so.6": {
|
||||
"sha256": "1fd75fe70354a416d75aef22bcae68c47bd25d20e2d0568c30b1a9838cf62f11",
|
||||
"bytes": 2592224
|
||||
},
|
||||
"lib/libsystemd.so.0": {
|
||||
"sha256": "bdf59c828b547bcdbe7b3576c0810d9d9d2e982d8b61b365812542da6a4c4a99",
|
||||
"bytes": 910592
|
||||
},
|
||||
"lib/libudev.so.1": {
|
||||
"sha256": "4298228175fa62a36af88b1afd4406cdd8b1bf166621f957d0dbbde45a698fe5",
|
||||
"bytes": 207288
|
||||
},
|
||||
"lib/libxcb.so.1": {
|
||||
"sha256": "7958a0136b121bdc4c708968569ad152a9ed208ab026e2537b1005dde64ca440",
|
||||
"bytes": 162392
|
||||
},
|
||||
"lib/libz.so.1": {
|
||||
"sha256": "86200da370f20476a2507e9097a789b5ef97269b4ca8d5e164ad82dab9d99892",
|
||||
"bytes": 113000
|
||||
},
|
||||
"lib/libzstd.so.1": {
|
||||
"sha256": "0a2128bc10841fb29e76d08d945864dfb0b6a66da5df6df5d8299197439e54bb",
|
||||
"bytes": 755864
|
||||
},
|
||||
"licenses/UPSTREAM-SOURCE.txt": {
|
||||
"sha256": "ff2fbc975e47bffd1e3c488e14f1fbe417c83db82fb5ab41e16a9c1da96edb2a",
|
||||
"bytes": 293
|
||||
},
|
||||
"licenses/VESC-Tool-LICENSE": {
|
||||
"sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986",
|
||||
"bytes": 35149
|
||||
},
|
||||
"licenses/config_export.h": {
|
||||
"sha256": "f0c483aa27b9910bab1bff4f7baf66fe4077461e65a1d3e11169c4a722212756",
|
||||
"bytes": 4724
|
||||
},
|
||||
"licenses/engine_main.cpp": {
|
||||
"sha256": "e4b32dc5c9def71208d8daad830d5c47852f39f85a321713abc45dc15173b4fc",
|
||||
"bytes": 21539
|
||||
},
|
||||
"licenses/gir1.2-glib-2.0.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/gir1.2-gudev-1.0.copyright": {
|
||||
"sha256": "e8208610819fd05fca6ea4776156858bb934e29c408ae9bdac4a8266a53178e3",
|
||||
"bytes": 1056
|
||||
},
|
||||
"licenses/libblkid-dev.copyright": {
|
||||
"sha256": "3492fb92cb56cf517d9a899828b4f989038421a38736e49e5525ffbd33196eab",
|
||||
"bytes": 23160
|
||||
},
|
||||
"licenses/libbrotli-dev.copyright": {
|
||||
"sha256": "24a64e5bb83d0960d1835696a1e23c0896ad6055b0ca47c66ab0eb9a766324b1",
|
||||
"bytes": 1354
|
||||
},
|
||||
"licenses/libdouble-conversion3.copyright": {
|
||||
"sha256": "1cc0b36cdfe5a674e11cb9907a88291c7602d3805bddd334cc07d57231a0cd00",
|
||||
"bytes": 1999
|
||||
},
|
||||
"licenses/libegl-dev.copyright": {
|
||||
"sha256": "37920f219f60efa2fde5d003c51be0608b2159cc74c5eb6d70d630907db2d9a3",
|
||||
"bytes": 4422
|
||||
},
|
||||
"licenses/libevdev-dev.copyright": {
|
||||
"sha256": "01d0da4919f92dd06e7ec73fee6cf5dccac3c6e475917b9dc57cb22800b732de",
|
||||
"bytes": 5460
|
||||
},
|
||||
"licenses/libexpat1-dev.copyright": {
|
||||
"sha256": "60919fe1a156395ff14511bb6ff79756c50ade2fff59ac60c9bae1d8a7fe6292",
|
||||
"bytes": 1756
|
||||
},
|
||||
"licenses/libexpat1.copyright": {
|
||||
"sha256": "60919fe1a156395ff14511bb6ff79756c50ade2fff59ac60c9bae1d8a7fe6292",
|
||||
"bytes": 1756
|
||||
},
|
||||
"licenses/libfontconfig-dev.copyright": {
|
||||
"sha256": "b215a61cdd3e62b5b17cc28b1852c78acb3dd38be0fb30706f7efc050dba91db",
|
||||
"bytes": 1301
|
||||
},
|
||||
"licenses/libfreetype-dev.copyright": {
|
||||
"sha256": "ce6d766883ea111e7f47acc09e9d49be8827daa6a03f2c2707243a425d41f0e9",
|
||||
"bytes": 31209
|
||||
},
|
||||
"licenses/libgirepository-2.0-0.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/libgl-dev.copyright": {
|
||||
"sha256": "37920f219f60efa2fde5d003c51be0608b2159cc74c5eb6d70d630907db2d9a3",
|
||||
"bytes": 4422
|
||||
},
|
||||
"licenses/libglib2.0-0t64.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/libglib2.0-bin.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/libglib2.0-data.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/libglib2.0-dev-bin.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/libglib2.0-dev.copyright": {
|
||||
"sha256": "b52574b109fb876dfa71dea7a98b8aee1bb583f6aa90c327d1e4dc0a1d59f211",
|
||||
"bytes": 54957
|
||||
},
|
||||
"licenses/libglu1-mesa-dev.copyright": {
|
||||
"sha256": "7802232600641c113e2948fbc2feae6de45f26af59a43c28836e0ef846f8dd94",
|
||||
"bytes": 4055
|
||||
},
|
||||
"licenses/libglx-dev.copyright": {
|
||||
"sha256": "37920f219f60efa2fde5d003c51be0608b2159cc74c5eb6d70d630907db2d9a3",
|
||||
"bytes": 4422
|
||||
},
|
||||
"licenses/libgudev-1.0-dev.copyright": {
|
||||
"sha256": "e8208610819fd05fca6ea4776156858bb934e29c408ae9bdac4a8266a53178e3",
|
||||
"bytes": 1056
|
||||
},
|
||||
"licenses/libinput-bin.copyright": {
|
||||
"sha256": "f075af6e7319a471289bc1908aae04e44ba954ea53b11fcf42ee8d5422f40363",
|
||||
"bytes": 2292
|
||||
},
|
||||
"licenses/libinput-dev.copyright": {
|
||||
"sha256": "f075af6e7319a471289bc1908aae04e44ba954ea53b11fcf42ee8d5422f40363",
|
||||
"bytes": 2292
|
||||
},
|
||||
"licenses/libinput10.copyright": {
|
||||
"sha256": "f075af6e7319a471289bc1908aae04e44ba954ea53b11fcf42ee8d5422f40363",
|
||||
"bytes": 2292
|
||||
},
|
||||
"licenses/libmd4c0.copyright": {
|
||||
"sha256": "68e5ce452a6fc2bee44279ca61a7064950d146481e325eeaece6c9bea095fd2f",
|
||||
"bytes": 23273
|
||||
},
|
||||
"licenses/libmount-dev.copyright": {
|
||||
"sha256": "3492fb92cb56cf517d9a899828b4f989038421a38736e49e5525ffbd33196eab",
|
||||
"bytes": 23160
|
||||
},
|
||||
"licenses/libmtdev-dev.copyright": {
|
||||
"sha256": "7ca89f7e6e0ab15b9941aa80def7c50b22a7bd6356dd1eacf28ca4382f72a6de",
|
||||
"bytes": 1628
|
||||
},
|
||||
"licenses/libopengl-dev.copyright": {
|
||||
"sha256": "37920f219f60efa2fde5d003c51be0608b2159cc74c5eb6d70d630907db2d9a3",
|
||||
"bytes": 4422
|
||||
},
|
||||
"licenses/libpcre2-16-0.copyright": {
|
||||
"sha256": "030511beb4d9d620ad09914c369c36ec0528dcf301d1923cc643c948ee7c6a38",
|
||||
"bytes": 6626
|
||||
},
|
||||
"licenses/libpcre2-dev.copyright": {
|
||||
"sha256": "030511beb4d9d620ad09914c369c36ec0528dcf301d1923cc643c948ee7c6a38",
|
||||
"bytes": 6626
|
||||
},
|
||||
"licenses/libpcre2-posix3.copyright": {
|
||||
"sha256": "030511beb4d9d620ad09914c369c36ec0528dcf301d1923cc643c948ee7c6a38",
|
||||
"bytes": 6626
|
||||
},
|
||||
"licenses/libpkgconf3.copyright": {
|
||||
"sha256": "676c75e54ff7e3b9892940fbf1b31763cb6f2a6527a5607a3640ac59a2441fba",
|
||||
"bytes": 7501
|
||||
},
|
||||
"licenses/libpng-dev.copyright": {
|
||||
"sha256": "4620d402b97601a910946acccbbe2e15bdffac11615bf9046520990d3b00f2b9",
|
||||
"bytes": 13051
|
||||
},
|
||||
"licenses/libpthread-stubs0-dev.copyright": {
|
||||
"sha256": "e45b85577d0f6883300ccfb004ab79e1a4f2cf3777b64eb989525115af400b5a",
|
||||
"bytes": 1849
|
||||
},
|
||||
"licenses/libqt5bluetooth5-bin.copyright": {
|
||||
"sha256": "937c1479a92f32fbd29957b9339b35e9edb7c85be3be6543c8c7495ea799933e",
|
||||
"bytes": 7982
|
||||
},
|
||||
"licenses/libqt5bluetooth5.copyright": {
|
||||
"sha256": "937c1479a92f32fbd29957b9339b35e9edb7c85be3be6543c8c7495ea799933e",
|
||||
"bytes": 7982
|
||||
},
|
||||
"licenses/libqt5concurrent5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5core5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5dbus5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5gamepad5-dev.copyright": {
|
||||
"sha256": "eb1b57f89c6b2c7e25472e1e59c796ed6bc7ff5c5eb08845a95fe9653f781c5b",
|
||||
"bytes": 3446
|
||||
},
|
||||
"licenses/libqt5gamepad5.copyright": {
|
||||
"sha256": "eb1b57f89c6b2c7e25472e1e59c796ed6bc7ff5c5eb08845a95fe9653f781c5b",
|
||||
"bytes": 3446
|
||||
},
|
||||
"licenses/libqt5gui5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5network5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5nfc5.copyright": {
|
||||
"sha256": "937c1479a92f32fbd29957b9339b35e9edb7c85be3be6543c8c7495ea799933e",
|
||||
"bytes": 7982
|
||||
},
|
||||
"licenses/libqt5positioning5-plugins.copyright": {
|
||||
"sha256": "2d54a4f412d1a2c1f5138b06a94a6cdd445e267227fb0526fc376ab2e7d9b63f",
|
||||
"bytes": 18782
|
||||
},
|
||||
"licenses/libqt5positioning5.copyright": {
|
||||
"sha256": "2d54a4f412d1a2c1f5138b06a94a6cdd445e267227fb0526fc376ab2e7d9b63f",
|
||||
"bytes": 18782
|
||||
},
|
||||
"licenses/libqt5positioningquick5.copyright": {
|
||||
"sha256": "2d54a4f412d1a2c1f5138b06a94a6cdd445e267227fb0526fc376ab2e7d9b63f",
|
||||
"bytes": 18782
|
||||
},
|
||||
"licenses/libqt5printsupport5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5qml5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5qmlmodels5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5qmlworkerscript5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5quick5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5quickcontrols2-5.copyright": {
|
||||
"sha256": "334854f9488a33d1211ded54478a35071369a4addaf86d2581820025b244ae03",
|
||||
"bytes": 10979
|
||||
},
|
||||
"licenses/libqt5quickparticles5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5quickshapes5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5quicktemplates2-5.copyright": {
|
||||
"sha256": "334854f9488a33d1211ded54478a35071369a4addaf86d2581820025b244ae03",
|
||||
"bytes": 10979
|
||||
},
|
||||
"licenses/libqt5quicktest5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5quickwidgets5.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/libqt5serialport5-dev.copyright": {
|
||||
"sha256": "679e7d434baaf5b3b80e3f7764d6e57a7a971f11693f8d0420f1de5dc7696a6b",
|
||||
"bytes": 6220
|
||||
},
|
||||
"licenses/libqt5serialport5.copyright": {
|
||||
"sha256": "679e7d434baaf5b3b80e3f7764d6e57a7a971f11693f8d0420f1de5dc7696a6b",
|
||||
"bytes": 6220
|
||||
},
|
||||
"licenses/libqt5sql5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5svg5-dev.copyright": {
|
||||
"sha256": "71bc825205f218db925d601e2c4cee8f1cdd36ae07ece4e63736bfb0afe0453f",
|
||||
"bytes": 8476
|
||||
},
|
||||
"licenses/libqt5svg5.copyright": {
|
||||
"sha256": "71bc825205f218db925d601e2c4cee8f1cdd36ae07ece4e63736bfb0afe0453f",
|
||||
"bytes": 8476
|
||||
},
|
||||
"licenses/libqt5test5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5widgets5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libqt5xml5t64.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/libselinux1-dev.copyright": {
|
||||
"sha256": "864f1bb189f609075d580b8c4aada16d85a44546884f1687989e1cacc8c56751",
|
||||
"bytes": 1957
|
||||
},
|
||||
"licenses/libsepol-dev.copyright": {
|
||||
"sha256": "78d2a34606a0302057ec499a3b3c07bcbc43ca334064ce4c80891a0f69c8f6c6",
|
||||
"bytes": 3750
|
||||
},
|
||||
"licenses/libudev-dev.copyright": {
|
||||
"sha256": "a7d06854714a1ca99f6dbd1a1641dde5bcf28635be149f6554449618f8f427f3",
|
||||
"bytes": 12776
|
||||
},
|
||||
"licenses/libvulkan-dev.copyright": {
|
||||
"sha256": "c579213e28f67944a7e407816b8a8e1d2d2406b3d820e420aa923807c450dc07",
|
||||
"bytes": 1964
|
||||
},
|
||||
"licenses/libwacom-dev.copyright": {
|
||||
"sha256": "5026eb61394922e821cfea069fe9740141b1d6abd3ea900e79655ad45ea1cb8a",
|
||||
"bytes": 1624
|
||||
},
|
||||
"licenses/libx11-dev.copyright": {
|
||||
"sha256": "0b380a7fd5b2228f26e9585e56f14812efd3350f3df307507d2bc055dfd8de3e",
|
||||
"bytes": 47102
|
||||
},
|
||||
"licenses/libxau-dev.copyright": {
|
||||
"sha256": "118dd263a7b91c8f21c489f949bf13281dff9e766deea92b829dac4dce66601a",
|
||||
"bytes": 1224
|
||||
},
|
||||
"licenses/libxcb-xinerama0.copyright": {
|
||||
"sha256": "4f7cb9db6bf6542f5417e3d674c780d3a5fd12291a54d63054fb576ee0cfae80",
|
||||
"bytes": 1781
|
||||
},
|
||||
"licenses/libxcb-xinput0.copyright": {
|
||||
"sha256": "4f7cb9db6bf6542f5417e3d674c780d3a5fd12291a54d63054fb576ee0cfae80",
|
||||
"bytes": 1781
|
||||
},
|
||||
"licenses/libxcb1-dev.copyright": {
|
||||
"sha256": "4f7cb9db6bf6542f5417e3d674c780d3a5fd12291a54d63054fb576ee0cfae80",
|
||||
"bytes": 1781
|
||||
},
|
||||
"licenses/libxdmcp-dev.copyright": {
|
||||
"sha256": "1bcbb50f8603fe8b86d330bfa460b772ea04fbd3c70e4499f0768b5340b9fd6e",
|
||||
"bytes": 1265
|
||||
},
|
||||
"licenses/libxext-dev.copyright": {
|
||||
"sha256": "bc57e445ca1d9fe082c8d54189dd411ff26caa8552c9c63d44ea06a982f32124",
|
||||
"bytes": 10421
|
||||
},
|
||||
"licenses/libxkbcommon-dev.copyright": {
|
||||
"sha256": "5eeaeb1b6e029a0274e1573765bb0bae2926ef96a3679203faa4fd00fdaeaa88",
|
||||
"bytes": 3566
|
||||
},
|
||||
"licenses/pkgconf-bin.copyright": {
|
||||
"sha256": "676c75e54ff7e3b9892940fbf1b31763cb6f2a6527a5607a3640ac59a2441fba",
|
||||
"bytes": 7501
|
||||
},
|
||||
"licenses/pkgconf.copyright": {
|
||||
"sha256": "676c75e54ff7e3b9892940fbf1b31763cb6f2a6527a5607a3640ac59a2441fba",
|
||||
"bytes": 7501
|
||||
},
|
||||
"licenses/python3-packaging.copyright": {
|
||||
"sha256": "51fe4bbadf841c4e4d02ad97ba375bcde0ae11a51da62ae16fdcbe723d3cdad2",
|
||||
"bytes": 2444
|
||||
},
|
||||
"licenses/qt5-qmake-bin.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/qt5-qmake.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/qt5-qmltooling-plugins.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/qtbase5-dev-tools.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/qtbase5-dev.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/qtbase5-private-dev.copyright": {
|
||||
"sha256": "4839c591405c069c6daad29f578f0e9d09580d15571b4c1e6bb7bbc0902b8c08",
|
||||
"bytes": 131975
|
||||
},
|
||||
"licenses/qtchooser.copyright": {
|
||||
"sha256": "0b3fa692b33acfbb5b9539335c66938c957350b2a24d2fec35de89e09198738e",
|
||||
"bytes": 5193
|
||||
},
|
||||
"licenses/qtconnectivity5-dev.copyright": {
|
||||
"sha256": "937c1479a92f32fbd29957b9339b35e9edb7c85be3be6543c8c7495ea799933e",
|
||||
"bytes": 7982
|
||||
},
|
||||
"licenses/qtdeclarative5-dev-tools.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/qtdeclarative5-dev.copyright": {
|
||||
"sha256": "5efc270a798372057f7ee88c14cf4a7cb41289607f2a15a5c5aa7d26809eddd0",
|
||||
"bytes": 44601
|
||||
},
|
||||
"licenses/qtpositioning5-dev.copyright": {
|
||||
"sha256": "2d54a4f412d1a2c1f5138b06a94a6cdd445e267227fb0526fc376ab2e7d9b63f",
|
||||
"bytes": 18782
|
||||
},
|
||||
"licenses/qtquickcontrols2-5-dev.copyright": {
|
||||
"sha256": "334854f9488a33d1211ded54478a35071369a4addaf86d2581820025b244ae03",
|
||||
"bytes": 10979
|
||||
},
|
||||
"licenses/uuid-dev.copyright": {
|
||||
"sha256": "3492fb92cb56cf517d9a899828b4f989038421a38736e49e5525ffbd33196eab",
|
||||
"bytes": 23160
|
||||
},
|
||||
"licenses/x11proto-dev.copyright": {
|
||||
"sha256": "7b40446cf2035abc6836c7a7f411ec79153dbf4530bce209e26aa2fd7c4dd55a",
|
||||
"bytes": 3963
|
||||
},
|
||||
"licenses/xorg-sgml-doctools.copyright": {
|
||||
"sha256": "f8f02d5cfd7d4ed0eb6c46deacb3a64c1fa5bc60e06db10ecbf202e6fe1d5a89",
|
||||
"bytes": 2271
|
||||
},
|
||||
"licenses/xtrans-dev.copyright": {
|
||||
"sha256": "29e6f06b1dcd85f1bc4b3e9374b92967cb2a274abde00169784f1dd1c7c95431",
|
||||
"bytes": 6364
|
||||
},
|
||||
"licenses/zlib1g-dev.copyright": {
|
||||
"sha256": "9e5b96d63773a5d177ba264254390f792be07e41748ebd94730981c6cac31cc6",
|
||||
"bytes": 2927
|
||||
},
|
||||
"plugins/platforms/libqoffscreen.so": {
|
||||
"sha256": "f2f19a29e816c7e5c60fd52b9d3c1a214634c38e06cd2ffdf9fc88e9f0c49ffd",
|
||||
"bytes": 193880
|
||||
}
|
||||
},
|
||||
"host_libraries": [
|
||||
"ld-linux-x86-64.so.2",
|
||||
"libc.so.6",
|
||||
"libdl.so.2",
|
||||
"libm.so.6",
|
||||
"libpthread.so.0",
|
||||
"libresolv.so.2",
|
||||
"librt.so.1"
|
||||
],
|
||||
"offline_verified": true,
|
||||
"hardware_qualified": false,
|
||||
"clean_os_qualified": false
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Bundle an admitted native engine and the actual ELF dependency closure.
|
||||
|
||||
Private, installer-owned Qt runtime; never installs packages on the build host.
|
||||
The target is Ubuntu 24.04 amd64. Only its glibc family stays a host prerequisite.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
SYSTEM = {"libc.so.6", "libm.so.6", "libpthread.so.0", "libdl.so.2", "librt.so.1", "libresolv.so.2", "ld-linux-x86-64.so.2"}
|
||||
|
||||
|
||||
def build(engine, sysroot, source, output):
|
||||
output.mkdir(mode=0o700, exist_ok=False)
|
||||
staging = output / "payload"
|
||||
(staging / "bin").mkdir(parents=True)
|
||||
(staging / "lib").mkdir()
|
||||
(staging / "plugins/platforms").mkdir(parents=True)
|
||||
binary = staging / "bin/mission-core-vesc-engine"
|
||||
shutil.copyfile(engine, binary); binary.chmod(0o755)
|
||||
qtlib = sysroot / "usr/lib/x86_64-linux-gnu"
|
||||
plugin = qtlib / "qt5/plugins/platforms/libqoffscreen.so"
|
||||
shutil.copyfile(plugin, staging / "plugins/platforms/libqoffscreen.so")
|
||||
env = dict(os.environ, LD_LIBRARY_PATH=str(qtlib), LC_ALL="C")
|
||||
sources = {}
|
||||
for executable in (engine, plugin):
|
||||
result = subprocess.run(["/usr/bin/ldd", str(executable)], env=env, capture_output=True, text=True, check=True).stdout
|
||||
if "not found" in result: raise RuntimeError("Native runtime dependency missing")
|
||||
for name, path in re.findall(r"^\s*(\S+) => (/[\S]+) \(", result, re.MULTILINE):
|
||||
if name in SYSTEM: continue
|
||||
library = Path(path)
|
||||
if name in sources and sources[name] != library: raise RuntimeError("Conflicting dependency")
|
||||
sources[name] = library
|
||||
for name, library in sources.items(): shutil.copyfile(library, staging / "lib" / name)
|
||||
env.update(LD_LIBRARY_PATH=str(staging / "lib"), QT_PLUGIN_PATH=str(staging / "plugins"),
|
||||
QT_QPA_PLATFORM="offscreen", XDG_CONFIG_HOME=str(output / "config"), XDG_CACHE_HOME=str(output / "cache"))
|
||||
# All non-glibc ELF dependencies must now resolve inside the shipped payload.
|
||||
for executable in (binary, staging / "plugins/platforms/libqoffscreen.so"):
|
||||
result = subprocess.run(["/usr/bin/ldd", str(executable)], env=env, capture_output=True, text=True, check=True).stdout
|
||||
if "not found" in result: raise RuntimeError("Bundled native closure incomplete")
|
||||
for name, path in re.findall(r"^\s*(\S+) => (/[\S]+) \(", result, re.MULTILINE):
|
||||
if name not in SYSTEM and not Path(path).resolve().is_relative_to(staging):
|
||||
raise RuntimeError("Undeclared host dependency: " + name)
|
||||
proc = subprocess.run([str(binary), "--offline"], env=env, input=b'{"id":1,"method":"engine"}\n',
|
||||
capture_output=True, timeout=15, check=True)
|
||||
responses = [json.loads(line) for line in proc.stdout.splitlines()]
|
||||
if len(responses) != 2 or not responses[0]["ready"] or responses[1]["result"]["connected"]:
|
||||
raise RuntimeError("Bundled engine acceptance failed")
|
||||
(output / "offline.stdout").write_bytes(proc.stdout)
|
||||
(output / "offline.stderr").write_bytes(proc.stderr)
|
||||
(staging / "licenses").mkdir()
|
||||
shutil.copyfile(source / "LICENSE", staging / "licenses/VESC-Tool-LICENSE")
|
||||
(staging / "licenses/UPSTREAM-SOURCE.txt").write_text(
|
||||
"VESC Tool 7.00, unmodified upstream sources and resources:\n"
|
||||
"https://github.com/vedderb/vesc_tool/tree/01d5f10901116c311e3fb84d5a1541f663d3ce20\n"
|
||||
"Source archive SHA-256: 4565ecec4e41e094bcc43127a51970e810aa9080a9d7c025af64632bfd4c0189\n"
|
||||
"The process adapter source is included alongside this notice.\n")
|
||||
for name in ("engine_main.cpp", "config_export.h"):
|
||||
shutil.copyfile(engine.parent / name, staging / "licenses" / name)
|
||||
# Preserve dependency notices available in the private signed-package sysroot.
|
||||
for index, path in enumerate(sorted((sysroot / "usr/share/doc").glob("*/copyright"))):
|
||||
shutil.copyfile(path, staging / "licenses" / (path.parent.name + ".copyright"))
|
||||
# Host libraries copied into the closure retain their distribution notices.
|
||||
for library in sources.values():
|
||||
if library.resolve().is_relative_to(sysroot): continue
|
||||
owner = subprocess.run(["dpkg-query", "-S", str(library)], capture_output=True, text=True)
|
||||
if owner.returncode: continue
|
||||
package = owner.stdout.split(": ", 1)[0].split(":", 1)[0]
|
||||
notice = Path("/usr/share/doc") / package / "copyright"
|
||||
if notice.is_file(): shutil.copyfile(notice, staging / "licenses" / (package + ".copyright"))
|
||||
metadata = {str(p.relative_to(staging)): {"sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "bytes":p.stat().st_size}
|
||||
for p in sorted(staging.rglob("*")) if p.is_file()}
|
||||
archive = output / "mission-core-vesc-native-runtime.tar.gz"
|
||||
with tarfile.open(archive, "w:gz") as stream:
|
||||
for path in sorted(staging.rglob("*")):
|
||||
if path.is_file(): stream.add(path, arcname=str(path.relative_to(staging)))
|
||||
report = {"schema":"missioncore.vesc.native-runtime/v1", "upstream_version":"7.00",
|
||||
"upstream_commit":"01d5f10901116c311e3fb84d5a1541f663d3ce20", "os":"ubuntu-24.04-amd64",
|
||||
"file":archive.name, "bytes":archive.stat().st_size, "sha256":hashlib.sha256(archive.read_bytes()).hexdigest(),
|
||||
"engine_sha256":hashlib.sha256(binary.read_bytes()).hexdigest(), "files":metadata,
|
||||
"host_libraries":sorted(SYSTEM), "offline_verified":True, "hardware_qualified":False,
|
||||
"clean_os_qualified":False}
|
||||
(output / "bundle.json").write_text(json.dumps(report,indent=2)+"\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser=argparse.ArgumentParser()
|
||||
for name in ("engine","sysroot","source","output"):parser.add_argument("--"+name,type=Path,required=True)
|
||||
args=parser.parse_args();build(args.engine,args.sysroot,args.source,args.output)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Installer acceptance under the service account; never opens a USB device."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def check(root):
|
||||
manifest = json.loads((root / "manifest.json").read_text())
|
||||
for name, expected in manifest["files"].items():
|
||||
path = root / name
|
||||
if path.is_symlink() or not path.resolve().is_relative_to(root.resolve()):
|
||||
raise RuntimeError("Untrusted native runtime path")
|
||||
if path.stat().st_size != expected["bytes"] or hashlib.sha256(path.read_bytes()).hexdigest() != expected["sha256"]:
|
||||
raise RuntimeError("Native runtime integrity check failed")
|
||||
with tempfile.TemporaryDirectory(prefix="mission-core-vesc-check-") as temporary:
|
||||
env = {"PATH":"/usr/bin:/bin", "LANG":"C.UTF-8", "QT_QPA_PLATFORM":"offscreen",
|
||||
"LD_LIBRARY_PATH":str(root / "lib"), "QT_PLUGIN_PATH":str(root / "plugins"),
|
||||
"XDG_CONFIG_HOME":temporary, "XDG_CACHE_HOME":temporary}
|
||||
result = subprocess.run([str(root / "bin/mission-core-vesc-engine"), "--offline"],
|
||||
env=env, input=b'{"id":1,"method":"engine"}\n', capture_output=True, timeout=15, check=True)
|
||||
replies = [json.loads(line) for line in result.stdout.splitlines()]
|
||||
if (len(replies) != 2 or not replies[0]["ready"] or not replies[1]["ok"]
|
||||
or replies[1]["result"]["hardware_enabled"] or replies[1]["result"]["connected"]
|
||||
or replies[1]["result"]["commit"] != manifest["upstream_commit"]):
|
||||
raise RuntimeError("Native engine offline check failed")
|
||||
print(json.dumps({"ok":True, "upstream_commit":manifest["upstream_commit"], "hardware_access":False}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.geteuid() == 0: raise RuntimeError("Run as the VESC service account")
|
||||
check(Path(__file__).resolve().parent / "native")
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Versioned offline qualification artifact, not a runtime installer.
|
||||
|
||||
Reuse an attested upstream Tool build's object files unchanged. Only the adapter
|
||||
entry point is compiled. The previous staging, packages and services are untouched.
|
||||
All inputs, link objects, outputs and checks are hashed in the private report.
|
||||
"""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
COMMIT = "01d5f10901116c311e3fb84d5a1541f663d3ce20"
|
||||
UPSTREAM_SHA256 = "4565ecec4e41e094bcc43127a51970e810aa9080a9d7c025af64632bfd4c0189"
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def run(args):
|
||||
release = platform.freedesktop_os_release()
|
||||
if os.geteuid() == 0 or (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != ("ubuntu", "24.04", "x86_64"):
|
||||
raise RuntimeError("Unprivileged Ubuntu 24.04 amd64 required")
|
||||
group = Path("/sys/fs/cgroup") / Path("/proc/self/cgroup").read_text().strip().split("::", 1)[1].lstrip("/")
|
||||
limit = (group / "memory.max").read_text().strip()
|
||||
if limit == "max" or int(limit) > 3 * 1024**3:
|
||||
raise RuntimeError("A bounded user scope with MemoryMax <= 3G is required")
|
||||
previous = json.loads(args.upstream_report.read_text())
|
||||
if previous.get("state") != "complete" or previous.get("source_sha256") != UPSTREAM_SHA256:
|
||||
raise RuntimeError("Unqualified upstream build")
|
||||
upstream = Path(previous["binary"])
|
||||
if digest(upstream) != previous["binary_sha256"]:
|
||||
raise RuntimeError("Upstream binary changed")
|
||||
source = upstream.parents[2]
|
||||
if source.name != "vesc_tool-" + COMMIT:
|
||||
raise RuntimeError("Upstream source path mismatch")
|
||||
os.umask(0o077)
|
||||
root = args.output.resolve()
|
||||
root.mkdir(parents=True, mode=0o700, exist_ok=False)
|
||||
# Paths enter a generated makefile, never a shell command assembled from JSON.
|
||||
if any(not re.fullmatch(r"[A-Za-z0-9_./-]+", str(p)) for p in (root, source)):
|
||||
raise RuntimeError("Build paths must be make-safe")
|
||||
with zipfile.ZipFile(args.artifact) as bundle:
|
||||
for name in ("offline_main.cpp", "config_export.h", "engine_main.cpp", "native_bundle.py"):
|
||||
(root / name).write_bytes(bundle.read(name))
|
||||
report = {"schema": "missioncore.vesc.native-probe/v1", "state": "running",
|
||||
"started_at": datetime.now(timezone.utc).isoformat(), "monotonic_started": time.monotonic(),
|
||||
"source_commit": COMMIT, "artifact_sha256": digest(args.artifact),
|
||||
"upstream_report_sha256": digest(args.upstream_report),
|
||||
"adapter_sha256": digest(root / "offline_main.cpp"),
|
||||
"hardware_access": False, "runtime_installed": False, "system_packages_installed": False,
|
||||
"jobs": [], "checks": []}
|
||||
|
||||
def publish():
|
||||
(root / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
|
||||
staging = source.parents[1]
|
||||
qtbase = staging / "sysroot/usr"
|
||||
env = dict(os.environ, LC_ALL="C", QT_QPA_PLATFORM="offscreen",
|
||||
LD_LIBRARY_PATH=str(qtbase / "lib/x86_64-linux-gnu"),
|
||||
QT_PLUGIN_PATH=str(qtbase / "lib/x86_64-linux-gnu/qt5/plugins"),
|
||||
XDG_CONFIG_HOME=str(root / "config"), XDG_CACHE_HOME=str(root / "cache"))
|
||||
|
||||
def execute(name, command, data=None, expected=0, timeout=60):
|
||||
start = time.monotonic()
|
||||
proc = subprocess.run(command, cwd=source, env=env, input=data,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
|
||||
(root / (name + ".stdout")).write_bytes(proc.stdout)
|
||||
(root / (name + ".stderr")).write_bytes(proc.stderr)
|
||||
report["jobs"].append({"id": name, "exit_code": proc.returncode,
|
||||
"duration_seconds": time.monotonic() - start,
|
||||
"stdout_sha256": hashlib.sha256(proc.stdout).hexdigest(),
|
||||
"stderr_sha256": hashlib.sha256(proc.stderr).hexdigest()})
|
||||
publish()
|
||||
if proc.returncode != expected:
|
||||
raise RuntimeError("Native probe step failed: " + name)
|
||||
return proc.stdout
|
||||
|
||||
try:
|
||||
makefile = (source / "Makefile").read_text().replace("\\\n", " ")
|
||||
match = re.search(r"^OBJECTS\s*=\s*(.+)$", makefile, re.MULTILINE)
|
||||
if not match:
|
||||
raise RuntimeError("Upstream link objects missing")
|
||||
objects = [source / name for name in match.group(1).split() if name != "build/lin/obj/main.o"]
|
||||
if not 100 < len(objects) < 1000 or any(not p.is_file() for p in objects):
|
||||
raise RuntimeError("Upstream object inventory incomplete")
|
||||
report["link_objects"] = [{"file": str(p.relative_to(source)), "sha256": digest(p)} for p in objects]
|
||||
report["upstream_makefile_sha256"] = digest(source / "Makefile")
|
||||
target = root / "mission-core-vesc-offline"
|
||||
wrapper = root / "Makefile.native"
|
||||
wrapper.write_text(
|
||||
"include " + str(source / "Makefile") + "\n"
|
||||
".PHONY: mission-core-native-probe\n"
|
||||
"mission-core-native-probe:\n"
|
||||
"\t$(CXX) -c $(CXXFLAGS) $(INCPATH) -o " + str(root / "offline_main.o") + " " + str(root / "offline_main.cpp") + "\n"
|
||||
"\t$(LINK) $(LFLAGS) -o " + str(target) + " " + str(root / "offline_main.o") +
|
||||
" $(filter-out build/lin/obj/main.o,$(OBJECTS)) $(OBJCOMP) $(LIBS)\n")
|
||||
execute("compile-link", ["/usr/bin/make", "-f", str(wrapper), "mission-core-native-probe"], timeout=180)
|
||||
report["binary_sha256"] = digest(target)
|
||||
for index, archive in enumerate(args.archives):
|
||||
raw = archive.read_bytes()
|
||||
native = json.loads(execute("archive-%d" % index, [str(target)], raw))
|
||||
if not native["ok"] or not native["compatibility"]["legacy_power_loss_correction"]:
|
||||
raise RuntimeError("Upstream compatibility check failed")
|
||||
import base64
|
||||
packet = base64.b64decode(native["compatibility"]["offline_detect_example_base64"])
|
||||
if packet[:2] != bytes([58, 0]) or int.from_bytes(packet[2:6], "big", signed=True) != 50000:
|
||||
raise RuntimeError("Expected native 5.02 detect correction was not applied")
|
||||
rpm_packets = [base64.b64decode(p) for p in native["offline_signed_rpm_examples_base64"]]
|
||||
if (len(rpm_packets) != 2 or any(len(p) != 5 or p[0] != 8 for p in rpm_packets)
|
||||
or [int.from_bytes(p[1:], "big", signed=True) for p in rpm_packets] != [3000, -3000]):
|
||||
raise RuntimeError("Upstream signed RPM serialization mismatch")
|
||||
report["checks"].append({"id": "signed-rpm-serialization-%d" % index, "ok": True})
|
||||
report["checks"].append({"id": "native-archive-%d" % index, "ok": True,
|
||||
"archive_sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"motor_parameter_count": len(native["motor"]["parameters"]),
|
||||
"application_parameter_count": len(native["application"]["parameters"]),
|
||||
"binary_round_trip_exact": True,
|
||||
"xml_equivalent_by_upstream_comparison": True,
|
||||
"xml_binary_exact": all(native[k]["xml_round_trip_exact"] for k in ("motor", "application")),
|
||||
"legacy_power_loss_correction": True})
|
||||
# Fail closed on archive corruption and unsupported firmware instead
|
||||
# of silently presenting the bundled defaults as actual settings.
|
||||
original = json.loads(raw)
|
||||
damaged = json.loads(raw); damaged["configs"]["motor"]["sha256"] = "0" * 64
|
||||
unknown = json.loads(raw); unknown["identity"]["major"] = 99
|
||||
wrong_signature = json.loads(raw)
|
||||
payload = bytearray(base64.b64decode(original["configs"]["motor"]["payload"])); payload[1] ^= 1
|
||||
wrong_signature["configs"]["motor"]["payload"] = base64.b64encode(payload).decode()
|
||||
wrong_signature["configs"]["motor"]["sha256"] = hashlib.sha256(payload).hexdigest()
|
||||
truncated = json.loads(raw)
|
||||
payload = base64.b64decode(original["configs"]["motor"]["payload"])[:-1]
|
||||
truncated["configs"]["motor"].update(payload=base64.b64encode(payload).decode(),
|
||||
sha256=hashlib.sha256(payload).hexdigest(), bytes=len(payload))
|
||||
for name, value in (("corrupt", damaged), ("unsupported", unknown), ("signature", wrong_signature), ("truncated", truncated)):
|
||||
rejected = json.loads(execute("%s-%d" % (name, index), [str(target)], json.dumps(value).encode(), expected=1))
|
||||
if rejected["ok"] or rejected["hardware_access"]:
|
||||
raise RuntimeError("Invalid archive was not rejected")
|
||||
report["checks"].append({"id": "%s-%d" % (name, index), "ok": True})
|
||||
for item in report["link_objects"]:
|
||||
if digest(source / item["file"]) != item["sha256"]:
|
||||
raise RuntimeError("Upstream objects were modified")
|
||||
engine = root / "mission-core-vesc-engine"
|
||||
wrapper.write_text(wrapper.read_text() +
|
||||
"\n.PHONY: mission-core-native-engine\nmission-core-native-engine:\n"
|
||||
"\t$(CXX) -c $(CXXFLAGS) $(INCPATH) -o " + str(root / "engine_main.o") + " " + str(root / "engine_main.cpp") + "\n"
|
||||
"\t$(LINK) $(LFLAGS) -o " + str(engine) + " " + str(root / "engine_main.o") +
|
||||
" $(filter-out build/lin/obj/main.o,$(OBJECTS)) $(OBJCOMP) $(LIBS)\n")
|
||||
execute("engine-compile", ["/usr/bin/make", "-f", str(wrapper), "mission-core-native-engine"], timeout=180)
|
||||
requests = [{"id": 1, "method": "engine"}, {"id": 2, "method": "current", "current_a": 30},
|
||||
{"id": 3, "method": "hall_start", "current_a": 5}, {"id": 4, "method": "arbitrary_packet"}]
|
||||
responses = [json.loads(line) for line in execute("engine-offline", [str(engine), "--offline"],
|
||||
b"".join(json.dumps(r).encode()+b"\n" for r in requests)).splitlines()]
|
||||
if (len(responses) != 5 or not responses[0]["ready"] or not responses[1]["ok"]
|
||||
or any(r["ok"] for r in responses[2:]) or responses[1]["result"]["hardware_enabled"]):
|
||||
raise RuntimeError("Native engine offline boundary failed")
|
||||
report["checks"].append({"id": "engine-offline-denies-hardware", "ok": True})
|
||||
execute("runtime-bundle", ["/usr/bin/python3", str(root / "native_bundle.py"),
|
||||
"--engine", str(engine), "--sysroot", str(staging / "sysroot"), "--source", str(source), "--output", str(root / "runtime")], timeout=180)
|
||||
report["native_runtime"] = json.loads((root / "runtime/bundle.json").read_text())
|
||||
report.update(state="complete", binary=str(target), upstream_objects_unchanged=True)
|
||||
except Exception as error:
|
||||
report.update(state="error", error=str(error))
|
||||
raise
|
||||
finally:
|
||||
report["finished_at"] = datetime.now(timezone.utc).isoformat()
|
||||
report["duration_seconds"] = time.monotonic() - report["monotonic_started"]
|
||||
publish()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--artifact", type=Path, required=True)
|
||||
parser.add_argument("--upstream-report", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--archives", type=Path, nargs="+", required=True)
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""The Node release carries this model profile from the first hardware use."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
import hashlib
|
||||
import json
|
||||
import tarfile
|
||||
|
||||
|
||||
def payload():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
files = [("usr/lib/mission-core-vesc/runtime/" + p.name, p.read_bytes(), 0o644)
|
||||
for p in sorted((root / "runtime").glob("*.py"))]
|
||||
files.extend(("usr/lib/mission-core-vesc/runtime/" + str(p.relative_to(root / "runtime")), p.read_bytes(), 0o644)
|
||||
for p in sorted((root / "runtime/schemas").rglob("*")) if p.is_file())
|
||||
files.append(("usr/lib/mission-core-vesc/runtime/archive.py",
|
||||
(root.parents[1] / "src/k1link/device_plugins/vesc/archive.py").read_bytes(), 0o644))
|
||||
for name in ("mission-core-vesc.service", "mission-core-node-vesc-prepare.service"):
|
||||
files.append(("usr/lib/systemd/system/" + name, (root / "packaging" / name).read_bytes(), 0o644))
|
||||
files.append(("usr/lib/mission-core-vesc/prepare.py", (root / "packaging/prepare.py").read_bytes(), 0o644))
|
||||
files.append(("usr/lib/mission-core-vesc/clear_runtime_cache.py", (root / "packaging/clear_runtime_cache.py").read_bytes(), 0o644))
|
||||
files.append(("usr/share/mission-core-node/profiles/vesc/70-mission-core-vesc.rules",
|
||||
(root / "packaging/70-mission-core-vesc.rules").read_bytes(), 0o644))
|
||||
manifest = json.loads((root / "packaging/native-runtime.json").read_text())
|
||||
bundle = root / "build/native-runtime" / manifest["file"]
|
||||
data = bundle.read_bytes()
|
||||
if len(data) != manifest["bytes"] or hashlib.sha256(data).hexdigest() != manifest["sha256"]:
|
||||
raise ValueError("Native VESC Tool bundle changed")
|
||||
seen = set()
|
||||
with tarfile.open(bundle, "r:gz") as archive:
|
||||
for member in archive.getmembers():
|
||||
path = PurePosixPath(member.name)
|
||||
if (not member.isfile() or path.is_absolute() or ".." in path.parts
|
||||
or path.as_posix() != member.name or member.name in seen
|
||||
or member.name not in manifest["files"]):
|
||||
raise ValueError("Unexpected native payload member")
|
||||
seen.add(member.name)
|
||||
expected = manifest["files"][member.name]
|
||||
if member.size != expected["bytes"] or member.size > 128 * 1024**2:
|
||||
raise ValueError("Native member size mismatch")
|
||||
content = archive.extractfile(member).read()
|
||||
if hashlib.sha256(content).hexdigest() != expected["sha256"]:
|
||||
raise ValueError("Native member hash mismatch")
|
||||
mode = 0o755 if member.name.startswith("bin/") else 0o644
|
||||
files.append(("usr/lib/mission-core-vesc/native/" + member.name, content, mode))
|
||||
if seen != set(manifest["files"]): raise ValueError("Incomplete native payload")
|
||||
files.append(("usr/lib/mission-core-vesc/native/manifest.json",
|
||||
(root / "packaging/native-runtime.json").read_bytes(), 0o644))
|
||||
files.append(("usr/lib/mission-core-vesc/native_check.py",
|
||||
(root / "packaging/native_check.py").read_bytes(), 0o644))
|
||||
return files
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Versioned, idempotent VESC profile; no packages downloaded and no motor I/O."""
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from runtime.serial import discover
|
||||
from runtime.service import atomic
|
||||
|
||||
STATE = Path("/var/lib/mission-core-node-profiles/vesc")
|
||||
|
||||
|
||||
def prepare():
|
||||
if os.geteuid() != 0 or sys.argv[1:]:
|
||||
raise RuntimeError("Fixed system profile only")
|
||||
release = platform.freedesktop_os_release()
|
||||
if (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != ("ubuntu", "24.04", "x86_64"):
|
||||
raise RuntimeError("Ubuntu 24.04 amd64 required")
|
||||
STATE.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
for directory in (STATE.parent, STATE):
|
||||
info = directory.lstat()
|
||||
if directory.is_symlink() or info.st_uid != 0 or info.st_mode & 0o022:
|
||||
raise RuntimeError("Untrusted profile state")
|
||||
lock = os.open(STATE / "prepare.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
report = {"schema": "missioncore.node.device-preparation/v1", "model_id": "vesc.controller",
|
||||
"version": "0.7.4", "run_id": uuid.uuid4().hex, "started_at": time.time(),
|
||||
"monotonic_started": time.monotonic(), "state": "running", "steps": []}
|
||||
|
||||
def publish():
|
||||
atomic(STATE / "preparation.json", report)
|
||||
os.chmod(STATE / "preparation.json", 0o644)
|
||||
|
||||
def run(name, label, args):
|
||||
step = {"id": name, "label": label, "state": "running"}
|
||||
report["steps"].append(step)
|
||||
publish()
|
||||
result = subprocess.run(args, capture_output=True, timeout=45,
|
||||
env={"PATH": "/usr/sbin:/usr/bin:/sbin:/bin", "LANG": "C.UTF-8"})
|
||||
step["state"] = "complete" if result.returncode == 0 else "error"
|
||||
publish()
|
||||
if result.returncode:
|
||||
raise RuntimeError("Не завершён этап: " + label)
|
||||
|
||||
try:
|
||||
import pwd
|
||||
try:
|
||||
pwd.getpwnam("mission-core-vesc")
|
||||
except KeyError:
|
||||
run("account", "Подготовка доступа", ["/usr/sbin/adduser", "--system", "--group", "--home",
|
||||
"/var/lib/mission-core-vesc", "--no-create-home", "--disabled-login", "mission-core-vesc"])
|
||||
run("native", "Проверка VESC Tool", ["/usr/sbin/runuser", "-u", "mission-core-vesc", "--",
|
||||
"/usr/bin/python3", "-I", "/usr/lib/mission-core-vesc/native_check.py"])
|
||||
source = Path("/usr/share/mission-core-node/profiles/vesc/70-mission-core-vesc.rules")
|
||||
rules = source.read_bytes()
|
||||
report["udev_sha256"] = hashlib.sha256(rules).hexdigest()
|
||||
target = Path("/etc/udev/rules.d/70-mission-core-vesc.rules")
|
||||
if target.is_symlink():
|
||||
raise RuntimeError("Untrusted udev destination")
|
||||
target.write_bytes(rules)
|
||||
target.chmod(0o644)
|
||||
run("rules", "Настройка USB-доступа", ["/usr/bin/udevadm", "control", "--reload-rules"])
|
||||
for i, device in enumerate(discover()):
|
||||
run("usb" + str(i), "Применение USB-доступа", ["/usr/bin/udevadm", "trigger", "--action=change",
|
||||
"/sys/class/tty/" + device.tty])
|
||||
run("settle", "Проверка USB-доступа", ["/usr/bin/udevadm", "settle", "--timeout=10"])
|
||||
run("enable", "Подготовка службы", ["/usr/bin/systemctl", "enable", "mission-core-vesc.service"])
|
||||
run("runtime", "Запуск чтения контроллеров", ["/usr/bin/systemctl", "restart", "mission-core-vesc.service"])
|
||||
report["state"] = "complete"
|
||||
except (OSError, RuntimeError, subprocess.SubprocessError) as error:
|
||||
report.update(state="error", message=str(error)[:300])
|
||||
finally:
|
||||
report["duration_seconds"] = time.monotonic() - report["monotonic_started"]
|
||||
publish()
|
||||
os.close(lock)
|
||||
return report["state"] == "complete"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(0 if prepare() else 1)
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Unprivileged VESC Tool build spike; never installs packages or opens a device.
|
||||
|
||||
Run in a bounded user systemd scope on Ubuntu 24.04 amd64. APT resolves and
|
||||
downloads signed Ubuntu packages into this job; dpkg-deb only extracts them.
|
||||
The output is an engineering build, not an installed or qualified runtime.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
COMMIT = "01d5f10901116c311e3fb84d5a1541f663d3ce20"
|
||||
ARCHIVE_SHA256 = "4565ecec4e41e094bcc43127a51970e810aa9080a9d7c025af64632bfd4c0189"
|
||||
DEPS = (
|
||||
"qtbase5-dev", "qtbase5-private-dev", "qtdeclarative5-dev",
|
||||
"qtquickcontrols2-5-dev", "libqt5serialport5-dev", "qtconnectivity5-dev",
|
||||
"qtpositioning5-dev", "libqt5gamepad5-dev", "libqt5svg5-dev",
|
||||
)
|
||||
|
||||
|
||||
def build(archive, root, dependency_cache=None, resume=None):
|
||||
if os.geteuid() == 0:
|
||||
raise RuntimeError("This build must not run as root")
|
||||
release = platform.freedesktop_os_release()
|
||||
if (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != ("ubuntu", "24.04", "x86_64"):
|
||||
raise RuntimeError("Ubuntu 24.04 amd64 required")
|
||||
assert hashlib.sha256(archive.read_bytes()).hexdigest() == ARCHIVE_SHA256
|
||||
group = Path("/sys/fs/cgroup") / Path("/proc/self/cgroup").read_text().strip().split("::", 1)[1].lstrip("/")
|
||||
limit = (group / "memory.max").read_text().strip()
|
||||
if limit == "max" or int(limit) > 3 * 1024**3:
|
||||
raise RuntimeError("Run in a user scope with MemoryMax=3G")
|
||||
os.umask(0o077)
|
||||
root.mkdir(mode=0o700, parents=True, exist_ok=False)
|
||||
root = root.resolve()
|
||||
report = {"schema": "missioncore.vesc.tool-build/v1", "source_commit": COMMIT,
|
||||
"source_sha256": ARCHIVE_SHA256, "state": "running", "jobs": [],
|
||||
"started_at": datetime.now(timezone.utc).isoformat(), "monotonic_started": time.monotonic(),
|
||||
"hardware_access": False, "system_packages_installed": False, "packages": []}
|
||||
env = dict(os.environ, LC_ALL="C", DEBIAN_FRONTEND="noninteractive", QT_QPA_PLATFORM="offscreen")
|
||||
|
||||
def publish():
|
||||
(root / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
|
||||
def run(name, args, cwd=None, timeout=600):
|
||||
job = {"id": name, "state": "running"}; report["jobs"].append(job); publish()
|
||||
started = time.monotonic()
|
||||
with (root / (name + ".stdout")).open("wb") as out, (root / (name + ".stderr")).open("wb") as err:
|
||||
result = subprocess.run(args, cwd=cwd or root, env=env, stdout=out, stderr=err, timeout=timeout)
|
||||
job.update(state="complete" if result.returncode == 0 else "error", exit_code=result.returncode,
|
||||
duration_seconds=time.monotonic() - started)
|
||||
publish()
|
||||
if result.returncode:
|
||||
raise RuntimeError("Build step failed: " + name)
|
||||
return (root / (name + ".stdout")).read_text()
|
||||
|
||||
try:
|
||||
staging = root
|
||||
if resume is not None:
|
||||
previous_raw = (resume / "report.json").read_bytes()
|
||||
previous = json.loads(previous_raw)
|
||||
if previous.get("source_sha256") != ARCHIVE_SHA256 or previous.get("error") != "Build step failed: compile":
|
||||
raise RuntimeError("Only this source's failed compile may resume")
|
||||
staging = resume.resolve()
|
||||
report["resume_report_sha256"] = hashlib.sha256(previous_raw).hexdigest()
|
||||
report["resumed_staging"] = str(staging)
|
||||
downloads = staging / "packages"
|
||||
if resume is None: downloads.mkdir()
|
||||
sysroot = staging / "sysroot"
|
||||
if resume is None: sysroot.mkdir()
|
||||
if resume is not None:
|
||||
report["packages"] = previous["packages"]
|
||||
for item in report["packages"]:
|
||||
if hashlib.sha256((downloads / item["file"]).read_bytes()).hexdigest() != item["sha256"]:
|
||||
raise RuntimeError("Resumed dependency changed")
|
||||
elif dependency_cache is not None:
|
||||
previous = json.loads((dependency_cache / "report.json").read_text())
|
||||
if previous.get("source_sha256") != ARCHIVE_SHA256 or not previous.get("packages"):
|
||||
raise RuntimeError("Unqualified dependency cache")
|
||||
for item in previous["packages"]:
|
||||
name = item["file"]
|
||||
if Path(name).name != name or not name.endswith(".deb"):
|
||||
raise RuntimeError("Invalid cached package name")
|
||||
package = dependency_cache / "packages" / name
|
||||
if hashlib.sha256(package.read_bytes()).hexdigest() != item["sha256"]:
|
||||
raise RuntimeError("Cached dependency changed")
|
||||
shutil.copyfile(package, downloads / name)
|
||||
report["dependency_cache_report_sha256"] = hashlib.sha256((dependency_cache / "report.json").read_bytes()).hexdigest()
|
||||
else:
|
||||
# Host lists may refer to superseded security packages. Refresh only
|
||||
# this job's signed Ubuntu indexes; never update /var/lib/apt or invoke
|
||||
# the host's update hooks (Timescale and other sources are irrelevant).
|
||||
aptdir = root / "apt"; aptdir.mkdir()
|
||||
for name in ("lists", "lists/partial", "archives", "archives/partial"):
|
||||
(aptdir / name).mkdir(exist_ok=True)
|
||||
sources = aptdir / "sources.list"
|
||||
sources.write_text("".join(
|
||||
"deb [signed-by=/usr/share/keyrings/ubuntu-archive-keyring.gpg] " + url + " " + suite + " main universe\n"
|
||||
for url, suite in (("https://archive.ubuntu.com/ubuntu", "noble"),
|
||||
("https://archive.ubuntu.com/ubuntu", "noble-updates"),
|
||||
("https://security.ubuntu.com/ubuntu", "noble-security"))))
|
||||
config = aptdir / "apt.conf"
|
||||
config.write_text(
|
||||
'Dir::Etc::Parts "-";\nDir::Etc::main "-";\n'
|
||||
'Dir::Etc::sourceparts "-";\nDir::Etc::sourcelist "' + str(sources) + '";\n'
|
||||
'Dir::State::lists "' + str(aptdir / "lists") + '";\n'
|
||||
'Dir::Cache::archives "' + str(aptdir / "archives") + '";\n'
|
||||
'Dir::Cache::pkgcache "";\nDir::Cache::srcpkgcache "";\n'
|
||||
'Acquire::Languages "none";\nDebug::NoLocking "true";\n'
|
||||
'#clear APT::Update::Post-Invoke;\n#clear APT::Update::Post-Invoke-Success;\n')
|
||||
env["APT_CONFIG"] = str(config)
|
||||
apt = ["/usr/bin/apt-get"]
|
||||
run("private-indexes", [*apt, "update"])
|
||||
plan = run("dependencies-plan", [*apt, "--simulate", "--no-install-recommends", "--no-remove", "install", *DEPS])
|
||||
packages = re.findall(r"^Inst (\S+)(?: \[[^\]]+\])? \((\S+)", plan, re.MULTILINE)
|
||||
if not packages or len(packages) > 150:
|
||||
raise RuntimeError("Unexpected dependency plan; inspect before changing profile")
|
||||
for index, (name, version) in enumerate(packages):
|
||||
# apt-get download verifies the archive against the host's trusted
|
||||
# repository metadata. No maintainer script or package install runs.
|
||||
run("download-%03d" % index, [*apt, "download", name + "=" + version], downloads)
|
||||
for index, package in enumerate(sorted(downloads.glob("*.deb")) if resume is None else []):
|
||||
report["packages"].append({"file": package.name, "sha256": hashlib.sha256(package.read_bytes()).hexdigest()})
|
||||
run("extract-%03d" % index, ["/usr/bin/dpkg-deb", "--extract", str(package), str(sysroot)])
|
||||
source = staging / "source"
|
||||
if resume is None:
|
||||
source.mkdir()
|
||||
with tarfile.open(archive) as stream:
|
||||
stream.extractall(source, filter="data")
|
||||
source = source / ("vesc_tool-" + COMMIT)
|
||||
qtbase = sysroot / "usr"
|
||||
# APT omits already-installed runtime packages. Complete the private
|
||||
# development symlinks from declared host libraries, recording provenance.
|
||||
report["host_libraries"] = []
|
||||
for name in ("libGL.so.1", "libGLX.so.0", "libGLU.so.1"):
|
||||
target = qtbase / "lib/x86_64-linux-gnu" / name
|
||||
host = Path("/usr/lib/x86_64-linux-gnu") / name
|
||||
if not target.exists() and host.exists():
|
||||
shutil.copyfile(host, target)
|
||||
report["host_libraries"].append({"source": str(host.resolve()), "sha256": hashlib.sha256(host.read_bytes()).hexdigest()})
|
||||
qtarch = qtbase / "lib/x86_64-linux-gnu/qt5"
|
||||
qtbin = qtbase / "lib/qt5/bin"
|
||||
qtconfig = "[Paths]\nPrefix=" + str(qtbase) + "\n" + "\n".join(
|
||||
name + "=" + str(path) for name, path in {
|
||||
"Headers": qtbase / "include/x86_64-linux-gnu/qt5",
|
||||
"Libraries": qtbase / "lib/x86_64-linux-gnu", "ArchData": qtarch,
|
||||
"HostData": qtarch, "Binaries": qtbin, "HostBinaries": qtbin,
|
||||
"Plugins": qtarch / "plugins", "Qml2Imports": qtarch / "qml",
|
||||
"Data": qtbase / "share/qt5",
|
||||
}.items()) + "\n"
|
||||
(qtbin / "qt.conf").write_text(qtconfig)
|
||||
env["LD_LIBRARY_PATH"] = str(qtbase / "lib/x86_64-linux-gnu")
|
||||
env["QT_PLUGIN_PATH"] = str(qtarch / "plugins")
|
||||
env["PKG_CONFIG_LIBDIR"] = str(qtbase / "lib/x86_64-linux-gnu/pkgconfig")
|
||||
env["PKG_CONFIG_SYSROOT_DIR"] = str(sysroot)
|
||||
run("qmake", [str(qtbin / "qmake"), "-config", "release", "CONFIG += release_lin build_original exclude_fw",
|
||||
"VT_GIT_COMMIT=" + COMMIT[:8], "INCLUDEPATH += " + str(qtbase / "include") + " " + str(qtbase / "include/x86_64-linux-gnu"),
|
||||
"QMAKE_LIBDIR += " + str(qtbase / "lib/x86_64-linux-gnu")], source)
|
||||
run("compile", ["/usr/bin/make", "-j2"], source, timeout=1800)
|
||||
binary = source / "build/lin/vesc_tool_7.00"
|
||||
result = run("version", [str(binary), "--version"], source, timeout=30)
|
||||
report.update(state="complete", binary=str(binary), binary_sha256=hashlib.sha256(binary.read_bytes()).hexdigest(),
|
||||
version_output=result, runtime_installed=False, hardware_qualified=False)
|
||||
except Exception as error:
|
||||
report.update(state="error", error=str(error))
|
||||
raise
|
||||
finally:
|
||||
report["duration_seconds"] = time.monotonic() - report["monotonic_started"]
|
||||
report["finished_at"] = datetime.now(timezone.utc).isoformat()
|
||||
publish()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--dependencies", type=Path)
|
||||
parser.add_argument("--resume", type=Path)
|
||||
args = parser.parse_args()
|
||||
build(args.source.resolve(), args.output, args.dependencies, args.resume)
|
||||
Reference in New Issue
Block a user