feat(viperbuild): build(run=, args=) 支持编译后直接运行

- ViperBuild:解析 run(bool) / args(list[str]);非空 args 隐含 run,含空格元素自动加引号,内嵌双引号 fatal

- Phase2:链接成功后按配置运行产物(命令行形式 "exe" args...),CLI --run 仍生效,phase2command 模式跳过
This commit is contained in:
TermiNexus committed 2026-10-24 00:25:47 +08:00
1 parent 22e4fc5d50
commit 024e61ce45
2 files changed
+107 -7

No files matched your search

+23 -5
View File
@@ -18,6 +18,7 @@ import lib.core.Gen.Translator as HandlesTranslator
import lib.Phase.BuildPipeline as BuildPipeline
import lib.core.IncludesScanner as IncludesScanner
import lib.core.CompilerFactory as CompilerFactory
import lib.core.builtin.ViperBuild as ViperBuild
import lib.Stub.StubMerger as StubMerger
import lib.Projectrans.Utils as Utils
import lib.Projectrans.Config as Config
@@ -1512,13 +1513,30 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou
# --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)
if args.get_bool("run") and BuildPipeline._phase2_cmd_path is not None:
# --run / viperbuild build(run=True[, args=[...]]): run the linked exe after linking.
# viperbuild 的 args 已在 Pass 0 拼好(含按需加引号),这里只负责拼命令行。
vb_run_wanted: int = 0
vb_run_args: str = None
if ViperBuild.IsApplied() != 0:
vb_run_wanted = ViperBuild.RunAfterBuild()
vb_run_args = ViperBuild.RunArgs()
run_wanted: bool = args.get_bool("run") or vb_run_wanted != 0
if run_wanted and BuildPipeline._phase2_cmd_path is not None:
# phase2command mode: the exe is not linked yet (the command is in the bat); skip --run
VLogger.info("phase2command mode: skipping --run (exe not linked yet)", "phase2cmd")
elif args.get_bool("run"):
VLogger.info(f"run: {exe_path}", "run")
rp: subprocess.CompletedProcess* = subprocess.run(exe_path, False, False)
if rp is not None: VLogger.info(f"exit code: {rp.returncode}", "run")
elif run_wanted:
run_cmd_len: sizet = len(exe_path) + 8
if vb_run_args is not None: run_cmd_len += len(vb_run_args) + 1
run_cmd: bytes = stdlib.malloc(run_cmd_len)
if run_cmd is not None:
if vb_run_args is not None and string.strlen(vb_run_args) > 0:
viperlib.snprintf(run_cmd, run_cmd_len, "\"%s\" %s", exe_path, vb_run_args)
else:
viperlib.snprintf(run_cmd, run_cmd_len, "\"%s\"", exe_path)
VLogger.info(f"run: {run_cmd}", "run")
rp: subprocess.CompletedProcess* = subprocess.run(run_cmd, False, False)
if rp is not None: VLogger.info(f"exit code: {rp.returncode}", "run")
stdlib.free(run_cmd)
else:
VLogger.error("link failed", "PhaseC")
stdlib.free(entries)
+84 -2
View File
@@ -70,6 +70,9 @@ _c_clean_output: int = 0
_c_clean_temp: int = 0
_c_clean_build_dir: int = 0
_c_clean_includes: int = 0
# build(run=, args=):编译后直接运行(args 非空时隐含 run)
_c_run: int = 0
_c_run_args: str = None
# Whether `import viperbuild` / `from viperbuild import ...` appeared in the closure (diagnostics only)
_saw_viperbuild: int = 0
@@ -738,9 +741,54 @@ def _HandleConfigCall(cl: ast.Call*) -> int:
return 0
def _JoinRunArgs(lst: list[str]*) -> str:
"""Join run args for CreateProcess: elements containing blanks are wrapped in double quotes.
Embedded double quotes are rejected (fail-fast) — Windows argv quoting rules would make the
actual argv ambiguous, and macro config should not silently alter the user's arguments.
"""
if lst is None: return None
n: sizet = len(lst)
if n == 0: return None
total: sizet = 0
for i in range(int(n)):
s: str = lst.get(sizet(i))
if s is None: continue
total += string.strlen(s) + 3
buf: str = stdlib.malloc(total + 1)
if buf is None: return None
pos: sizet = 0
for i in range(int(n)):
s = lst.get(sizet(i))
if s is None: continue
sl: sizet = string.strlen(s)
need_quote: int = 0
j: sizet = 0
while j < sl:
if s[j] == ' ' or s[j] == '\t':
need_quote = 1
if s[j] == '"':
stdlib.free(buf)
return None
j += 1
if pos > 0:
buf[pos] = ' '
pos += 1
if need_quote != 0:
buf[pos] = '"'
pos += 1
string.strcpy(buf + pos, s)
pos += sl
if need_quote != 0:
buf[pos] = '"'
pos += 1
buf[pos] = '\0'
return buf
def _HandleBuildCall(cl: ast.Call*) -> int:
"""Handle a proj.build(triple=, datalayout=, selfmachine=) call."""
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build
"""Handle a proj.build(triple=, datalayout=, selfmachine=, run=, args=) call."""
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build, _c_run, _c_run_args
if cl is None: return 1
if _c_has_build != 0:
VLogger.error("viperbuild: proj.build() may be called only once", "viperbuild")
@@ -767,6 +815,27 @@ def _HandleBuildCall(cl: ast.Call*) -> int:
VLogger.error("viperbuild: build(selfmachine=...) must be a boolean constant", "viperbuild")
return 1
_c_selfmachine = b
elif string.strcmp(kw.arg, "run") == 0:
rb: int = _EvalBool(kw.value)
if rb < 0:
VLogger.error("viperbuild: build(run=...) must be a boolean constant", "viperbuild")
return 1
_c_run = rb
elif string.strcmp(kw.arg, "args") == 0:
if _scratch is None: return 1
tmp_args: list[str]* = list[str](_scratch, 8)
if tmp_args is None: return 1
if _EvalList(kw.value, tmp_args) == 0:
VLogger.error("viperbuild: build(args=...) must be a list[str] constant expression", "viperbuild")
return 1
joined_args: str = _JoinRunArgs(tmp_args)
# None + non-empty list = element rejected (embedded quote); empty list is legal.
if joined_args is None and len(tmp_args) > 0:
VLogger.error("viperbuild: build(args=...) has an element that cannot be passed through (embedded double quote); quote it in code or avoid it", "viperbuild")
return 1
_c_run_args = joined_args
# Non-empty args imply run: `build(args=["--a"])` == run the exe with --a.
if _c_run_args is not None and string.strlen(_c_run_args) > 0: _c_run = 1
else:
VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for build(...)", "viperbuild")
return 1
@@ -1127,6 +1196,7 @@ def _ResetAll() -> None:
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_project, _c_has_build
global _c_adv_asan, _c_adv_traceback
global _c_clean_output, _c_clean_temp, _c_clean_build_dir, _c_clean_includes
global _c_run, _c_run_args
global _saw_viperbuild
_alias = None
_ctor_name = None
@@ -1156,6 +1226,8 @@ def _ResetAll() -> None:
_c_clean_temp = 0
_c_clean_build_dir = 0
_c_clean_includes = 0
_c_run = 0
_c_run_args = None
def DiscoverAndApply(mb: memhub.MemBuddy*, entry_path: str) -> int:
@@ -1250,3 +1322,13 @@ def CleanBuildDir() -> int:
def CleanIncludes() -> int:
"""Whether viperbuild requested cleaning the includes cache (proj.clean.includes); 1=yes / 0=no."""
return _c_clean_includes
def RunAfterBuild() -> int:
"""Whether viperbuild requested running the exe after a successful build; 1=yes / 0=no."""
return _c_run
def RunArgs() -> str:
"""Space-joined run arguments for build(args=[...]) (already quoted where needed); None = none."""
return _c_run_args