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
+154

No files matched your search

+120
View File
@@ -15,6 +15,8 @@ import w32.win32sync as win32sync
import subprocess import subprocess
import viperlib import viperlib
import ast import ast
import os as oslib
import shutil
import lib.core.VLogger as VLogger import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HandlesTranslator import lib.core.Gen.Translator as HandlesTranslator
import lib.core.Gen.DebugInfoGen as DebugInfoGen import lib.core.Gen.DebugInfoGen as DebugInfoGen
@@ -2400,3 +2402,121 @@ def run_pipeline(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, m
result.Success = 1 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: if lret == 0:
VLogger.success(f"output: {exe_path}", "PhaseC") 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, # --traceback: export <exe>.stackmap (skipped under --phase2command,
# where nothing was actually linked). # where nothing was actually linked).
if BuildPipeline._traceback_mode != 0 and BuildPipeline._phase2_cmd_path is None: BuildPipeline.WriteStackMap(mb, exe_path) 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 StrictMode: int = 0
IncludesDir: str = None # includes dir path (first element of includes array, single-dir compat) 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) 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) # includes prebuilt cache dir suffix (asan mode uses .binary_asan to isolate instrumented obj)
_inc_bin_suffix: str = ".binary" _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 CompilerCmd, CompilerFlags, LinkerCmd, LinkerFlags, LinkerOutput, LinkerStaticLibs, LinkerDynamicLibs
global TargetTriple, TargetDataLayout, Sha1SliceLevel, StrictMode global TargetTriple, TargetDataLayout, Sha1SliceLevel, StrictMode
global IncludesDir, IncludesDirs global IncludesDir, IncludesDirs
global Qt6Path, Qt6Dlls, Qt6Plugins
if path is None: return 1 if path is None: return 1
if _mbuddy is None: return 1 if _mbuddy is None: return 1
@@ -161,6 +165,17 @@ def Load_project_config(path: str) -> int:
IncludesDirs = inc_buf IncludesDirs = inc_buf
if IncludesDir is None: IncludesDir = 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 return 0
+11
View File
@@ -24,6 +24,11 @@ import w32.win32base as w32base
import w32.fileio as fileio import w32.fileio as fileio
import sys import sys
import ast import ast
import os
import os._win32
import os._posix
import os.path
import shutil
import mlirlite import mlirlite
import subprocess import subprocess
import viperlib import viperlib
@@ -106,6 +111,12 @@ def main() -> int:
Phase1._mbuddy = mb Phase1._mbuddy = mb
Phase2._mbuddy = mb Phase2._mbuddy = mb
subprocess._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) # hashlib._mbuddy = (mb)
VLogger._mbuddy = mb VLogger._mbuddy = mb
lib._mbuddy = mb lib._mbuddy = mb