feat(build): 实现 Qt 运行时依赖部署(runtime.qt6_path / qt6_dlls / qt6_plugins)

背景:project.vpj 里的 runtime 块此前只是声明(编译器不读取),Qr 程序运行时报 Could not find the Qt platform plugin "windows"。

实现:链接成功后(Phase2 的 PhaseC 成功点)调用 BuildPipeline.deploy_runtime_deps(exe_path)——把 qt6_dlls 各项从 <qt6_path>/bin 复制到 exe 同目录;把新增的可选字段 qt6_plugins 各项从 <qt6_path>/plugins 复制到 <exe 目录>/<相对路径>(父目录不存在则 mkdir)。Config.vp 新增 Qt6Path/Qt6Dlls/Qt6Plugins 读取(口径同 LinkerStaticLibs)。

fail-fast:声明了但源文件不存在 / 复制失败 / mkdir 失败 => VLogger.error 并中止构建;三字段全未声明则整段跳过。

验证:重编 SUCCESS(210.11s);ViperMNIST 产物含 7 个 Qt6*.dll + platforms/qwindows.dll,实跑 stderr 无插件报错;ViperAIChat / ViperQtHelloWorld 同样 OK 并完成部署。
This commit is contained in:
TermiNexus committed 2026-10-07 09:08:35 +08:00
1 parent 848dc0ab38
commit 36d23d2f42
4 files changed
+155 -1

No files matched your search

+121 -1
View File
@@ -15,6 +15,8 @@ import w32.win32sync as win32sync
import subprocess
import viperlib
import ast
import os as oslib
import shutil
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HandlesTranslator
import lib.core.Gen.DebugInfoGen as DebugInfoGen
@@ -2399,4 +2401,122 @@ def run_pipeline(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, m
return result
result.Success = 1
return result
return result
# =============================================================================
# Runtime dependency deployment (project.vpj runtime.*)
# After a successful link, copy the declared runtime deps next to the exe.
# Optional: no-op unless runtime.qt6_path / qt6_dlls / qt6_plugins are declared.
# Fail-fast: a declared source that is missing, or a failed copy/dir-create, aborts the build.
# =============================================================================
def _deploy_dir_of(path: str) -> str:
"""Return the directory portion of path (text before the last / or backslash); None if no separator."""
if path is None: return None
plen: sizet = string.strlen(path)
i: sizet = plen
while i > 0:
if path[i - 1] == '/' or path[i - 1] == '\\': break
i -= 1
if i == 0: return None
buf: str = _mbuddy.alloc(i)
if buf is None: return None
string.strncpy(buf, path, i - 1)
buf[i - 1] = '\0'
return buf
def _deploy_join(base: str, name: str) -> str:
"""Join base and name with '/'; mbuddy-allocated."""
if base is None or name is None: return None
bl: sizet = string.strlen(base)
nl: sizet = string.strlen(name)
buf: str = _mbuddy.alloc(bl + nl + 2)
if buf is None: return None
viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name)
return buf
def _deploy_token(s: str, start: sizet, end: sizet) -> str:
"""Copy s[start:end] into an mbuddy-allocated NUL-terminated string; None on OOM."""
n: sizet = end - start
buf: str = _mbuddy.alloc(n + 1)
if buf is None: return None
string.strncpy(buf, s + start, n)
buf[n] = '\0'
return buf
def deploy_runtime_deps(exe_path: str) -> int:
"""Deploy declared runtime dependencies (project.vpj runtime.*) next to the exe; 0=ok, 1=fail.
No-op when runtime.qt6_path / qt6_dlls / qt6_plugins are undeclared. Fail-fast otherwise."""
if exe_path is None: return 0
qt6_path: str = Config.Qt6Path
if qt6_path is None: return 0
dlls: str = Config.Qt6Dlls
plugins: str = Config.Qt6Plugins
if dlls is None and plugins is None: return 0
exe_dir: str = _deploy_dir_of(exe_path)
if exe_dir is None:
VLogger.error(f"runtime deploy: cannot determine exe directory from {exe_path}", "runtime")
return 1
bin_dir: str = _deploy_join(qt6_path, "bin")
plug_root: str = _deploy_join(qt6_path, "plugins")
# runtime.qt6_dlls: <qt6_path>/bin/<dll> -> <exe_dir>/<dll>
if dlls is not None:
total: sizet = string.strlen(dlls)
i: sizet = 0
while i < total:
while i < total and dlls[i] == ' ': i += 1
if i >= total: break
start: sizet = i
while i < total and dlls[i] != ' ': i += 1
if i > start:
name: str = _deploy_token(dlls, start, i)
if name is None:
VLogger.error("runtime deploy: DLL name allocation failed (OOM)", "runtime")
return 1
src: str = _deploy_join(bin_dir, name)
dst: str = _deploy_join(exe_dir, name)
if not oslib.isfile(src):
VLogger.error(f"runtime deploy: declared DLL not found: {src}", "runtime")
return 1
if shutil.copyfile(src, dst) != 0:
VLogger.error(f"runtime deploy: failed to copy {src} -> {dst}", "runtime")
return 1
VLogger.info(f"runtime deploy: {name}", "runtime")
# runtime.qt6_plugins: <qt6_path>/plugins/<rel> -> <exe_dir>/<rel> (ensure parent dir)
if plugins is not None:
total_p: sizet = string.strlen(plugins)
ip: sizet = 0
while ip < total_p:
while ip < total_p and plugins[ip] == ' ': ip += 1
if ip >= total_p: break
start_p: sizet = ip
while ip < total_p and plugins[ip] != ' ': ip += 1
if ip > start_p:
rel: str = _deploy_token(plugins, start_p, ip)
if rel is None:
VLogger.error("runtime deploy: plugin path allocation failed (OOM)", "runtime")
return 1
src_p: str = _deploy_join(plug_root, rel)
dst_p: str = _deploy_join(exe_dir, rel)
parent: str = _deploy_dir_of(dst_p)
if parent is not None:
if not oslib.isdir(parent):
if oslib.mkdir(parent) != 0:
VLogger.error(f"runtime deploy: failed to create dir {parent}", "runtime")
return 1
if not oslib.isfile(src_p):
VLogger.error(f"runtime deploy: declared plugin not found: {src_p}", "runtime")
return 1
if shutil.copyfile(src_p, dst_p) != 0:
VLogger.error(f"runtime deploy: failed to copy {src_p} -> {dst_p}", "runtime")
return 1
VLogger.info(f"runtime deploy: {rel}", "runtime")
return 0
+8
View File
@@ -1506,6 +1506,14 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou
if lret == 0:
VLogger.success(f"output: {exe_path}", "PhaseC")
# Deploy declared runtime deps (project.vpj runtime.*) next to the exe; fail-fast on error.
# Skipped under --phase2command (the exe is not linked yet).
if BuildPipeline._phase2_cmd_path is None:
if BuildPipeline.deploy_runtime_deps(exe_path) != 0:
VLogger.error("runtime dependency deployment failed, aborting compilation", "runtime")
stdlib.free(entries)
stdlib.free(asm_entries)
return 1
# --traceback: export <exe>.stackmap (skipped under --phase2command,
# where nothing was actually linked).
if BuildPipeline._traceback_mode != 0 and BuildPipeline._phase2_cmd_path is None: BuildPipeline.WriteStackMap(mb, exe_path)
+15
View File
@@ -37,6 +37,9 @@ Sha1SliceLevel: int = 0 # SHA1 slice level (directory dispersion depth)
StrictMode: int = 0
IncludesDir: str = None # includes dir path (first element of includes array, single-dir compat)
IncludesDirs: str = None # all includes dirs (space-separated, multi-dir support)
Qt6Path: str = None # runtime.qt6_path: Qt6 install root (optional)
Qt6Dlls: str = None # runtime.qt6_dlls joined (space-separated DLL names; optional)
Qt6Plugins: str = None # runtime.qt6_plugins joined (space-separated paths relative to <qt6_path>/plugins; optional)
# includes prebuilt cache dir suffix (asan mode uses .binary_asan to isolate instrumented obj)
_inc_bin_suffix: str = ".binary"
@@ -51,6 +54,7 @@ def Load_project_config(path: str) -> int:
global CompilerCmd, CompilerFlags, LinkerCmd, LinkerFlags, LinkerOutput, LinkerStaticLibs, LinkerDynamicLibs
global TargetTriple, TargetDataLayout, Sha1SliceLevel, StrictMode
global IncludesDir, IncludesDirs
global Qt6Path, Qt6Dlls, Qt6Plugins
if path is None: return 1
if _mbuddy is None: return 1
@@ -161,6 +165,17 @@ def Load_project_config(path: str) -> int:
IncludesDirs = inc_buf
if IncludesDir is None: IncludesDir = inc_buf
# Read runtime sub-object (optional): declared runtime deps deployed next to the exe.
# qt6_dlls / qt6_plugins join via _join_flags_array (space-separated, same口径 as linker.static_libs).
runtime: JsonValue* = root["runtime"]
if runtime is not None and runtime.is_object():
qp_val: JsonValue* = runtime["qt6_path"]
Qt6Path = qp_val.as_string() if qp_val is not None else None
qd_val: JsonValue* = runtime["qt6_dlls"]
Qt6Dlls = _join_flags_array(qd_val)
qpl_val: JsonValue* = runtime["qt6_plugins"]
Qt6Plugins = _join_flags_array(qpl_val)
return 0
+11
View File
@@ -24,6 +24,11 @@ import w32.win32base as w32base
import w32.fileio as fileio
import sys
import ast
import os
import os._win32
import os._posix
import os.path
import shutil
import mlirlite
import subprocess
import viperlib
@@ -106,6 +111,12 @@ def main() -> int:
Phase1._mbuddy = mb
Phase2._mbuddy = mb
subprocess._mbuddy = mb
# Runtime-dep deploy primitives (BuildPipeline.deploy_runtime_deps): os.mkdir + shutil.copyfile.
os._mbuddy = mb
os._win32._mbuddy = mb
os._posix._mbuddy = mb
os.path._mbuddy = mb
shutil._mbuddy = mb
# hashlib._mbuddy = (mb)
VLogger._mbuddy = mb
lib._mbuddy = mb