96 lines
5.8 KiB
Python
96 lines
5.8 KiB
Python
"""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)
|