"""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)