From 36d23d2f42631f7bc7966df9c3e60ed061623078 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 7 Oct 2026 09:08:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(build):=20=E5=AE=9E=E7=8E=B0=20Qt=20?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E4=BE=9D=E8=B5=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=EF=BC=88runtime.qt6=5Fpath=20/=20qt6=5Fdlls=20/=20qt6=5Fplugin?= =?UTF-8?q?s=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:project.vpj 里的 runtime 块此前只是声明(编译器不读取),Qr 程序运行时报 Could not find the Qt platform plugin "windows"。 实现:链接成功后(Phase2 的 PhaseC 成功点)调用 BuildPipeline.deploy_runtime_deps(exe_path)——把 qt6_dlls 各项从 /bin 复制到 exe 同目录;把新增的可选字段 qt6_plugins 各项从 /plugins 复制到 /<相对路径>(父目录不存在则 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 并完成部署。 --- App/lib/Phase/BuildPipeline.vp | 122 ++++++++++++++++++++++++++++++- App/lib/Phase/Phase2/__init__.vp | 8 ++ App/lib/Projectrans/Config.vp | 15 ++++ App/main.vp | 11 +++ 4 files changed, 155 insertions(+), 1 deletion(-) diff --git a/App/lib/Phase/BuildPipeline.vp b/App/lib/Phase/BuildPipeline.vp index 40c5577..f16224c 100644 --- a/App/lib/Phase/BuildPipeline.vp +++ b/App/lib/Phase/BuildPipeline.vp @@ -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 \ No newline at end of file + 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: /bin/ -> / + 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: /plugins/ -> / (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 \ No newline at end of file diff --git a/App/lib/Phase/Phase2/__init__.vp b/App/lib/Phase/Phase2/__init__.vp index bc2199f..ca8fd8d 100644 --- a/App/lib/Phase/Phase2/__init__.vp +++ b/App/lib/Phase/Phase2/__init__.vp @@ -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 .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) diff --git a/App/lib/Projectrans/Config.vp b/App/lib/Projectrans/Config.vp index 5ba6751..ea5aa7a 100644 --- a/App/lib/Projectrans/Config.vp +++ b/App/lib/Projectrans/Config.vp @@ -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 /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 diff --git a/App/main.vp b/App/main.vp index 0cd4b9c..013a5bb 100644 --- a/App/main.vp +++ b/App/main.vp @@ -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