diff --git a/App/lib/Phase/Phase2/__init__.vp b/App/lib/Phase/Phase2/__init__.vp index ef6c800..0ec4c0f 100644 --- a/App/lib/Phase/Phase2/__init__.vp +++ b/App/lib/Phase/Phase2/__init__.vp @@ -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 .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) diff --git a/App/lib/core/builtin/ViperBuild.vp b/App/lib/core/builtin/ViperBuild.vp index b7e9661..919cfa2 100644 --- a/App/lib/core/builtin/ViperBuild.vp +++ b/App/lib/core/builtin/ViperBuild.vp @@ -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