feat(fleet): preserve operator VESC integration before final driver merge

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:40:46 +03:00
parent dad11b47d7
commit 71a648fec8
128 changed files with 22390 additions and 59 deletions
@@ -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
+676
View File
@@ -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": 50352726,
"sha256": "cc78c273e025f0738ed79f94d4bc6a70e504f2e7204b1ef694119e6a230b913a",
"engine_sha256": "9d2d6a87032e3c60f2666cadd5d5d1520edac05087e0dcb7a5ac3875f2ace325",
"files": {
"bin/mission-core-vesc-engine": {
"sha256": "9d2d6a87032e3c60f2666cadd5d5d1520edac05087e0dcb7a5ac3875f2ace325",
"bytes": 24801544
},
"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": "e789004d14185140bda21c10a9d7d8f0b48174a9b5053ec1d70755842b1888de",
"bytes": 20913
},
"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
}
+95
View File
@@ -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)
+34
View File
@@ -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")
+183
View File
@@ -0,0 +1,183 @@
"""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")
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()
+49
View File
@@ -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
+88
View File
@@ -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.6.3", "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)
+188
View File
@@ -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)