背景: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 并完成部署。
2522 lines
113 KiB
Plaintext
2522 lines
113 KiB
Plaintext
import lib.core.Gen.GenApi as GenApi
|
|
import t, c
|
|
from stdint import *
|
|
import stdio
|
|
import string
|
|
import stdlib
|
|
import memhub
|
|
import viperlib
|
|
import vthreading
|
|
import w32.fileio as fileio
|
|
import w32.win32file
|
|
import w32.win32base
|
|
import w32.win32process as win32process
|
|
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
|
|
import lib.Stub.StubMerger as StubMerger
|
|
import lib.Stub.StubGen.Converter as StubConverter
|
|
import lib.Projectrans.Config as Config
|
|
import lib.core.Gen.Generator as GenGenerator
|
|
import lib.core.Gen.Expr.ExprGenCls as ExprGenCls
|
|
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
|
|
|
|
# BuildPipeline - compile pipeline: write .ll to temp, run llc (.ll -> .obj),
|
|
# then link with clang++ (.obj -> .exe).
|
|
|
|
# global mbuddy pointer
|
|
_mbuddy: memhub.MemBuddy*
|
|
|
|
# ASan mode (--asan): instrument .ll via opt -passes=asan; the ASan runtime is
|
|
# declared explicitly in project.vpj's linker.static_libs / dynamic_libs (TPV injects nothing).
|
|
_asan_mode: bool = 0
|
|
|
|
# Traceback mode (--traceback): force-build includes/traceback.vp, inject the startup
|
|
# Install() ctor, keep frame pointers, link dbghelp, then export <exe>.stackmap.
|
|
_traceback_mode: bool = 0
|
|
|
|
# --phase2command xxx.bat: do not run llc/link; write the llc (incl. ASan opt) and clang
|
|
# link commands to the given .bat path for manual execution. None = run normally (default).
|
|
_phase2_cmd_path: str = None
|
|
# whether the bat header (@echo off) has been written; set to 1 on first write
|
|
_phase2_bat_init: int = 0
|
|
|
|
# MLIR lowering step (fixed, no backend switch): mlirlite emits MLIR but opt/llc accept
|
|
# only LLVM IR, so run mlir-opt (validate) then mlir-translate --mlir-to-llvmir first.
|
|
MLIR_OPT_CMD: str = "mlir-opt"
|
|
MLIR_TRANSLATE_CMD: str = "mlir-translate"
|
|
|
|
|
|
def _ir_sibling_path(ir_path: str, suffix: str) -> str:
|
|
"""Derive a sibling path from <...>.ll: strip the .ll suffix and append suffix; mbuddy-allocated."""
|
|
if ir_path is None or suffix is None: return None
|
|
n: sizet = string.strlen(ir_path)
|
|
if n < 3: return None
|
|
suf_len: sizet = string.strlen(suffix)
|
|
buf: str = _mbuddy.alloc(n + suf_len + 1)
|
|
if buf is None: return None
|
|
string.strncpy(buf, ir_path, n - 3)
|
|
buf[n - 3] = '\0'
|
|
string.strcat(buf, suffix)
|
|
return buf
|
|
|
|
# source buffer size (1MB)
|
|
define SRC_BUF_SIZE = 1048576 # 2^20 = 1MB
|
|
|
|
# .vpi stub buffer size (256KB, same as Phase1). Use a compile-time define: a module-level
|
|
# global with this name would emit a second strong symbol and llvm-link reports multiply defined.
|
|
define PYI_BUF_SIZE = 262144
|
|
|
|
# _llc_frame_ptr_flag - llc extra flag for --traceback: " -frame-pointer=all" while
|
|
# enabled (leading space), "" otherwise so the llc command stays byte-identical.
|
|
def _llc_frame_ptr_flag() -> str:
|
|
if _traceback_mode != 0: return " -frame-pointer=all"
|
|
return ""
|
|
|
|
|
|
# _append_traceback_dbghelp - append -ldbghelp for --traceback (StackWalk64 etc. live in
|
|
# dbghelp); appended so libraries follow the objects, else the link has undefined symbols.
|
|
def _append_traceback_dbghelp(linker_flags: str) -> str:
|
|
if _traceback_mode == 0 or linker_flags is None: return linker_flags
|
|
need: sizet = string.strlen(linker_flags) + 16
|
|
buf: str = _mbuddy.alloc(need)
|
|
if buf is None:
|
|
VLogger.error("--traceback: failed to allocate -ldbghelp linker argument buffer, aborting compilation", "TRACEBACK")
|
|
return linker_flags
|
|
viperlib.snprintf(buf, need, "%s -ldbghelp", linker_flags)
|
|
return buf
|
|
|
|
|
|
# --traceback call-site sidecar (<sha1>.vpcall): the compiler accumulates F/C records per
|
|
# module; WriteCallSidecar writes and registers them, WriteStackMap reads them after link.
|
|
define TB_VPCALL_MAX = 8192
|
|
_tb_vpcall_buf: bytes = None # TB_VPCALL_MAX * 8: one char* per slot (stdlib-allocated, one-shot per process)
|
|
_tb_vpcall_n: int = 0
|
|
|
|
|
|
def _tb_register_vpcall(path: str) -> int:
|
|
"""Register a written .vpcall path in the global table for WriteStackMap to read."""
|
|
global _tb_vpcall_buf, _tb_vpcall_n
|
|
if path is None: return 0
|
|
if _tb_vpcall_buf is None:
|
|
_tb_vpcall_buf = stdlib.malloc(sizet(TB_VPCALL_MAX) * 8)
|
|
if _tb_vpcall_buf is None:
|
|
VLogger.error("--traceback: failed to allocate .vpcall path table, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
_tb_vpcall_n = 0
|
|
if _tb_vpcall_n >= TB_VPCALL_MAX:
|
|
VLogger.error("--traceback: .vpcall path table overflow (module count exceeds limit), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(_tb_vpcall_n) * 8)
|
|
slot[0] = path
|
|
_tb_vpcall_n += 1
|
|
return 0
|
|
|
|
|
|
def WriteCallSidecar(temp_dir: str, sha1: str, text: str) -> int:
|
|
"""--traceback: write this module's call-site text to <temp_dir>/<slice>/<sha1>.vpcall; None text skips."""
|
|
if temp_dir is None or sha1 is None or text is None: return 0
|
|
vpath: str = StubMergerCollect._sliced_path(temp_dir, string.strlen(temp_dir), sha1, "vpcall")
|
|
if vpath is None:
|
|
VLogger.error(f"--traceback: failed to build .vpcall path (sha1={sha1}), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
vf: fileio.File* = fileio.File(vpath, fileio.MODE.W)
|
|
if vf.closed:
|
|
VLogger.error(f"--traceback: failed to write .vpcall (cannot open {vpath}), aborting compilation", "TRACEBACK")
|
|
stdlib.free(vpath)
|
|
return 1
|
|
vf.write(text, string.strlen(text))
|
|
vf.close()
|
|
return _tb_register_vpcall(vpath)
|
|
|
|
|
|
# _tb_basename_is_traceback - whether this source file is includes/traceback.vp
|
|
def _tb_basename_is_traceback(file_path: str) -> int:
|
|
if file_path is None: return 0
|
|
flen: sizet = string.strlen(file_path)
|
|
if flen < 12: return 0
|
|
if flen > 12:
|
|
prev: char = file_path[flen - 13]
|
|
if prev != '/' and prev != '\\': return 0
|
|
return 1 if string.strcmp(file_path + (flen - 12), "traceback.vp") == 0 else 0
|
|
|
|
# TranslateFileGetTrans - translate one file and return a Translator (caller dumps IR).
|
|
# File -> AST -> .vpi -> LLVM IR; includes use loose scoping, user files strict (local + imports).
|
|
def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, current_package: str = None, declare_only: int = 0, temp_dir: str = None, pre_allow_dep_inject: int = 0) -> HandlesTranslator.Translator*:
|
|
"""Translate one file and return a Translator (caller dumps IR); None on failure."""
|
|
if file_path is None: return None
|
|
f: fileio.File* = fileio.File(file_path, fileio.MODE.R)
|
|
if f.closed: return None
|
|
# read_all's capacity includes the terminator (reads cap-1 bytes max) and silently
|
|
# truncates longer files; reject by size first, else parsing gets confusing syntax errors.
|
|
if f.size() >= SRC_BUF_SIZE:
|
|
VLogger.error(f"source file exceeds {SRC_BUF_SIZE} byte limit, refusing to read: {file_path}", "fileio")
|
|
f.close()
|
|
return None
|
|
src_buf: bytes = stdlib.malloc(SRC_BUF_SIZE)
|
|
if src_buf is None:
|
|
f.close()
|
|
return None
|
|
bytes_read: LONG = f.read_all(src_buf, SRC_BUF_SIZE)
|
|
f.close()
|
|
if bytes_read <= 0:
|
|
stdlib.free(src_buf)
|
|
return None
|
|
if bytes_read < SRC_BUF_SIZE: src_buf[bytes_read] = 0
|
|
lx: ast.Lexer* = ast.new_lexer(mb)
|
|
ast._lexer_init(lx, src_buf, mb)
|
|
tokens: ast.Token* = ast.tokenize(lx)
|
|
tree: ast.AST* = ast.parse_tokens(mb, tokens)
|
|
# AST is independent after parse; free the source buffer (avoid a 1MB/file leak)
|
|
stdlib.free(src_buf)
|
|
if tree is None:
|
|
# fail-fast location: print a Python-style parse-error traceback (like fatal_error):
|
|
# Traceback (most recent call last) / File "<path>", line N / SyntaxError: <message>
|
|
GenApi.dump_parse_traceback(file_path)
|
|
err_line: int = ast.get_parse_err_line()
|
|
err_col: int = ast.get_parse_err_col()
|
|
VLogger.error(f"parse failed: {file_path} (line {err_line} col {err_col})", "PARSE")
|
|
return None
|
|
# Generate the .vpi stub from the AST before tr.translate (same as Phase1); it lets
|
|
# other modules query function signatures and CTypedef definitions.
|
|
if temp_dir is not None:
|
|
pyi_buf: bytes = stdlib.malloc(PYI_BUF_SIZE)
|
|
if pyi_buf is not None:
|
|
pyi_buf[0] = 0
|
|
# Pass file_path as rel_path: vpi-time fatal_error must blame this source file,
|
|
# and the absolute path lets _read_source_line open and print the source line.
|
|
pyi_pos: sizet = StubConverter._GeneratePyiFromAst(mb, tree, file_path, pyi_buf, PYI_BUF_SIZE)
|
|
if pyi_pos > 0:
|
|
# write {sha1}.vpi into temp_dir (sliced path)
|
|
td_len_vpi: sizet = len(temp_dir)
|
|
pyi_path: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpi, sha1_val, "vpi")
|
|
if pyi_path is not None:
|
|
pf: fileio.File* = fileio.File(pyi_path, fileio.MODE.W)
|
|
if not pf.closed:
|
|
pf.write(pyi_buf, pyi_pos)
|
|
pf.close()
|
|
stdlib.free(pyi_path)
|
|
stdlib.free(pyi_buf)
|
|
tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__())
|
|
tr.__before_init__()
|
|
tr.__init__()
|
|
tr.ModuleSha1 = sha1_val
|
|
tr.CurrentPackage = current_package
|
|
tr._declare_only = declare_only
|
|
# App pre-registration (Phase A-pre) switch: when 1, import handling + dependency vpi
|
|
# injection also run in the declare phase. includes pre-registration must pass 0.
|
|
tr._pre_allow_dep_inject = pre_allow_dep_inject
|
|
# P2: reuse the global gen singleton (created by App/main.vp); create it as a fallback
|
|
# when unregistered (defensive for direct-call paths).
|
|
gen: GenGenerator.Generator* = GenGenerator._g_gen
|
|
if gen is None:
|
|
gen = mb.alloc(GenGenerator.Generator.__sizeof__())
|
|
if gen is None:
|
|
VLogger.error("Generator alloc failed", "GEN")
|
|
return None
|
|
gen.__before_init__()
|
|
gen.__init__(mb)
|
|
GenGenerator._g_gen = gen
|
|
# P2-P5: inject the Handles module-level singletons (delegated functions use local
|
|
# _g_gen to avoid package/class name ambiguity from in-body imports).
|
|
GenApi._g_gen = gen
|
|
# P6: inject singletons for statement/assign/var/import/translate-driver modules
|
|
# set the current file name (for error reporting)
|
|
GenApi.set_current_file_type(file_path)
|
|
# --traceback: mirror the switch into the Gen-layer DWARF emitter. 0 => every DI entry
|
|
# point is a no-op and the IR stays byte-identical to the pre-DWARF output.
|
|
DebugInfoGen.SetEnabled(_traceback_mode)
|
|
# also set it for HandlesImports (import SHA1 checks need it; a circular dep blocks reading HandlesType)
|
|
GenApi.set_current_file_imports(file_path)
|
|
# clear the call stack (reset before each file translation)
|
|
GenApi.clear_trace()
|
|
# module switch: clear the CDefine constant table to isolate each module's constants
|
|
GenApi.clear_cdefine_constants()
|
|
# module switch: clear the CTypedef table to isolate each module's typedefs
|
|
GenApi.clear_ctypedefs()
|
|
# namespace isolation: includes loose (all visible), user files strict (local + imports only)
|
|
strict_mode: int = 1
|
|
if string.strstr(file_path, "includes") is not None: strict_mode = 0
|
|
GenApi.reset_visible_structs(mb, strict_mode)
|
|
# Extract docstrings before tr.translate (pre-registration registers cross-module __doc__;
|
|
# must run before translate recycles the AST pool blocks, which crashes under ASan).
|
|
if gen is not None: gen.ExprGenCls.extract_module_docstrings(tree, sha1_val)
|
|
ret: int = tr.translate(tree)
|
|
if ret != 0:
|
|
VLogger.error(f"file translation failed: {file_path}", "Translate")
|
|
return None
|
|
# --traceback: for includes/traceback.vp, inject the startup Install() ctor into its IR.
|
|
# The injector lives in the Gen layer, so no new cross-module mlirlite dependency.
|
|
if _traceback_mode != 0 and _tb_basename_is_traceback(file_path) != 0: HandlesTranslator.InjectTracebackInstallCtor(tr)
|
|
return tr
|
|
|
|
|
|
class BuildResult:
|
|
Success: int
|
|
OutputPath: str
|
|
ErrorMsg: str
|
|
|
|
def __new__(self) -> t.CPtr:
|
|
return t.CPtr(_mbuddy.alloc(BuildResult.__sizeof__()))
|
|
|
|
def __init__(self):
|
|
self.Success = 0
|
|
self.OutputPath = None
|
|
self.ErrorMsg = None
|
|
|
|
|
|
def ensure_dir(path: str) -> int:
|
|
"""Recursively create a directory (like mkdir -p); an existing directory counts as success."""
|
|
if path is None: return 1
|
|
|
|
path_len: sizet = len(path)
|
|
if path_len == 0: return 0
|
|
|
|
# Hot path (PROF: ensure_dir was 4.5%): if the final dir exists, return success early,
|
|
# saving an alloc + N+1 CreateDirectoryA calls. Semantics unchanged (exists == success).
|
|
attr: ULONG = w32.win32file.GetFileAttributesA(path)
|
|
if attr != 0xFFFFFFFF and (attr & w32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 0
|
|
|
|
# copy the path into a writable buffer (for stepwise truncation)
|
|
buf: bytes = _mbuddy.alloc(path_len + 1)
|
|
if buf is None: return 1
|
|
string.strcpy(buf, path)
|
|
|
|
# Temporarily truncate at each separator and create every directory level.
|
|
# CreateDirectoryA returns 0 when the dir exists (failure); ignore it.
|
|
plen: int = path_len
|
|
for i in range(plen):
|
|
ch: int = buf[i]
|
|
if ch == 47 or ch == 92: # '/' = 47, '\\' = 92
|
|
saved: int = ch
|
|
buf[i] = '\0'
|
|
w32.win32file.CreateDirectoryA(buf, None)
|
|
buf[i] = saved
|
|
|
|
# create the final directory
|
|
w32.win32file.CreateDirectoryA(buf, None)
|
|
return 0
|
|
|
|
|
|
# build_sliced_path - build a sliced path and ensure its directory exists. Fans out into
|
|
# SHA1-prefix subdirs per Config.Sha1SliceLevel; returns the full path (mbuddy) or None.
|
|
def build_sliced_path(base_dir: str, sha1: str, ext: str) -> str:
|
|
"""Build a sliced path and ensure its directory exists."""
|
|
if base_dir is None or sha1 is None or ext is None: return None
|
|
|
|
subdir: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
|
|
base_len: sizet = len(base_dir)
|
|
sha1_len: sizet = len(sha1)
|
|
ext_len: sizet = len(ext)
|
|
|
|
if subdir is not None:
|
|
sub_len: sizet = len(subdir)
|
|
# ensure_dir({base_dir}/{subdir})
|
|
dir_path: str = _mbuddy.alloc(base_len + sub_len + 2)
|
|
if dir_path is not None:
|
|
viperlib.snprintf(dir_path, base_len + sub_len + 2, "%s/%s", base_dir, subdir)
|
|
ensure_dir(dir_path)
|
|
# file path: {base_dir}/{subdir}/{sha1}.{ext}
|
|
path_len: sizet = base_len + sub_len + sha1_len + ext_len + 4
|
|
path: str = _mbuddy.alloc(path_len)
|
|
if path is None: return None
|
|
viperlib.snprintf(path, path_len, "%s/%s/%s.%s", base_dir, subdir, sha1, ext)
|
|
return path
|
|
else:
|
|
# no slicing: {base_dir}/{sha1}.{ext}
|
|
path_len = base_len + sha1_len + ext_len + 3
|
|
path = _mbuddy.alloc(path_len)
|
|
if path is None: return None
|
|
viperlib.snprintf(path, path_len, "%s/%s.%s", base_dir, sha1, ext)
|
|
return path
|
|
|
|
|
|
def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: str) -> int:
|
|
"""Write the IR buffer to a .ll file; 0 on success, non-zero on failure."""
|
|
if ir_buf is None or output_dir is None or module_name is None: return 1
|
|
|
|
# build sliced path: output_dir/{sha1 prefix}/{module_name}.ll
|
|
path: str = build_sliced_path(output_dir, module_name, "ll")
|
|
if path is None: return 1
|
|
|
|
# open the file for writing
|
|
f: fileio.File* = fileio.File(path, fileio.MODE.W)
|
|
if f.closed: return 1
|
|
|
|
written: int64 = f.write(ir_buf, ir_len)
|
|
f.close()
|
|
if written < 0: return 1
|
|
return 0
|
|
|
|
|
|
def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
|
|
"""Compile a .ll file to .obj via llc; 0 on success, non-zero on failure."""
|
|
if ir_path is None or cc_cmd is None or output_dir is None or module_name is None: return 1
|
|
|
|
# build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj
|
|
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
|
|
if obj_path is None: return 1
|
|
|
|
# ASan mode: run opt -passes=asan before llc. -asan-globals=false disables global
|
|
# instrumentation: TPV addresses globals directly and ASan redzones would shift them.
|
|
compile_input: str = ir_path
|
|
|
|
# ---- MLIR lowering (fixed, no switch): mlir-opt (validate) -> mlir-translate ----
|
|
# Must run before ASan opt and llc, replacing compile_input with <...>.ll.local.
|
|
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
|
|
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
|
|
if ll_local_path is None or mlir_opt_out is None: return 1
|
|
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
|
|
cmd_mo: str = _mbuddy.alloc(mo_len)
|
|
if cmd_mo is None: return 1
|
|
viperlib.snprintf(cmd_mo, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
|
|
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
|
|
cmd_mt: str = _mbuddy.alloc(mt_len)
|
|
if cmd_mt is None: return 1
|
|
viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
|
|
if _phase2_cmd_path is not None:
|
|
# write the command only (must precede the ASan/llc commands below)
|
|
_phase2_write_cmd(cmd_mo)
|
|
_phase2_write_cmd(cmd_mt)
|
|
else:
|
|
mo_rc: int = _run_cmd_wait(cmd_mo)
|
|
if mo_rc != 0:
|
|
VLogger.error(f"mlir-opt failed (module={module_name}, exit={mo_rc}, cmd={cmd_mo})", "MLIR")
|
|
return 1
|
|
mt_rc: int = _run_cmd_wait(cmd_mt)
|
|
if mt_rc != 0:
|
|
VLogger.error(f"mlir-translate failed (module={module_name}, exit={mt_rc}, cmd={cmd_mt})", "MLIR")
|
|
return 1
|
|
compile_input = ll_local_path
|
|
|
|
# --phase2command mode: do not run opt/llc; write the opt (ASan) + llc commands to the bat
|
|
if _phase2_cmd_path is not None:
|
|
p2_opt_cmd: str = None
|
|
if _asan_mode != 0:
|
|
p2_asan: str = build_sliced_path(output_dir, module_name, "asan.ll")
|
|
if p2_asan is not None:
|
|
p2_olen: sizet = len(p2_asan) + len(compile_input) + 192
|
|
p2_opt_cmd = _mbuddy.alloc(p2_olen)
|
|
if p2_opt_cmd is not None:
|
|
viperlib.snprintf(p2_opt_cmd, p2_olen, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", p2_asan, compile_input)
|
|
compile_input = p2_asan
|
|
llc_fp: str = _llc_frame_ptr_flag()
|
|
p2_clen: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp) + len(obj_path) + len(compile_input) + 64
|
|
p2_cmd: bytes = _mbuddy.alloc(p2_clen)
|
|
if p2_cmd is not None:
|
|
viperlib.snprintf(p2_cmd, p2_clen, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp, obj_path, compile_input)
|
|
if p2_opt_cmd is not None: _phase2_write_cmd(p2_opt_cmd)
|
|
if p2_cmd is not None: _phase2_write_cmd(p2_cmd)
|
|
return 0
|
|
|
|
if _asan_mode != 0:
|
|
asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll")
|
|
if asan_path is None: return 1
|
|
opt_cmd_len: sizet = len(asan_path) + len(compile_input) + 192
|
|
opt_cmd: bytes = _mbuddy.alloc(opt_cmd_len)
|
|
if opt_cmd is None: return 1
|
|
viperlib.snprintf(opt_cmd, opt_cmd_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false -o %s %s", asan_path, compile_input)
|
|
opt_ret: int = stdlib.system(opt_cmd)
|
|
if opt_ret != 0:
|
|
VLogger.error(f"opt ASan instrumentation failed (module={module_name})", "ASAN")
|
|
return 1
|
|
compile_input = asan_path
|
|
|
|
# command: llc -filetype=obj -o {obj_path} ir_path
|
|
llc_fp2: str = _llc_frame_ptr_flag()
|
|
cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp2) + len(obj_path) + len(compile_input) + 64
|
|
cmd: bytes = _mbuddy.alloc(cmd_len)
|
|
if cmd is None: return 1
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp2, obj_path, compile_input)
|
|
|
|
# Launch llc directly via CreateProcessA (avoids cmd/c hanging under asan); the child
|
|
# inherits the console. On failure, log module sha1 + exit code + full command.
|
|
sys_ret: int = _run_cmd_wait(cmd)
|
|
if sys_ret != 0:
|
|
VLogger.error(f"compilation failed (module={module_name}, exit={sys_ret}, cmd={cmd})", "LLC")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# llc parallel compile (Phase B, async threads): tasks/commands use stdlib.malloc (main pool mb
|
|
# gets reset); the worker never allocates from MemBuddy. Max threads = os.cpu_count() (16 here).
|
|
define LLC_MAX_THREADS = 16
|
|
|
|
# LlcTask.FailStage: which step failed (0=none, 1=mlir-opt, 2=mlir-translate, 3=ASan opt,
|
|
# 4=llc). Without this the main thread misreported mlir-opt/translate failures as llc failures.
|
|
define LLC_STAGE_NONE = 0
|
|
define LLC_STAGE_MLIR_OPT = 1
|
|
define LLC_STAGE_MLIR_TRANSLATE = 2
|
|
define LLC_STAGE_ASAN_OPT = 3
|
|
define LLC_STAGE_LLC = 4
|
|
|
|
|
|
class LlcTask:
|
|
"""Async llc task: command strings (stdlib.malloc), module SHA1 and result."""
|
|
OptCmd: bytes # ASan instrumentation command (None if not ASan)
|
|
LlcCmd: bytes # llc command
|
|
ModuleSha1: bytes # module SHA1 (for error reporting)
|
|
Result: int # worker-written: 0 success, non-zero failure
|
|
# ExitCode is appended at the end so existing offsets are unchanged; all construction
|
|
# sites use __sizeof__() and memset the whole record, so offsets need no update.
|
|
ExitCode: int # worker-written: exit code of the failing command (0 on success)
|
|
# MLIR-backend lowering commands (None if not MLIR); also appended at the end.
|
|
MlirOptCmd: bytes # mlir-opt validation command
|
|
MlirTranslateCmd: bytes # mlir-translate lowering command
|
|
# failure stage (LLC_STAGE_*, see the constants above); appended at the end.
|
|
FailStage: int # worker-written: which step failed
|
|
|
|
|
|
def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
|
|
"""Fill an llc task (all stdlib.malloc, kept alive for the thread's run); 0 on success."""
|
|
if task is None or ir_path is None or output_dir is None or module_name is None: return 1
|
|
string.memset(task, 0, LlcTask.__sizeof__())
|
|
task.Result = -1
|
|
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
|
|
if obj_path is None: return 1
|
|
# module SHA1
|
|
sl: sizet = len(module_name)
|
|
task.ModuleSha1 = stdlib.malloc(sl + 1)
|
|
if task.ModuleSha1 is None: return 1
|
|
string.strcpy(task.ModuleSha1, module_name)
|
|
# llc input choice: ASan mode must feed the instrumented .asan.ll, same as the serial
|
|
# compile_ll_to_obj path (else llc ignores the instrumentation and ASan is half-blind).
|
|
compile_input: str = ir_path
|
|
# MLIR lowering (fixed): the worker runs mlir-opt (validate) -> mlir-translate first, so
|
|
# the later ASan OptCmd / LlcCmd input must be <...>.ll.local.
|
|
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
|
|
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
|
|
if ll_local_path is None or mlir_opt_out is None: return 1
|
|
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
|
|
task.MlirOptCmd = stdlib.malloc(mo_len)
|
|
if task.MlirOptCmd is None: return 1
|
|
viperlib.snprintf(task.MlirOptCmd, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
|
|
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
|
|
task.MlirTranslateCmd = stdlib.malloc(mt_len)
|
|
if task.MlirTranslateCmd is None: return 1
|
|
viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
|
|
compile_input = ll_local_path
|
|
# ASan mode: opt instrumentation command (the worker runs MLIR lowering/opt before llc,
|
|
# so LlcCmd's input must point at asan_path; failure sets Result).
|
|
if _asan_mode != 0:
|
|
asan_path: str = build_sliced_path(output_dir, module_name, "asan.ll")
|
|
if asan_path is None: return 1
|
|
opt_len: sizet = len(asan_path) + len(compile_input) + 192
|
|
task.OptCmd = stdlib.malloc(opt_len)
|
|
if task.OptCmd is None: return 1
|
|
viperlib.snprintf(task.OptCmd, opt_len, "opt -passes=forceattrs,asan -force-attribute=sanitize_address" " -asan-globals=false -o %s %s", asan_path, compile_input)
|
|
compile_input = asan_path
|
|
# llc command (input = compile_input: .ll normally, .asan.ll under ASan)
|
|
llc_fp3: str = _llc_frame_ptr_flag()
|
|
cmd_len: sizet = len(cc_cmd) + len(cc_flags) + len(llc_fp3) + len(obj_path) + len(compile_input) + 64
|
|
task.LlcCmd = stdlib.malloc(cmd_len)
|
|
if task.LlcCmd is None: return 1
|
|
viperlib.snprintf(task.LlcCmd, cmd_len, "%s %s%s -o %s %s", cc_cmd, cc_flags, llc_fp3, obj_path, compile_input)
|
|
return 0
|
|
|
|
|
|
def free_llc_task(task: LlcTask*):
|
|
"""Free the task's command strings; the record itself is a slot in the task array."""
|
|
if task is None: return
|
|
if task.OptCmd is not None:
|
|
stdlib.free(task.OptCmd)
|
|
task.OptCmd = None
|
|
if task.LlcCmd is not None:
|
|
stdlib.free(task.LlcCmd)
|
|
task.LlcCmd = None
|
|
if task.MlirOptCmd is not None:
|
|
stdlib.free(task.MlirOptCmd)
|
|
task.MlirOptCmd = None
|
|
if task.MlirTranslateCmd is not None:
|
|
stdlib.free(task.MlirTranslateCmd)
|
|
task.MlirTranslateCmd = None
|
|
if task.ModuleSha1 is not None:
|
|
stdlib.free(task.ModuleSha1)
|
|
task.ModuleSha1 = None
|
|
|
|
|
|
def _phase2_write_cmd(cmd: str) -> None:
|
|
"""--phase2command mode: append the command to the bat file without executing it."""
|
|
global _phase2_bat_init
|
|
if _phase2_cmd_path is None or cmd is None: return
|
|
f: fileio.File* = fileio.File(_phase2_cmd_path, fileio.MODE.A)
|
|
if f.closed:
|
|
VLogger.error("phase2command: failed to open bat", "phase2cmd")
|
|
return
|
|
if _phase2_bat_init == 0:
|
|
_phase2_bat_init = 1
|
|
hdr1: str = "@echo off\r\n"
|
|
hdr2: str = "rem TransPyV --phase2command generated commands\r\n"
|
|
f.write(hdr1, string.strlen(hdr1))
|
|
f.write(hdr2, string.strlen(hdr2))
|
|
f.write(cmd, string.strlen(cmd))
|
|
f.write("\r\n", 2)
|
|
f.close()
|
|
|
|
|
|
def _run_cmd_wait(cmd: str) -> int:
|
|
"""Run a command synchronously via CreateProcessA and return its exit code."""
|
|
if cmd is None: return -1
|
|
cl_len: sizet = len(cmd)
|
|
cmd_line: bytes = stdlib.malloc(cl_len + 1)
|
|
if cmd_line is None: return -1
|
|
string.strcpy(cmd_line, cmd)
|
|
sa: w32.win32base.SECURITY_ATTRIBUTES = w32.win32base.SECURITY_ATTRIBUTES()
|
|
si: win32process.STARTUPINFOA = win32process.STARTUPINFOA()
|
|
pi: win32process.PROCESS_INFORMATION = win32process.PROCESS_INFORMATION()
|
|
# T() only allocas, no memset: unset STARTUPINFOA fields are stack garbage, so concurrent
|
|
# threads get random dwFlags and CreateProcessA uses junk stdio handles. Clear si/sa/pi.
|
|
string.memset(&si, 0, win32process.STARTUPINFOA.__sizeof__())
|
|
string.memset(&sa, 0, w32.win32base.SECURITY_ATTRIBUTES.__sizeof__())
|
|
string.memset(&pi, 0, win32process.PROCESS_INFORMATION.__sizeof__())
|
|
sa.nLength = w32.win32base.SECURITY_ATTRIBUTES.__sizeof__()
|
|
sa.lpSecurityDescriptor = t.CPtr(0)
|
|
sa.bInheritHandle = 1
|
|
si.cb = win32process.STARTUPINFOA.__sizeof__()
|
|
# create_flags=0: the child inherits the parent console (output shown, like system);
|
|
# si.dwFlags stays 0 after memset (no STARTF_USESTDHANDLES, no std handles).
|
|
ok: int = win32process.CreateProcessA(t.CPtr(0), cmd_line, t.CPtr(0), t.CPtr(0), 1, 0, t.CPtr(0), t.CPtr(0), &si, &pi)
|
|
# CreateProcessA failure diagnostics: read GetLastError immediately (later calls overwrite
|
|
# it). Use warning, not error (error would sys.exit in a worker thread).
|
|
if ok == 0:
|
|
cp_err: uint32 = w32.win32base.GetLastError()
|
|
VLogger.warning(f"CreateProcessA failed (GetLastError={cp_err}) external command not started: cmd={cmd}", "LLC")
|
|
stdlib.free(cmd_line)
|
|
if ok == 0: return -1
|
|
win32sync.WaitForSingleObject(pi.hProcess, w32.win32base.INFINITE)
|
|
exit_code: uint32 = 0
|
|
win32process.GetExitCodeProcess(pi.hProcess, &exit_code)
|
|
w32.win32base.CloseHandle(pi.hProcess)
|
|
w32.win32base.CloseHandle(pi.hThread)
|
|
return int(exit_code)
|
|
|
|
|
|
def _llc_task_worker(arg: t.CPtr) -> t.CPtr:
|
|
"""llc thread entry: only calls _run_cmd_wait, no pool allocation (thread-safe)."""
|
|
task: LlcTask* = (LlcTask*)(arg)
|
|
if task is None: return t.CPtr(0)
|
|
# MLIR backend: validate (mlir-opt) -> lower (mlir-translate), then ASan opt -> llc
|
|
if task.MlirOptCmd is not None:
|
|
mo_rc: int = _run_cmd_wait(task.MlirOptCmd)
|
|
if mo_rc != 0:
|
|
task.ExitCode = mo_rc
|
|
task.FailStage = LLC_STAGE_MLIR_OPT
|
|
task.Result = 1
|
|
return t.CPtr(0)
|
|
if task.MlirTranslateCmd is not None:
|
|
mt_rc: int = _run_cmd_wait(task.MlirTranslateCmd)
|
|
if mt_rc != 0:
|
|
task.ExitCode = mt_rc
|
|
task.FailStage = LLC_STAGE_MLIR_TRANSLATE
|
|
task.Result = 1
|
|
return t.CPtr(0)
|
|
if task.OptCmd is not None:
|
|
opt_rc: int = _run_cmd_wait(task.OptCmd)
|
|
if opt_rc != 0:
|
|
task.ExitCode = opt_rc
|
|
task.FailStage = LLC_STAGE_ASAN_OPT
|
|
task.Result = 1
|
|
return t.CPtr(0)
|
|
llc_rc: int = _run_cmd_wait(task.LlcCmd)
|
|
task.ExitCode = llc_rc
|
|
if llc_rc != 0:
|
|
task.FailStage = LLC_STAGE_LLC
|
|
task.Result = 1
|
|
else:
|
|
task.FailStage = LLC_STAGE_NONE
|
|
task.Result = 0
|
|
return t.CPtr(0)
|
|
|
|
|
|
def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int:
|
|
"""Run llc in parallel: batches of max_threads threads, join all then check results."""
|
|
if tasks is None or task_count <= 0: return 0
|
|
# --phase2command mode: do not spawn threads; write all opt (ASan) + llc commands to the bat
|
|
if _phase2_cmd_path is not None:
|
|
for pi2 in range(task_count):
|
|
tsk_p: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(pi2) * LlcTask.__sizeof__())
|
|
if tsk_p is None: continue
|
|
if tsk_p.MlirOptCmd is not None: _phase2_write_cmd(tsk_p.MlirOptCmd)
|
|
if tsk_p.MlirTranslateCmd is not None: _phase2_write_cmd(tsk_p.MlirTranslateCmd)
|
|
if tsk_p.OptCmd is not None: _phase2_write_cmd(tsk_p.OptCmd)
|
|
if tsk_p.LlcCmd is not None: _phase2_write_cmd(tsk_p.LlcCmd)
|
|
VLogger.info(f"phase2command mode: {task_count} llc commands written to {_phase2_cmd_path}", "phase2cmd")
|
|
return 0
|
|
if max_threads <= 0: max_threads = 1
|
|
# Thread-handle array: list[str] (str=bytes=i8*, explicit elem_size=8) stores handles;
|
|
# list[t.CPtr] append crashes reading page zero. Grab the handle right after start().
|
|
handles: list[str]* = list[str](_mbuddy, 8)
|
|
if handles is None:
|
|
VLogger.error("run_llc_parallel: failed to allocate thread handle array", "PhaseB")
|
|
return 1
|
|
slot_size: sizet = LlcTask.__sizeof__()
|
|
ret: int = 0
|
|
# Batched execution: batch_start steps by max_threads (each batch spawn+join+close).
|
|
for batch_start in range(0, task_count, max_threads):
|
|
batch_n: int = max_threads
|
|
if batch_start + batch_n > task_count: batch_n = task_count - batch_start
|
|
# spawn this batch
|
|
for j in range(batch_n):
|
|
tsk: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size)
|
|
th: vthreading.Thread* = vthreading.Thread(_llc_task_worker, t.CPtr(tsk))
|
|
th.start()
|
|
# Read the handle right after start() (the Thread object's stack slot may be
|
|
# reused; reading self.handle at join could see a cleared/overwritten value).
|
|
handles.append(t.CPtr(th.handle))
|
|
for j in range(batch_n):
|
|
h_j: t.CPtr = t.CPtr(handles.get(batch_start + j))
|
|
wr: uint32 = win32sync.WaitForSingleObject(h_j, 600000)
|
|
if wr != 0: VLogger.error("llc thread join timeout/failure (not finished within 600 seconds)", "PhaseB")
|
|
w32.win32base.CloseHandle(h_j)
|
|
# check this batch's results
|
|
for j in range(batch_n):
|
|
tsk2: LlcTask* = (LlcTask*)(uint64(tasks) + sizet(batch_start + j) * slot_size)
|
|
if tsk2 is not None and tsk2.Result != 0:
|
|
# Failure: use FailStage to pinpoint the step and print that step's command.
|
|
# Previously everything printed the LlcCmd as "llc failed", misdirecting MLIR failures.
|
|
st: int = tsk2.FailStage
|
|
stage_name: str = "unknown stage (worker did not finish)"
|
|
stage_cmd: bytes = tsk2.LlcCmd
|
|
if st == LLC_STAGE_MLIR_OPT:
|
|
stage_name = "mlir-opt validation"
|
|
stage_cmd = tsk2.MlirOptCmd
|
|
elif st == LLC_STAGE_MLIR_TRANSLATE:
|
|
stage_name = "mlir-translate lowering"
|
|
stage_cmd = tsk2.MlirTranslateCmd
|
|
elif st == LLC_STAGE_ASAN_OPT:
|
|
stage_name = "opt ASan instrumentation"
|
|
stage_cmd = tsk2.OptCmd
|
|
elif st == LLC_STAGE_LLC:
|
|
stage_name = "llc compile"
|
|
stage_cmd = tsk2.LlcCmd
|
|
VLogger.error(f"{stage_name} failed: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (exit={tsk2.ExitCode} cmd={(stage_cmd if stage_cmd is not None else "?")})", "LLC")
|
|
ret = 1
|
|
return ret
|
|
|
|
|
|
def _CollectObjFilesImpl(base_dir: str, bd_len: sizet, out_buf: bytes, out_pos: sizet, out_size: sizet) -> sizet:
|
|
"""Recursively scan base_dir, collect .obj paths into out_buf, return the new out_pos."""
|
|
find_data: w32.win32file.WIN32_FIND_DATAA* = _mbuddy.alloc(w32.win32file.WIN32_FIND_DATAA.__sizeof__())
|
|
if find_data is None:
|
|
VLogger.warning("_CollectObjFilesImpl failed to allocate find_data, fewer .obj collected", "collect")
|
|
return out_pos
|
|
string.memset(find_data, 0, w32.win32file.WIN32_FIND_DATAA.__sizeof__())
|
|
|
|
pattern: bytes = _mbuddy.alloc(bd_len + 8)
|
|
if pattern is None:
|
|
VLogger.warning("_CollectObjFilesImpl failed to allocate pattern, fewer .obj collected", "collect")
|
|
return out_pos
|
|
viperlib.snprintf(pattern, bd_len + 8, "%s/*", base_dir)
|
|
|
|
handle: w32.win32base.HANDLE = w32.win32file.FindFirstFileA(pattern, find_data)
|
|
_mbuddy.free(pattern)
|
|
if handle == w32.win32base.INVALID_HANDLE_VALUE:
|
|
VLogger.warning("_CollectObjFilesImpl cannot open directory, fewer .obj collected", "collect")
|
|
return out_pos
|
|
|
|
while True:
|
|
fname: str = find_data.cFileName
|
|
if fname is not None:
|
|
fname_len: sizet = len(fname)
|
|
if fname_len > 0:
|
|
# skip . and ..
|
|
is_dot: int = 0
|
|
if fname_len == 1 and fname[0] == '.': is_dot = 1
|
|
elif fname_len == 2 and fname[0] == '.' and fname[1] == '.': is_dot = 1
|
|
if is_dot == 0:
|
|
attrs: ULONG = find_data.dwFileAttributes
|
|
is_dir: int = 0
|
|
if (attrs & w32.win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: is_dir = 1
|
|
if is_dir != 0:
|
|
# recurse into the subdirectory
|
|
sub_dir: bytes = _mbuddy.alloc(bd_len + fname_len + 2)
|
|
if sub_dir is not None:
|
|
viperlib.snprintf(sub_dir, bd_len + fname_len + 2, "%s/%s", base_dir, fname)
|
|
sub_len: sizet = len(sub_dir)
|
|
out_pos = _CollectObjFilesImpl(sub_dir, sub_len, out_buf, out_pos, out_size)
|
|
_mbuddy.free(sub_dir)
|
|
else:
|
|
# check whether this is a .obj file
|
|
if fname_len > 4 and fname[fname_len - 4] == '.' and fname[fname_len - 3] == 'o' and fname[fname_len - 2] == 'b' and fname[fname_len - 1] == 'j':
|
|
need: sizet = bd_len + 1 + fname_len + 2
|
|
if out_pos + need >= out_size: break
|
|
if out_pos > 0:
|
|
out_buf[out_pos] = ' '
|
|
out_pos += 1
|
|
viperlib.snprintf(out_buf + out_pos, need, "%s/%s", base_dir, fname)
|
|
out_pos += bd_len + 1 + fname_len
|
|
|
|
if w32.win32file.FindNextFileA(handle, find_data) == 0: break
|
|
|
|
w32.win32file.FindClose(handle)
|
|
return out_pos
|
|
|
|
|
|
def collect_obj_files(includes_binary_dir: str, out_buf: bytes, out_size: sizet) -> sizet:
|
|
"""Recursively scan includes_binary_dir, collect all .obj paths into out_buf; bytes written."""
|
|
if includes_binary_dir is None or out_buf is None or out_size == 0: return 0
|
|
|
|
out_buf[0] = '\0'
|
|
dir_len: sizet = len(includes_binary_dir)
|
|
out_pos: sizet = _CollectObjFilesImpl(includes_binary_dir, dir_len, out_buf, 0, out_size)
|
|
out_buf[out_pos] = '\0'
|
|
return out_pos
|
|
|
|
|
|
def _append_host_export_implib(linker_flags: str, implib_path: str) -> str:
|
|
"""Host-export mode: append --out-implib=<abs path> when linker_flags has --export-all-symbols."""
|
|
if linker_flags is None or implib_path is None: return linker_flags
|
|
if string.strstr(linker_flags, "--export-all-symbols") is None: return linker_flags
|
|
extra: sizet = string.strlen(" -Wl,--out-implib=") + string.strlen(implib_path) + 1
|
|
buf: str = _mbuddy.alloc(string.strlen(linker_flags) + extra)
|
|
if buf is None:
|
|
VLogger.warning("host export: failed to allocate import lib argument buffer (fragments cannot import host symbols)", "link")
|
|
return linker_flags
|
|
viperlib.snprintf(buf, string.strlen(linker_flags) + extra, "%s -Wl,--out-implib=%s", linker_flags, implib_path)
|
|
VLogger.info(f"host export: produced import lib {implib_path}", "link")
|
|
return buf
|
|
|
|
|
|
def link_obj_to_exe(output_dir: str, module_name: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int:
|
|
"""Link .obj files into an .exe via clang++; 0 on success, non-zero on failure."""
|
|
if output_dir is None or linker_cmd is None or module_name is None: return 1
|
|
|
|
# build sliced .obj path: output_dir/{sha1 prefix}/{module_name}.obj
|
|
obj_path: str = build_sliced_path(output_dir, module_name, "obj")
|
|
if obj_path is None: return 1
|
|
|
|
# Host-exe export (when linker.flags has --export-all-symbols) also emits an import lib
|
|
# {output_dir}/{linker_output}.dll.a; REPL fragments link it to import the host.
|
|
if linker_output is not None:
|
|
implib_len: sizet = string.strlen(output_dir) + string.strlen(linker_output) + 8
|
|
implib_path: str = _mbuddy.alloc(implib_len)
|
|
if implib_path is not None:
|
|
viperlib.snprintf(implib_path, implib_len, "%s/%s.dll.a", output_dir, linker_output)
|
|
linker_flags = _append_host_export_implib(linker_flags, implib_path)
|
|
|
|
# --traceback: link dbghelp (after the host-export rewrite, so both survive)
|
|
linker_flags = _append_traceback_dbghelp(linker_flags)
|
|
|
|
# collect .obj paths from includes.binary
|
|
EXTRA_BUF_SIZE: sizet = 32768
|
|
extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE)
|
|
if extra_objs is None: return 1
|
|
extra_len: sizet = 0
|
|
if includes_binary_dir is not None:
|
|
extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE)
|
|
if extra_len > 0: VLogger.info(f"appended {extra_len} bytes of includes.binary .obj files", "link")
|
|
else: VLogger.warning(f"includes.binary has no .obj files: {includes_binary_dir}", "link")
|
|
|
|
# Command: clang++ {obj_path} extra_objs [libs] -o {...}/{linker_output} linker_flags. Objects
|
|
# and explicit libs must precede -l flags. The ASan runtime is declared in project.vpj, not hardcoded.
|
|
use_flags: str = linker_flags
|
|
static_libs: str = linker_static_libs
|
|
if static_libs is None: static_libs = ""
|
|
if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0:
|
|
# Merge static + dynamic into one space-separated string (Viper str += would emit
|
|
# invalid add i8* IR, so build it with snprintf).
|
|
merged_sl: str = _mbuddy.alloc(len(static_libs) + len(linker_dynamic_libs) + 2)
|
|
if merged_sl is not None:
|
|
if len(static_libs) > 0:
|
|
viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s %s", static_libs, linker_dynamic_libs)
|
|
else:
|
|
viperlib.snprintf(merged_sl, len(static_libs) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs)
|
|
static_libs = merged_sl
|
|
sl_len: sizet = len(static_libs)
|
|
cmd_len: sizet = len(linker_cmd) + len(obj_path) + len(use_flags) + len(output_dir) + len(linker_output) + extra_len + sl_len + 128
|
|
cmd: bytes = _mbuddy.alloc(cmd_len)
|
|
if cmd is None: return 1
|
|
if sl_len > 0 and extra_len > 0:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, static_libs, output_dir, linker_output, use_flags)
|
|
elif sl_len > 0:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, static_libs, output_dir, linker_output, use_flags)
|
|
elif extra_len > 0:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s/%s %s", linker_cmd, obj_path, extra_objs, output_dir, linker_output, use_flags)
|
|
else:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s -o %s/%s %s", linker_cmd, obj_path, output_dir, linker_output, use_flags)
|
|
|
|
# --phase2command mode: do not link; write the command to the bat
|
|
if _phase2_cmd_path is not None:
|
|
_phase2_write_cmd(cmd)
|
|
return 0
|
|
|
|
result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
|
|
if result is None:
|
|
VLogger.error("subprocess.run returned None", "link")
|
|
return 1
|
|
if result.returncode != 0:
|
|
VLogger.error(f"link failed, return code: {result.returncode}", "link")
|
|
VLogger.error(f"command: {cmd}", "link")
|
|
# Show linker error output.
|
|
# Note: subprocess merges stderr into stdout on Windows, so result.stderr is always None.
|
|
if result.stdout is not None: VLogger.error(result.stdout, "link")
|
|
else: VLogger.error("no output captured", "link")
|
|
if result.stderr is not None: VLogger.error(result.stderr, "link")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def asm_to_obj(asm_path: str, sha1_val: str, output_dir: str, triple: str) -> int:
|
|
"""Assemble a .s source into .obj with llvm-mc on the sliced path; 0 on success, non-zero on failure."""
|
|
if asm_path is None or sha1_val is None or output_dir is None: return 1
|
|
obj_path: str = build_sliced_path(output_dir, sha1_val, "obj")
|
|
if obj_path is None: return 1
|
|
trip: str = triple
|
|
if trip is None or trip[0] == '\0': trip = "x86_64-none-elf"
|
|
cmd_len: sizet = len("llvm-mc -triple=") + len(trip) + len(" -filetype=obj -o ") + len(obj_path) + len(asm_path) + 64
|
|
cmd: bytes = _mbuddy.alloc(cmd_len)
|
|
if cmd is None: return 1
|
|
viperlib.snprintf(cmd, cmd_len, "llvm-mc -triple=%s -filetype=obj -o %s %s", trip, obj_path, asm_path)
|
|
|
|
# --phase2command mode: write the command to the bat (same as llc/link)
|
|
if _phase2_cmd_path is not None:
|
|
_phase2_write_cmd(cmd)
|
|
return 0
|
|
|
|
result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
|
|
if result is None:
|
|
VLogger.error("llvm-mc subprocess.run returned None", "asm")
|
|
return 1
|
|
if result.returncode != 0:
|
|
if result.stdout is not None: stdio.printf("%s\n", result.stdout)
|
|
if result.stderr is not None: stdio.printf("%s\n", result.stderr)
|
|
VLogger.error(f"llvm-mc assembly failed, return code: {result.returncode}, command: {cmd}", "asm")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def compile_module_to_obj(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
|
|
"""Compile IR to .obj without linking; 0 on success, non-zero on failure."""
|
|
if ir_buf is None or temp_dir is None or output_dir is None or module_name is None: return 1
|
|
|
|
# Step 1: write the .ll file
|
|
ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name)
|
|
if ret != 0:
|
|
VLogger.error(f"failed to write .ll: {module_name}", "compile")
|
|
return 1
|
|
|
|
# Step 2: build the sliced .ll path and compile -> .obj
|
|
ir_path: str = build_sliced_path(temp_dir, module_name, "ll")
|
|
if ir_path is None: return 1
|
|
|
|
ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags)
|
|
if ret != 0:
|
|
VLogger.error(f"llc compilation failed: {module_name}", "compile")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def link_objs_to_exe(obj_paths: str, obj_paths_len: sizet, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> int:
|
|
"""Link multiple .obj files into an .exe; 0 on success, non-zero on failure."""
|
|
if obj_paths is None or obj_paths_len == 0 or linker_cmd is None: return 1
|
|
|
|
# Dedup .obj paths: the same module's obj can be added by several collection stages, and
|
|
# a path appearing twice causes duplicate symbols. Keep the first occurrence, skip the rest.
|
|
if obj_paths_len > 0:
|
|
dedup: bytes = _mbuddy.alloc(obj_paths_len + 1)
|
|
if dedup is not None:
|
|
dedup[0] = '\0'
|
|
dpos: sizet = 0
|
|
dsrc_i: sizet = 0
|
|
while dsrc_i < obj_paths_len:
|
|
# skip leading spaces
|
|
while dsrc_i < obj_paths_len and obj_paths[dsrc_i] == ' ': dsrc_i += 1
|
|
if dsrc_i >= obj_paths_len: break
|
|
dstart: sizet = dsrc_i
|
|
while dsrc_i < obj_paths_len and obj_paths[dsrc_i] != ' ': dsrc_i += 1
|
|
dtlen: sizet = dsrc_i - dstart
|
|
# check whether dedup already has this token
|
|
ddup: int = 0
|
|
dj: sizet = 0
|
|
while dj < dpos:
|
|
while dj < dpos and dedup[dj] == ' ': dj += 1
|
|
if dj >= dpos: break
|
|
ds2: sizet = dj
|
|
while dj < dpos and dedup[dj] != ' ': dj += 1
|
|
dt2len: sizet = dj - ds2
|
|
if dt2len == dtlen:
|
|
deq: int = 1
|
|
for dk in range(dtlen):
|
|
if dedup[ds2 + dk] != obj_paths[dstart + dk]:
|
|
deq = 0
|
|
break
|
|
if deq != 0:
|
|
ddup = 1
|
|
break
|
|
if ddup == 0:
|
|
if dpos > 0:
|
|
dedup[dpos] = ' '
|
|
dpos += 1
|
|
string.memcpy(dedup + dpos, obj_paths + dstart, dtlen)
|
|
dpos += dtlen
|
|
dedup[dpos] = '\0'
|
|
if dpos > 0 and dpos != obj_paths_len:
|
|
obj_paths = dedup
|
|
obj_paths_len = dpos
|
|
|
|
# collect .obj paths from includes.binary
|
|
EXTRA_BUF_SIZE: sizet = 32768
|
|
extra_objs: bytes = _mbuddy.alloc(EXTRA_BUF_SIZE)
|
|
if extra_objs is None: return 1
|
|
extra_len: sizet = 0
|
|
if includes_binary_dir is not None: extra_len = collect_obj_files(includes_binary_dir, extra_objs, EXTRA_BUF_SIZE)
|
|
|
|
# Command: clang++ obj_paths extra_objs -o linker_output linker_flags. The ASan runtime
|
|
# comes from project.vpj, not TPV. Host export: import lib is <linker_output>.dll.a.
|
|
if linker_output is not None:
|
|
implib_len2: sizet = string.strlen(linker_output) + 8
|
|
implib_path2: str = _mbuddy.alloc(implib_len2)
|
|
if implib_path2 is not None:
|
|
viperlib.snprintf(implib_path2, implib_len2, "%s.dll.a", linker_output)
|
|
linker_flags = _append_host_export_implib(linker_flags, implib_path2)
|
|
# --traceback: link dbghelp (after the host-export rewrite)
|
|
linker_flags = _append_traceback_dbghelp(linker_flags)
|
|
use_flags: str = linker_flags
|
|
static_libs2: str = linker_static_libs
|
|
if static_libs2 is None: static_libs2 = ""
|
|
if linker_dynamic_libs is not None and len(linker_dynamic_libs) > 0:
|
|
# merge static + dynamic (Viper str += emits invalid IR; use snprintf)
|
|
merged_sl2: str = _mbuddy.alloc(len(static_libs2) + len(linker_dynamic_libs) + 2)
|
|
if merged_sl2 is not None:
|
|
if len(static_libs2) > 0:
|
|
viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s %s", static_libs2, linker_dynamic_libs)
|
|
else:
|
|
viperlib.snprintf(merged_sl2, len(static_libs2) + len(linker_dynamic_libs) + 2, "%s", linker_dynamic_libs)
|
|
static_libs2 = merged_sl2
|
|
sl2_len: sizet = len(static_libs2)
|
|
cmd_len: sizet = len(linker_cmd) + obj_paths_len + len(use_flags) + len(linker_output) + extra_len + sl2_len + 128
|
|
cmd: bytes = _mbuddy.alloc(cmd_len)
|
|
if cmd is None: return 1
|
|
# linker.static_libs explicit static libs go after the .obj files and before -o
|
|
if sl2_len > 0 and extra_len > 0:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, static_libs2, linker_output, use_flags)
|
|
elif sl2_len > 0:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, static_libs2, linker_output, use_flags)
|
|
elif extra_len > 0:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s %s -o %s %s", linker_cmd, obj_paths, extra_objs, linker_output, use_flags)
|
|
else:
|
|
viperlib.snprintf(cmd, cmd_len, "%s %s -o %s %s", linker_cmd, obj_paths, linker_output, use_flags)
|
|
|
|
# Capture clang++ stdout/stderr via subprocess.run (stdlib.system could not capture errors like
|
|
# undefined reference). --phase2command mode: do not link; write the command to the bat.
|
|
if _phase2_cmd_path is not None:
|
|
_phase2_write_cmd(cmd)
|
|
return 0
|
|
result: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
|
|
if result is None:
|
|
VLogger.error("subprocess.run returned None", "link")
|
|
return 1
|
|
if result.returncode != 0:
|
|
# First print clang++'s full error output (incl. undefined reference) with stdio.printf;
|
|
# it must precede VLogger.error, which calls sys.exit(1) and terminates the process.
|
|
if result.stdout is not None: stdio.printf("=== clang++ link error output ===\n%s\n", result.stdout)
|
|
if result.stderr is not None:
|
|
stdio.printf("=== clang++ stderr ===\n%s\n", result.stderr)
|
|
VLogger.error(f"link failed, return code: {result.returncode}, command: {cmd}", "link")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# --traceback: <exe>.stackmap sidecar writer (address -> source line). After a successful link,
|
|
# llvm-dwarfdump parses the PE's DWARF; addresses are normalised to RVAs and written out.
|
|
|
|
# row record: u64 rva | u64 file_path | u32 line | u32 pad | u64 func_name (nullable).
|
|
# func_name is only carried by our .vpcall rows; DWARF rows write 0 and fall back to the subprogram table.
|
|
define TB_ROW_SIZE = 32
|
|
define TB_ROW_OFF_FILE = 8
|
|
define TB_ROW_OFF_LINE = 16
|
|
define TB_ROW_OFF_FUNC = 24
|
|
# func record: u64 low_rva | u64 high_rva | u64 name
|
|
define TB_FUNC_SIZE = 24
|
|
# dir record: u32 idx | u32 pad | u64 name
|
|
define TB_DIR_SIZE = 16
|
|
# file record: u32 idx | u32 dir_index | u64 name | u64 resolved_path
|
|
define TB_FILE_SIZE = 32
|
|
# initial capacities for the (growable) row / func tables
|
|
define TB_ROW_CAP0 = 65536
|
|
define TB_FUNC_CAP0 = 65536
|
|
# fixed per-CU table capacities (exceeding either aborts the build)
|
|
define TB_DIR_CAP = 4096
|
|
define TB_FILE_CAP = 65536
|
|
|
|
|
|
class TbPeLayout:
|
|
Ok: int
|
|
ImageBase: uint64
|
|
SizeOfImage: uint64
|
|
TextRva: uint64
|
|
|
|
def __new__(self) -> t.CPtr:
|
|
return t.CPtr(_mbuddy.alloc(TbPeLayout.__sizeof__()))
|
|
|
|
def __init__(self):
|
|
self.Ok = 0
|
|
self.ImageBase = 0
|
|
self.SizeOfImage = 0
|
|
self.TextRva = 0
|
|
|
|
|
|
class TbDump:
|
|
RowBuf: bytes
|
|
RowCap: int
|
|
RowCount: int
|
|
RawRowCount: int
|
|
FuncBuf: bytes
|
|
FuncCap: int
|
|
FuncCount: int
|
|
|
|
def __new__(self) -> t.CPtr:
|
|
return t.CPtr(_mbuddy.alloc(TbDump.__sizeof__()))
|
|
|
|
def __init__(self):
|
|
self.RowBuf = None
|
|
self.RowCap = 0
|
|
self.RowCount = 0
|
|
self.RawRowCount = 0
|
|
self.FuncBuf = None
|
|
self.FuncCap = 0
|
|
self.FuncCount = 0
|
|
|
|
|
|
class TbPool:
|
|
Pool: bytes
|
|
PoolLen: int
|
|
Offs: bytes
|
|
OffCount: int
|
|
|
|
def __new__(self) -> t.CPtr:
|
|
return t.CPtr(_mbuddy.alloc(TbPool.__sizeof__()))
|
|
|
|
def __init__(self):
|
|
self.Pool = None
|
|
self.PoolLen = 0
|
|
self.Offs = None
|
|
self.OffCount = 0
|
|
|
|
|
|
# ---- little-endian byte helpers (no struct.pack in Viper) ----
|
|
def _tb_u8(buf: bytes, off: sizet) -> uint32:
|
|
return uint32(int(buf[off]) & 0xFF)
|
|
|
|
|
|
def _tb_rd_u16(buf: bytes, off: sizet) -> uint32:
|
|
return _tb_u8(buf, off) | (_tb_u8(buf, off + 1) << 8)
|
|
|
|
|
|
def _tb_rd_u32(buf: bytes, off: sizet) -> uint32:
|
|
b0: uint32 = _tb_u8(buf, off)
|
|
b1: uint32 = _tb_u8(buf, off + 1)
|
|
b2: uint32 = _tb_u8(buf, off + 2)
|
|
b3: uint32 = _tb_u8(buf, off + 3)
|
|
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
|
|
|
|
|
|
def _tb_rd_u64(buf: bytes, off: sizet) -> uint64:
|
|
lo: uint64 = uint64(_tb_rd_u32(buf, off))
|
|
hi: uint64 = uint64(_tb_rd_u32(buf, off + 4))
|
|
return lo | (hi << 32)
|
|
|
|
|
|
def _tb_wr_u32(buf: bytes, off: sizet, v: uint64):
|
|
buf[off] = char(int(v & 0xFF))
|
|
buf[off + 1] = char(int((v >> 8) & 0xFF))
|
|
buf[off + 2] = char(int((v >> 16) & 0xFF))
|
|
buf[off + 3] = char(int((v >> 24) & 0xFF))
|
|
|
|
|
|
def _tb_wr_u64(buf: bytes, off: sizet, v: uint64):
|
|
_tb_wr_u32(buf, off, v & 0xFFFFFFFF)
|
|
_tb_wr_u32(buf, off + 4, (v >> 32) & 0xFFFFFFFF)
|
|
|
|
|
|
# ---- tiny string helpers (no re module) ----
|
|
def _tb_cc(s: str, i: sizet) -> int:
|
|
"""Byte value at s[i] as 0..255 (chars in DWARF text are ASCII)."""
|
|
if s is None: return 0
|
|
return int(s[i]) & 0xFF
|
|
|
|
|
|
def _tb_contains(s: str, sub: str) -> int:
|
|
if s is None or sub is None: return 0
|
|
return 1 if string.strstr(s, sub) is not None else 0
|
|
|
|
|
|
def _tb_startswith(s: str, pfx: str) -> int:
|
|
if s is None or pfx is None: return 0
|
|
pl: sizet = string.strlen(pfx)
|
|
if string.strlen(s) < pl: return 0
|
|
return 1 if string.strncmp(s, pfx, pl) == 0 else 0
|
|
|
|
|
|
def _tb_ltrim(s: str) -> str:
|
|
if s is None: return None
|
|
i: sizet = 0
|
|
while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1
|
|
return s + i
|
|
|
|
|
|
def _tb_len_trimmed(s: str) -> sizet:
|
|
if s is None: return 0
|
|
n: sizet = string.strlen(s)
|
|
while n > 0:
|
|
c: int = _tb_cc(s, n - 1)
|
|
if c == 32 or c == 9 or c == 13: n -= 1
|
|
else: break
|
|
return n
|
|
|
|
|
|
def _tb_dup_span(mb: memhub.MemBuddy*, src: str, n: sizet) -> str:
|
|
"""Copy src[0..n) into a fresh NUL-terminated string."""
|
|
if src is None: return None
|
|
out: str = mb.alloc(n + 1)
|
|
if out is None: return None
|
|
string.strncpy(out, src, n)
|
|
out[n] = '\0'
|
|
return out
|
|
|
|
|
|
def _tb_unquote(mb: memhub.MemBuddy*, v: str) -> str:
|
|
"""Trim trailing spaces and strip one pair of surrounding quotes (TPC _Unquote)."""
|
|
if v is None: return None
|
|
n: sizet = _tb_len_trimmed(v)
|
|
if n >= 2 and _tb_cc(v, 0) == 34 and _tb_cc(v, n - 1) == 34: return _tb_dup_span(mb, v + 1, n - 2)
|
|
return _tb_dup_span(mb, v, n)
|
|
|
|
|
|
def _tb_is_abs_path(s: str) -> int:
|
|
if s is None: return 0
|
|
c0: int = _tb_cc(s, 0)
|
|
if c0 == 0: return 0
|
|
if c0 == 47 or c0 == 92: return 1
|
|
if _tb_cc(s, 1) == 58: return 1
|
|
return 0
|
|
|
|
|
|
def _tb_join(mb: memhub.MemBuddy*, base: str, name: str) -> str:
|
|
"""Join base and name with a separator unless base already ends in one."""
|
|
if base is None or string.strlen(base) == 0: return name
|
|
if name is None: return base
|
|
bl: sizet = string.strlen(base)
|
|
nl: sizet = string.strlen(name)
|
|
buf: str = mb.alloc(bl + nl + 2)
|
|
if buf is None: return name
|
|
last: int = _tb_cc(base, bl - 1)
|
|
if last == 47 or last == 92: viperlib.snprintf(buf, bl + nl + 2, "%s%s", base, name)
|
|
else: viperlib.snprintf(buf, bl + nl + 2, "%s/%s", base, name)
|
|
return buf
|
|
|
|
|
|
def _tb_parse_dec(s: str) -> int:
|
|
"""Parse a leading decimal integer, skipping leading blanks (dwarfdump right-aligns indices)."""
|
|
v: int = 0
|
|
i: sizet = 0
|
|
while _tb_cc(s, i) == 32 or _tb_cc(s, i) == 9: i += 1
|
|
while _tb_cc(s, i) >= 48 and _tb_cc(s, i) <= 57:
|
|
v = v * 10 + (_tb_cc(s, i) - 48)
|
|
i += 1
|
|
return v
|
|
|
|
|
|
def _tb_bracket_idx(s: str) -> int:
|
|
"""Parse the N in '[N]' (first bracket group of the line)."""
|
|
i: sizet = 0
|
|
while _tb_cc(s, i) != 0:
|
|
if _tb_cc(s, i) == 91: return _tb_parse_dec(s + i + 1)
|
|
i += 1
|
|
return -1
|
|
|
|
|
|
def _tb_after_bracket(s: str) -> str:
|
|
i: sizet = 0
|
|
while _tb_cc(s, i) != 0:
|
|
if _tb_cc(s, i) == 93: return s + i + 1
|
|
i += 1
|
|
return None
|
|
|
|
|
|
def _tb_value_after_sep(s: str) -> str:
|
|
"""Return the text after the first '=' or ':' (left-trimmed), or None."""
|
|
if s is None: return None
|
|
i: sizet = 0
|
|
while _tb_cc(s, i) != 0:
|
|
c: int = _tb_cc(s, i)
|
|
if c == 61 or c == 58: return _tb_ltrim(s + i + 1)
|
|
i += 1
|
|
return None
|
|
|
|
|
|
def _tb_hex_digit(ch: int) -> int:
|
|
if ch >= 48 and ch <= 57: return ch - 48
|
|
if ch >= 97 and ch <= 102: return ch - 87
|
|
if ch >= 65 and ch <= 70: return ch - 55
|
|
return -1
|
|
|
|
|
|
def _tb_hex_in_parens(s: str, key: str) -> uint64:
|
|
"""Parse the 0x value inside the parentheses right after key; 0 when the form is absent."""
|
|
p: str = string.strstr(s, key)
|
|
if p is None: return 0
|
|
p = p + string.strlen(key)
|
|
while _tb_cc(p, 0) == 32 or _tb_cc(p, 0) == 9: p = p + 1
|
|
if _tb_cc(p, 0) != 40: return 0
|
|
p = p + 1
|
|
while _tb_cc(p, 0) == 32: p = p + 1
|
|
if _tb_cc(p, 0) != 48 or _tb_cc(p, 1) != 120: return 0
|
|
q: str = p + 2
|
|
v: uint64 = 0
|
|
i: sizet = 0
|
|
hd: int = _tb_hex_digit(_tb_cc(q, 0))
|
|
while hd >= 0:
|
|
v = (v << 4) | uint64(hd)
|
|
i += 1
|
|
hd = _tb_hex_digit(_tb_cc(q, i))
|
|
return v
|
|
|
|
|
|
def _tb_quoted_after(mb: memhub.MemBuddy*, s: str, key: str) -> str:
|
|
"""Extract the first double-quoted string following key (DW_AT_name ("..."))."""
|
|
p: str = string.strstr(s, key)
|
|
if p is None: return None
|
|
p = p + string.strlen(key)
|
|
q: str = string.strchr(p, 40)
|
|
if q is None: return None
|
|
q = q + 1
|
|
while _tb_cc(q, 0) == 32: q = q + 1
|
|
if _tb_cc(q, 0) != 34: return None
|
|
q = q + 1
|
|
start: str = q
|
|
n: sizet = 0
|
|
while _tb_cc(q, n) != 0:
|
|
c: int = _tb_cc(q, n)
|
|
if c == 92:
|
|
if _tb_cc(q, n + 1) == 0: break
|
|
n += 2
|
|
continue
|
|
if c == 34: break
|
|
n += 1
|
|
return _tb_dup_span(mb, start, n)
|
|
|
|
|
|
def _tb_dir_lookup(dirs: bytes, count: int, idx: int) -> str:
|
|
for i in range(count):
|
|
if int(_tb_rd_u32(dirs, sizet(i) * TB_DIR_SIZE)) == idx: return rawptr(_tb_rd_u64(dirs, sizet(i) * TB_DIR_SIZE + 8))
|
|
return None
|
|
|
|
|
|
def _tb_file_rec(files: bytes, count: int, idx: int) -> int:
|
|
for i in range(count):
|
|
if int(_tb_rd_u32(files, sizet(i) * TB_FILE_SIZE)) == idx: return i
|
|
return -1
|
|
|
|
|
|
def _tb_resolve_file(mb: memhub.MemBuddy*, files: bytes, fcount: int, dirs: bytes, dcount: int, frec: int) -> str:
|
|
"""Resolve a file-table entry to a path (absolute name wins, else dir join)."""
|
|
roff: sizet = sizet(frec) * TB_FILE_SIZE
|
|
cached: uint64 = _tb_rd_u64(files, roff + 24)
|
|
if cached != 0: return rawptr(cached)
|
|
name: str = rawptr(_tb_rd_u64(files, roff + 8))
|
|
path: str = name
|
|
if name is not None and _tb_is_abs_path(name) == 0:
|
|
didx: int = int(_tb_rd_u32(files, roff + 4))
|
|
base: str = _tb_dir_lookup(dirs, dcount, didx)
|
|
if base is not None and string.strlen(base) > 0:
|
|
if _tb_is_abs_path(base) == 0:
|
|
root: str = _tb_dir_lookup(dirs, dcount, 0)
|
|
if root is not None and _tb_is_abs_path(root) != 0: base = _tb_join(mb, root, base)
|
|
path = _tb_join(mb, base, name)
|
|
if path is None or string.strlen(path) == 0: path = "??"
|
|
_tb_wr_u64(files, roff + 24, uint64(path))
|
|
return path
|
|
|
|
|
|
def _tb_grow_rec(mb: memhub.MemBuddy*, buf: bytes, old_cap: int, rec_size: int, new_cap: int) -> bytes:
|
|
nb: bytes = mb.alloc(sizet(new_cap) * sizet(rec_size))
|
|
if nb is None: return None
|
|
string.memcpy(nb, buf, sizet(old_cap) * sizet(rec_size))
|
|
mb.free(buf)
|
|
return nb
|
|
|
|
|
|
def _tb_emit_row(mb: memhub.MemBuddy*, d: TbDump*, addr: uint64, path: str, line: int, fname: str = None) -> int:
|
|
if d is None: return 1
|
|
if d.RowCount >= d.RowCap:
|
|
ncap: int = d.RowCap * 2
|
|
nb: bytes = _tb_grow_rec(mb, d.RowBuf, d.RowCap, TB_ROW_SIZE, ncap)
|
|
if nb is None:
|
|
VLogger.error("--traceback: failed to grow DWARF line record table, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
d.RowBuf = nb
|
|
d.RowCap = ncap
|
|
off: sizet = sizet(d.RowCount) * TB_ROW_SIZE
|
|
_tb_wr_u64(d.RowBuf, off, addr)
|
|
_tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FILE, uint64(path))
|
|
_tb_wr_u32(d.RowBuf, off + TB_ROW_OFF_LINE, uint64(line))
|
|
fv: uint64 = 0
|
|
if fname is not None: fv = uint64(fname)
|
|
_tb_wr_u64(d.RowBuf, off + TB_ROW_OFF_FUNC, fv)
|
|
d.RowCount = d.RowCount + 1
|
|
d.RawRowCount = d.RawRowCount + 1
|
|
return 0
|
|
|
|
|
|
def _tb_emit_func(mb: memhub.MemBuddy*, d: TbDump*, low: uint64, high: uint64, name: str) -> int:
|
|
if d is None: return 1
|
|
if d.FuncCount >= d.FuncCap:
|
|
ncap: int = d.FuncCap * 2
|
|
nb: bytes = _tb_grow_rec(mb, d.FuncBuf, d.FuncCap, TB_FUNC_SIZE, ncap)
|
|
if nb is None:
|
|
VLogger.error("--traceback: failed to grow DWARF subprogram table, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
d.FuncBuf = nb
|
|
d.FuncCap = ncap
|
|
off: sizet = sizet(d.FuncCount) * TB_FUNC_SIZE
|
|
_tb_wr_u64(d.FuncBuf, off, low)
|
|
_tb_wr_u64(d.FuncBuf, off + 8, high)
|
|
_tb_wr_u64(d.FuncBuf, off + 16, uint64(name))
|
|
d.FuncCount = d.FuncCount + 1
|
|
return 0
|
|
|
|
|
|
def _tb_func_name_for(fbuf: bytes, fcount: int, rva: uint64) -> str:
|
|
"""bisect_right over the (sorted) func low_pc table, then range check."""
|
|
if fbuf is None or fcount <= 0: return None
|
|
lo: int = 0
|
|
hi: int = fcount
|
|
while lo < hi:
|
|
mid: int = lo + (hi - lo) / 2
|
|
if _tb_rd_u64(fbuf, sizet(mid) * TB_FUNC_SIZE) <= rva: lo = mid + 1
|
|
else: hi = mid
|
|
k: int = lo - 1
|
|
if k < 0: return None
|
|
low: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE)
|
|
high: uint64 = _tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 8)
|
|
if low <= rva and rva < high: return rawptr(_tb_rd_u64(fbuf, sizet(k) * TB_FUNC_SIZE + 16))
|
|
return None
|
|
|
|
|
|
def _tb_intern(p: TbPool*, s: str) -> uint64:
|
|
"""Intern s into the pool (linear scan; keeps offset 0 as the empty sentinel)."""
|
|
if p is None or s is None: return 0
|
|
for k in range(p.OffCount):
|
|
o: uint64 = _tb_rd_u64(p.Offs, sizet(k) * 8)
|
|
if string.strcmp(p.Pool + sizet(o), s) == 0: return o
|
|
off: uint64 = uint64(p.PoolLen)
|
|
n: sizet = string.strlen(s)
|
|
string.memcpy(p.Pool + sizet(off), s, n)
|
|
p.Pool[sizet(off) + n] = '\0'
|
|
p.PoolLen = p.PoolLen + int(n) + 1
|
|
_tb_wr_u64(p.Offs, sizet(p.OffCount) * 8, off)
|
|
p.OffCount = p.OffCount + 1
|
|
return off
|
|
|
|
|
|
def _tb_read_pe_layout(mb: memhub.MemBuddy*, exe_path: str) -> TbPeLayout*:
|
|
"""Read (ImageBase, SizeOfImage, .text RVA) from the PE headers."""
|
|
lay: TbPeLayout* = TbPeLayout()
|
|
if lay is None: return None
|
|
f: fileio.File* = fileio.File(exe_path, fileio.MODE.R)
|
|
if f.closed: return lay
|
|
HEAD: sizet = 4096
|
|
head: bytes = mb.alloc(HEAD)
|
|
if head is None:
|
|
f.close()
|
|
return lay
|
|
got: LONG = f.read_all(head, HEAD)
|
|
f.close()
|
|
if got < 0x40: return lay
|
|
if _tb_cc(head, 0) != 77 or _tb_cc(head, 1) != 90: return lay
|
|
e_lfanew: uint64 = uint64(_tb_rd_u32(head, 0x3C))
|
|
if e_lfanew + 24 > uint64(got): return lay
|
|
if _tb_cc(head, sizet(e_lfanew)) != 80 or _tb_cc(head, sizet(e_lfanew) + 1) != 69: return lay
|
|
coff: sizet = sizet(e_lfanew) + 4
|
|
nsec: uint64 = uint64(_tb_rd_u16(head, coff + 2))
|
|
optsz: uint64 = uint64(_tb_rd_u16(head, coff + 16))
|
|
opt: sizet = coff + 20
|
|
if uint64(opt) + 32 > uint64(got): return lay
|
|
magic: uint64 = uint64(_tb_rd_u16(head, opt))
|
|
img_base: uint64 = 0
|
|
if magic == 0x20B: img_base = _tb_rd_u64(head, opt + 24)
|
|
elif magic == 0x10B: img_base = uint64(_tb_rd_u32(head, opt + 28))
|
|
else: return lay
|
|
size_img: uint64 = uint64(_tb_rd_u32(head, opt + 56))
|
|
sec_off: sizet = opt + sizet(optsz)
|
|
text_rva: uint64 = 0x1000
|
|
min_rva: uint64 = 0xFFFFFFFF
|
|
found_text: int = 0
|
|
have_sec: int = 0
|
|
for si in range(nsec):
|
|
base: sizet = sec_off + sizet(si) * 40
|
|
if uint64(base) + 40 > uint64(got): break
|
|
va: uint64 = uint64(_tb_rd_u32(head, base + 12))
|
|
have_sec = 1
|
|
if va < min_rva: min_rva = va
|
|
if _tb_cc(head, base) == 46 and _tb_cc(head, base + 1) == 116 and _tb_cc(head, base + 2) == 101 and _tb_cc(head, base + 3) == 120 and _tb_cc(head, base + 4) == 116:
|
|
text_rva = va
|
|
found_text = 1
|
|
if found_text == 0 and have_sec != 0 and min_rva != 0xFFFFFFFF: text_rva = min_rva
|
|
lay.ImageBase = img_base
|
|
lay.SizeOfImage = size_img
|
|
lay.TextRva = text_rva
|
|
lay.Ok = 1
|
|
return lay
|
|
|
|
|
|
def _tb_parse_dump(mb: memhub.MemBuddy*, dump: str) -> TbDump*:
|
|
"""Parse llvm-dwarfdump text into a row table (addr, path, line) and a func table."""
|
|
d: TbDump* = TbDump()
|
|
if d is None: return None
|
|
LINE_CAP: sizet = 8192
|
|
d.RowBuf = mb.alloc(sizet(TB_ROW_CAP0) * TB_ROW_SIZE)
|
|
d.RowCap = TB_ROW_CAP0
|
|
d.FuncBuf = mb.alloc(sizet(TB_FUNC_CAP0) * TB_FUNC_SIZE)
|
|
d.FuncCap = TB_FUNC_CAP0
|
|
dirs: bytes = mb.alloc(sizet(TB_DIR_CAP) * TB_DIR_SIZE)
|
|
files: bytes = mb.alloc(sizet(TB_FILE_CAP) * TB_FILE_SIZE)
|
|
linebuf: bytes = mb.alloc(LINE_CAP)
|
|
if d.RowBuf is None or d.FuncBuf is None or dirs is None or files is None or linebuf is None:
|
|
VLogger.error("--traceback: failed to allocate dwarfdump parse buffer, aborting compilation", "TRACEBACK")
|
|
return None
|
|
|
|
in_line: int = 0
|
|
dir_count: int = 0
|
|
file_count: int = 0
|
|
pkind: int = 0 # 0=none, 1=file, 2=dir
|
|
pidx: int = 0
|
|
pname: str = None
|
|
pdir: int = 0
|
|
in_prog: int = 0
|
|
cur_low: uint64 = 0
|
|
cur_high: uint64 = 0
|
|
cur_has_low: int = 0
|
|
cur_has_high: int = 0
|
|
cname: str = None
|
|
|
|
dlen: sizet = string.strlen(dump)
|
|
pos: sizet = 0
|
|
while pos < dlen:
|
|
ls: sizet = pos
|
|
while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1
|
|
le: sizet = pos
|
|
if pos < dlen: pos += 1
|
|
if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1
|
|
ll: sizet = le - ls
|
|
if ll == 0: continue
|
|
if ll >= LINE_CAP: ll = LINE_CAP - 1
|
|
string.strncpy(linebuf, dump + ls, ll)
|
|
linebuf[ll] = '\0'
|
|
st: str = _tb_ltrim(linebuf)
|
|
|
|
# ---- line-table section gate ----
|
|
if _tb_contains(st, "debug_line contents") != 0:
|
|
in_line = 1
|
|
continue
|
|
if _tb_contains(st, "contents:") != 0 and _tb_contains(st, "debug_") != 0:
|
|
in_line = 0
|
|
continue
|
|
|
|
if in_line != 0:
|
|
if _tb_startswith(st, "debug_line[") != 0:
|
|
# new CU: commit pending file, reset both tables
|
|
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
|
|
file_count += 1
|
|
pkind = 0
|
|
pname = None
|
|
pdir = 0
|
|
dir_count = 0
|
|
file_count = 0
|
|
continue
|
|
if _tb_startswith(st, "file_names[") != 0:
|
|
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
|
|
file_count += 1
|
|
pname = None
|
|
pdir = 0
|
|
pkind = 1
|
|
pidx = _tb_bracket_idx(st)
|
|
continue
|
|
if _tb_startswith(st, "directory_names[") != 0 or _tb_startswith(st, "include_directories[") != 0:
|
|
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
|
|
file_count += 1
|
|
pname = None
|
|
pdir = 0
|
|
pidx = _tb_bracket_idx(st)
|
|
val: str = _tb_value_after_sep(_tb_after_bracket(st))
|
|
if val is not None and _tb_len_trimmed(val) > 0:
|
|
if dir_count < TB_DIR_CAP:
|
|
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
|
|
_tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(_tb_unquote(mb, val)))
|
|
dir_count += 1
|
|
pkind = 0
|
|
else: pkind = 2
|
|
continue
|
|
if _tb_startswith(st, "name:") != 0 and pkind != 0:
|
|
val2: str = _tb_unquote(mb, _tb_ltrim(st + 5))
|
|
if pkind == 1: pname = val2
|
|
else:
|
|
if dir_count < TB_DIR_CAP and val2 is not None:
|
|
_tb_wr_u32(dirs, sizet(dir_count) * TB_DIR_SIZE, uint64(pidx))
|
|
_tb_wr_u64(dirs, sizet(dir_count) * TB_DIR_SIZE + 8, uint64(val2))
|
|
dir_count += 1
|
|
pkind = 0
|
|
continue
|
|
if _tb_startswith(st, "dir_index:") != 0 and pkind == 1:
|
|
# "dir_index:" is 10 chars with the colon; st + 9 would point at the colon and
|
|
# always parse 0 (losing dir indices). Use st + 10.
|
|
pdir = _tb_parse_dec(_tb_ltrim(st + 10))
|
|
continue
|
|
if _tb_startswith(st, "Address") != 0 and _tb_contains(st, "Column") != 0:
|
|
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
|
|
file_count += 1
|
|
pkind = 0
|
|
pname = None
|
|
pdir = 0
|
|
continue
|
|
if _tb_cc(linebuf, 0) == 48 and _tb_cc(linebuf, 1) == 120:
|
|
if pkind == 1 and pname is not None and file_count < TB_FILE_CAP:
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE, uint64(pidx))
|
|
_tb_wr_u32(files, sizet(file_count) * TB_FILE_SIZE + 4, uint64(pdir))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 8, uint64(pname))
|
|
_tb_wr_u64(files, sizet(file_count) * TB_FILE_SIZE + 24, uint64(0))
|
|
file_count += 1
|
|
pkind = 0
|
|
pname = None
|
|
pdir = 0
|
|
ri: sizet = 2
|
|
addr: uint64 = 0
|
|
hd: int = _tb_hex_digit(_tb_cc(linebuf, ri))
|
|
while hd >= 0:
|
|
addr = (addr << 4) | uint64(hd)
|
|
ri += 1
|
|
hd = _tb_hex_digit(_tb_cc(linebuf, ri))
|
|
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1
|
|
lno: int = 0
|
|
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
|
|
lno = lno * 10 + (_tb_cc(linebuf, ri) - 48)
|
|
ri += 1
|
|
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1
|
|
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57: ri += 1
|
|
while ri < ll and (_tb_cc(linebuf, ri) == 32 or _tb_cc(linebuf, ri) == 9): ri += 1
|
|
fidx: int = 0
|
|
while ri < ll and _tb_cc(linebuf, ri) >= 48 and _tb_cc(linebuf, ri) <= 57:
|
|
fidx = fidx * 10 + (_tb_cc(linebuf, ri) - 48)
|
|
ri += 1
|
|
if lno > 0:
|
|
frec: int = _tb_file_rec(files, file_count, fidx)
|
|
fpath: str = None
|
|
if frec >= 0: fpath = _tb_resolve_file(mb, files, file_count, dirs, dir_count, frec)
|
|
if fpath is None: fpath = "??"
|
|
if _tb_emit_row(mb, d, addr, fpath, lno) != 0: return None
|
|
continue
|
|
|
|
# ---- DWARF debug_info subprogram parsing ----
|
|
if _tb_contains(linebuf, "DW_TAG_") != 0:
|
|
if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0:
|
|
hi: uint64 = cur_low + 1
|
|
if cur_has_high != 0:
|
|
hi = cur_high
|
|
if hi <= cur_low: hi = cur_low + cur_high
|
|
if hi > cur_low:
|
|
if _tb_emit_func(mb, d, cur_low, hi, cname) != 0: return None
|
|
in_prog = 0
|
|
cur_low = 0
|
|
cur_high = 0
|
|
cur_has_low = 0
|
|
cur_has_high = 0
|
|
cname = None
|
|
if _tb_contains(linebuf, "DW_TAG_subprogram") != 0: in_prog = 1
|
|
continue
|
|
if in_prog == 0: continue
|
|
if _tb_contains(linebuf, "DW_AT_low_pc") != 0:
|
|
cur_low = _tb_hex_in_parens(linebuf, "DW_AT_low_pc")
|
|
cur_has_low = 1
|
|
continue
|
|
if _tb_contains(linebuf, "DW_AT_high_pc") != 0:
|
|
cur_high = _tb_hex_in_parens(linebuf, "DW_AT_high_pc")
|
|
cur_has_high = 1
|
|
continue
|
|
if _tb_contains(linebuf, "DW_AT_name") != 0 and cname is None:
|
|
cname = _tb_quoted_after(mb, linebuf, "DW_AT_name")
|
|
continue
|
|
|
|
# trailing subprogram
|
|
if in_prog != 0 and cur_has_low != 0 and cname is not None and string.strlen(cname) > 0:
|
|
hi2: uint64 = cur_low + 1
|
|
if cur_has_high != 0:
|
|
hi2 = cur_high
|
|
if hi2 <= cur_low: hi2 = cur_low + cur_high
|
|
if hi2 > cur_low:
|
|
if _tb_emit_func(mb, d, cur_low, hi2, cname) != 0: return None
|
|
return d
|
|
|
|
|
|
# --traceback call-site normalisation: .vpcall + llvm-objdump. The MLIR backend emits no DWARF
|
|
# for our .vp functions, so each direct callq's return address is mapped back to file/line/function.
|
|
|
|
# call record: u64 caller | u64 callee | u64 file | u32 line | u32 next (next in group, NONE=tail)
|
|
define TB_CR_SIZE = 32
|
|
define TB_CR_OFF_CALLER = 0
|
|
define TB_CR_OFF_CALLEE = 8
|
|
define TB_CR_OFF_FILE = 16
|
|
define TB_CR_OFF_LINE = 24
|
|
define TB_CR_OFF_NEXT = 28
|
|
# function record: u64 sym | u64 file | u32 defline | u32 pad
|
|
define TB_FR_SIZE = 24
|
|
define TB_FR_OFF_SYM = 0
|
|
define TB_FR_OFF_FILE = 8
|
|
define TB_FR_OFF_DEFLINE = 16
|
|
# group hash slots (keyed by (caller,callee); open addressing + linear probing):
|
|
# u64 caller | u64 callee | u32 used | u32 head | u32 tail | u32 cursor
|
|
define TB_GRP_SIZE = 32
|
|
define TB_GRP_OFF_CALLER = 0
|
|
define TB_GRP_OFF_CALLEE = 8
|
|
define TB_GRP_OFF_USED = 16
|
|
define TB_GRP_OFF_HEAD = 20
|
|
define TB_GRP_OFF_TAIL = 24
|
|
define TB_GRP_OFF_CURSOR = 28
|
|
# invalid-index sentinel (all-ones u32)
|
|
TB_IDX_NONE: uint64 = 0xFFFFFFFF
|
|
|
|
|
|
class TbCallParse:
|
|
Cr: bytes # array of TB_CR_SIZE records
|
|
CrCap: int
|
|
CrN: int
|
|
Fr: bytes # array of TB_FR_SIZE records
|
|
FrCap: int
|
|
FrN: int
|
|
Grp: bytes # array of TB_GRP_SIZE slots
|
|
GrpCap: int
|
|
|
|
def __new__(self) -> t.CPtr:
|
|
return t.CPtr(_mbuddy.alloc(TbCallParse.__sizeof__()))
|
|
|
|
def __init__(self):
|
|
self.Cr = None
|
|
self.CrCap = 0
|
|
self.CrN = 0
|
|
self.Fr = None
|
|
self.FrCap = 0
|
|
self.FrN = 0
|
|
self.Grp = None
|
|
self.GrpCap = 0
|
|
|
|
|
|
def _tb_next_pow2(n: int) -> int:
|
|
p: int = 16
|
|
while p < n: p = p * 2
|
|
return p
|
|
|
|
|
|
def _tb_strhash(s: str) -> uint64:
|
|
"""FNV-1a hash (32-bit offset basis, 64-bit prime) used only for table addressing."""
|
|
h: uint64 = 0x811C9DC5
|
|
if s is None: return h
|
|
i: sizet = 0
|
|
while _tb_cc(s, i) != 0:
|
|
h = (h ^ uint64(_tb_cc(s, i))) * 0x100000001B3
|
|
i += 1
|
|
return h
|
|
|
|
|
|
def _tb_grp_get(grp: bytes, cap: int, caller: str, callee: str, do_insert: int) -> int:
|
|
"""Look up a slot by (caller, callee); insert on miss when do_insert=1; slot or -1."""
|
|
if grp is None or cap <= 0 or caller is None or callee is None: return -1
|
|
mask: uint64 = uint64(cap - 1)
|
|
hv: uint64 = (_tb_strhash(caller) * 0x100000001B3 + _tb_strhash(callee)) & mask
|
|
for i in range(cap):
|
|
slot: int = int((hv + uint64(i)) & mask)
|
|
off: sizet = sizet(slot) * TB_GRP_SIZE
|
|
if uint64(_tb_rd_u32(grp, off + TB_GRP_OFF_USED)) == 0:
|
|
if do_insert == 0: return -1
|
|
_tb_wr_u32(grp, off + TB_GRP_OFF_USED, uint64(1))
|
|
_tb_wr_u64(grp, off + TB_GRP_OFF_CALLER, uint64(caller))
|
|
_tb_wr_u64(grp, off + TB_GRP_OFF_CALLEE, uint64(callee))
|
|
_tb_wr_u32(grp, off + TB_GRP_OFF_HEAD, uint64(TB_IDX_NONE))
|
|
_tb_wr_u32(grp, off + TB_GRP_OFF_TAIL, uint64(TB_IDX_NONE))
|
|
_tb_wr_u32(grp, off + TB_GRP_OFF_CURSOR, uint64(TB_IDX_NONE))
|
|
return slot
|
|
sc: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLER))
|
|
sk: str = rawptr(_tb_rd_u64(grp, off + TB_GRP_OFF_CALLEE))
|
|
if sc is not None and sk is not None and string.strcmp(sc, caller) == 0 and string.strcmp(sk, callee) == 0: return slot
|
|
return -1
|
|
|
|
|
|
def _tb_cp_init(mb: memhub.MemBuddy*) -> TbCallParse*:
|
|
cp: TbCallParse* = TbCallParse()
|
|
if cp is None: return None
|
|
cp.CrCap = 1024
|
|
cp.FrCap = 1024
|
|
cp.Cr = mb.alloc(sizet(cp.CrCap) * TB_CR_SIZE)
|
|
cp.Fr = mb.alloc(sizet(cp.FrCap) * TB_FR_SIZE)
|
|
if cp.Cr is None or cp.Fr is None: return None
|
|
cp.CrN = 0
|
|
cp.FrN = 0
|
|
return cp
|
|
|
|
|
|
def _tb_cp_add_cr(mb: memhub.MemBuddy*, cp: TbCallParse*, caller: str, callee: str, file: str, line: int) -> int:
|
|
if cp.CrN >= cp.CrCap:
|
|
ncap: int = cp.CrCap * 2
|
|
nb: bytes = _tb_grow_rec(mb, cp.Cr, cp.CrCap, TB_CR_SIZE, ncap)
|
|
if nb is None:
|
|
VLogger.error("--traceback: failed to grow call record table, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
cp.Cr = nb
|
|
cp.CrCap = ncap
|
|
off: sizet = sizet(cp.CrN) * TB_CR_SIZE
|
|
_tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLER, uint64(caller))
|
|
_tb_wr_u64(cp.Cr, off + TB_CR_OFF_CALLEE, uint64(callee))
|
|
_tb_wr_u64(cp.Cr, off + TB_CR_OFF_FILE, uint64(file))
|
|
_tb_wr_u32(cp.Cr, off + TB_CR_OFF_LINE, uint64(line))
|
|
_tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE))
|
|
cp.CrN = cp.CrN + 1
|
|
return 0
|
|
|
|
|
|
def _tb_cp_add_fr(mb: memhub.MemBuddy*, cp: TbCallParse*, sym: str, file: str, defline: int) -> int:
|
|
if cp.FrN >= cp.FrCap:
|
|
ncap: int = cp.FrCap * 2
|
|
nb: bytes = _tb_grow_rec(mb, cp.Fr, cp.FrCap, TB_FR_SIZE, ncap)
|
|
if nb is None:
|
|
VLogger.error("--traceback: failed to grow function record table, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
cp.Fr = nb
|
|
cp.FrCap = ncap
|
|
off: sizet = sizet(cp.FrN) * TB_FR_SIZE
|
|
_tb_wr_u64(cp.Fr, off + TB_FR_OFF_SYM, uint64(sym))
|
|
_tb_wr_u64(cp.Fr, off + TB_FR_OFF_FILE, uint64(file))
|
|
_tb_wr_u32(cp.Fr, off + TB_FR_OFF_DEFLINE, uint64(defline))
|
|
cp.FrN = cp.FrN + 1
|
|
return 0
|
|
|
|
|
|
def _tb_read_whole(mb: memhub.MemBuddy*, path: str) -> str:
|
|
"""Read the whole file into a NUL-terminated string; None on failure."""
|
|
f: fileio.File* = fileio.File(path, fileio.MODE.R)
|
|
if f.closed: return None
|
|
sz: LONGLONG = f.size()
|
|
if sz < 0:
|
|
f.close()
|
|
return None
|
|
buf: str = mb.alloc(sizet(sz) + 2)
|
|
if buf is None:
|
|
f.close()
|
|
return None
|
|
got: LONG = f.read_all(buf, ULONG(sz) + ULONG(2))
|
|
f.close()
|
|
if got < 0: return None
|
|
return buf
|
|
|
|
|
|
def _tb_parse_f_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int:
|
|
"""Parse an F record line with symbol, defline and file fields (tab-separated)."""
|
|
p: sizet = a
|
|
while p < b and _tb_cc(s, p) != 9: p += 1
|
|
if p >= b: return 0
|
|
p += 1
|
|
f2: sizet = p
|
|
while f2 < b and _tb_cc(s, f2) != 9: f2 += 1
|
|
if f2 >= b: return 0
|
|
sym: str = _tb_dup_span(mb, s + p, f2 - p)
|
|
p = f2 + 1
|
|
f3: sizet = p
|
|
while f3 < b and _tb_cc(s, f3) != 9: f3 += 1
|
|
if f3 >= b: return 0
|
|
defline: int = _tb_parse_dec(s + p)
|
|
p = f3 + 1
|
|
file: str = _tb_dup_span(mb, s + p, b - p)
|
|
if sym is None or file is None: return 0
|
|
return _tb_cp_add_fr(mb, cp, sym, file, defline)
|
|
|
|
|
|
def _tb_parse_c_line(mb: memhub.MemBuddy*, s: str, a: sizet, b: sizet, cp: TbCallParse*) -> int:
|
|
"""Parse a C record line with caller, callee, line and file fields (tab-separated)."""
|
|
p: sizet = a
|
|
while p < b and _tb_cc(s, p) != 9: p += 1
|
|
if p >= b: return 0
|
|
p += 1
|
|
e2: sizet = p
|
|
while e2 < b and _tb_cc(s, e2) != 9: e2 += 1
|
|
if e2 >= b: return 0
|
|
caller: str = _tb_dup_span(mb, s + p, e2 - p)
|
|
p = e2 + 1
|
|
e3: sizet = p
|
|
while e3 < b and _tb_cc(s, e3) != 9: e3 += 1
|
|
if e3 >= b: return 0
|
|
callee: str = _tb_dup_span(mb, s + p, e3 - p)
|
|
p = e3 + 1
|
|
e4: sizet = p
|
|
while e4 < b and _tb_cc(s, e4) != 9: e4 += 1
|
|
if e4 >= b: return 0
|
|
line: int = _tb_parse_dec(s + p)
|
|
p = e4 + 1
|
|
file: str = _tb_dup_span(mb, s + p, b - p)
|
|
if caller is None or callee is None or file is None: return 0
|
|
return _tb_cp_add_cr(mb, cp, caller, callee, file, line)
|
|
|
|
|
|
def _tb_parse_vpcall_file(mb: memhub.MemBuddy*, path: str, cp: TbCallParse*) -> int:
|
|
whole: str = _tb_read_whole(mb, path)
|
|
if whole is None:
|
|
VLogger.error(f"--traceback: failed to read .vpcall: {path}, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
n: sizet = string.strlen(whole)
|
|
pos: sizet = 0
|
|
while pos < n:
|
|
ls: sizet = pos
|
|
while pos < n and _tb_cc(whole, pos) != 10: pos += 1
|
|
le: sizet = pos
|
|
if pos < n: pos += 1
|
|
if le > ls and _tb_cc(whole, le - 1) == 13: le -= 1
|
|
if le > ls:
|
|
typ: int = _tb_cc(whole, ls)
|
|
if typ == 70:
|
|
if _tb_parse_f_line(mb, whole, ls, le, cp) != 0: return 1
|
|
elif typ == 67:
|
|
if _tb_parse_c_line(mb, whole, ls, le, cp) != 0: return 1
|
|
return 0
|
|
|
|
|
|
def _tb_load_vpcalls(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int:
|
|
if _tb_vpcall_buf is None or _tb_vpcall_n <= 0: return 0
|
|
for k in range(_tb_vpcall_n):
|
|
slot: str* = (str*)(uint64(_tb_vpcall_buf) + sizet(k) * 8)
|
|
path: str = slot[0]
|
|
if path is not None:
|
|
if _tb_parse_vpcall_file(mb, path, cp) != 0: return 1
|
|
return 0
|
|
|
|
|
|
def _tb_build_groups(mb: memhub.MemBuddy*, cp: TbCallParse*) -> int:
|
|
"""Chain all C records into group lists keyed by (caller, callee) (head/tail/cursor)."""
|
|
if cp.CrN <= 0: return 0
|
|
cp.GrpCap = _tb_next_pow2(cp.CrN * 2 + 16)
|
|
cp.Grp = mb.alloc(sizet(cp.GrpCap) * TB_GRP_SIZE)
|
|
if cp.Grp is None:
|
|
VLogger.error("--traceback: failed to allocate call-site group table, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
string.memset(cp.Grp, 0, sizet(cp.GrpCap) * TB_GRP_SIZE)
|
|
for i in range(cp.CrN):
|
|
off: sizet = sizet(i) * TB_CR_SIZE
|
|
caller: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLER))
|
|
callee: str = rawptr(_tb_rd_u64(cp.Cr, off + TB_CR_OFF_CALLEE))
|
|
slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 1)
|
|
if slot < 0:
|
|
VLogger.error("--traceback: call-site group hash table overflow, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
goff: sizet = sizet(slot) * TB_GRP_SIZE
|
|
tailv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_TAIL))
|
|
if tailv == uint64(TB_IDX_NONE):
|
|
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_HEAD, uint64(i))
|
|
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, uint64(i))
|
|
else:
|
|
toff: sizet = sizet(int(tailv)) * TB_CR_SIZE
|
|
_tb_wr_u32(cp.Cr, toff + TB_CR_OFF_NEXT, uint64(i))
|
|
_tb_wr_u32(cp.Cr, off + TB_CR_OFF_NEXT, uint64(TB_IDX_NONE))
|
|
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_TAIL, uint64(i))
|
|
return 0
|
|
|
|
|
|
def _tb_emit_call_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, caller: str, callee: str, ret_va: uint64) -> int:
|
|
"""Emit a row for a return address from the next unconsumed C record of (caller, callee)."""
|
|
if caller is None or callee is None or cp.Grp is None or ret_va < img_lo: return 0
|
|
slot: int = _tb_grp_get(cp.Grp, cp.GrpCap, caller, callee, 0)
|
|
if slot < 0: return 0
|
|
goff: sizet = sizet(slot) * TB_GRP_SIZE
|
|
curv: uint64 = uint64(_tb_rd_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR))
|
|
if curv == uint64(TB_IDX_NONE): return 0
|
|
ci: int = int(curv)
|
|
coff: sizet = sizet(ci) * TB_CR_SIZE
|
|
file: str = rawptr(_tb_rd_u64(cp.Cr, coff + TB_CR_OFF_FILE))
|
|
line: int = int(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_LINE))
|
|
nxt: uint64 = uint64(_tb_rd_u32(cp.Cr, coff + TB_CR_OFF_NEXT))
|
|
_tb_wr_u32(cp.Grp, goff + TB_GRP_OFF_CURSOR, nxt)
|
|
return _tb_emit_row(mb, d, ret_va, file, line, caller)
|
|
|
|
|
|
def _tb_emit_entry_row(mb: memhub.MemBuddy*, d: TbDump*, cp: TbCallParse*, img_lo: uint64, addr: uint64, sym: str) -> int:
|
|
"""Emit a fallback function-entry row for an objdump label sym (file/line from the F record)."""
|
|
if sym is None or cp.Fr is None or cp.FrN <= 0 or addr < img_lo: return 0
|
|
for i in range(cp.FrN):
|
|
off: sizet = sizet(i) * TB_FR_SIZE
|
|
s: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_SYM))
|
|
if s is not None and string.strcmp(s, sym) == 0:
|
|
file: str = rawptr(_tb_rd_u64(cp.Fr, off + TB_FR_OFF_FILE))
|
|
dl: int = int(_tb_rd_u32(cp.Fr, off + TB_FR_OFF_DEFLINE))
|
|
return _tb_emit_row(mb, d, addr, file, dl, sym)
|
|
return 0
|
|
|
|
|
|
def _tb_hex_span(s: str, a: sizet, b: sizet) -> uint64:
|
|
v: uint64 = 0
|
|
i: sizet = a
|
|
while i < b:
|
|
hd: int = _tb_hex_digit(_tb_cc(s, i))
|
|
if hd < 0: break
|
|
v = (v << 4) | uint64(hd)
|
|
i += 1
|
|
return v
|
|
|
|
|
|
def _tb_run_objdump(mb: memhub.MemBuddy*, exe_path: str) -> str:
|
|
cl: sizet = string.strlen(exe_path) + 64
|
|
cmd: bytes = mb.alloc(cl)
|
|
if cmd is None:
|
|
VLogger.error("--traceback: failed to allocate objdump command buffer, aborting compilation", "TRACEBACK")
|
|
return None
|
|
viperlib.snprintf(cmd, cl, "llvm-objdump -d \"%s\"", exe_path)
|
|
res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
|
|
if res is None or res.returncode != 0:
|
|
rc: int = -1
|
|
if res is not None: rc = res.returncode
|
|
VLogger.error(f"--traceback: failed to run llvm-objdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK")
|
|
return None
|
|
dump: str = res.stdout
|
|
if dump is None:
|
|
VLogger.error("--traceback: llvm-objdump produced no output, aborting compilation", "TRACEBACK")
|
|
return None
|
|
return dump
|
|
|
|
|
|
def _tb_scan_objdump(mb: memhub.MemBuddy*, dump: str, img_lo: uint64, d: TbDump*, cp: TbCallParse*) -> int:
|
|
"""Parse llvm-objdump -d text, match C/F records and write our rows into d."""
|
|
dlen: sizet = string.strlen(dump)
|
|
pos: sizet = 0
|
|
in_text: int = 0
|
|
cur_func: str = None
|
|
prev_is_call: int = 0
|
|
prev_callee: str = None
|
|
prev_func: str = None
|
|
while pos < dlen:
|
|
ls: sizet = pos
|
|
while pos < dlen and _tb_cc(dump, pos) != 10: pos += 1
|
|
le: sizet = pos
|
|
if pos < dlen: pos += 1
|
|
if le > ls and _tb_cc(dump, le - 1) == 13: le -= 1
|
|
if le <= ls: continue
|
|
if _tb_startswith(dump + ls, "Disassembly of section") != 0:
|
|
# section change: a pending callq at the previous section's end has no valid return address
|
|
prev_is_call = 0
|
|
if _tb_contains(dump + ls, ".text") != 0: in_text = 1
|
|
else: in_text = 0
|
|
continue
|
|
if in_text == 0: continue
|
|
c0: int = _tb_cc(dump, ls)
|
|
is_hex0: int = 0
|
|
if (c0 >= 48 and c0 <= 57) or (c0 >= 97 and c0 <= 102) or (c0 >= 65 and c0 <= 70): is_hex0 = 1
|
|
if is_hex0 == 0: continue
|
|
# first space
|
|
sp: sizet = ls
|
|
while sp < le and _tb_cc(dump, sp) != 32: sp += 1
|
|
if sp + 1 < le and _tb_cc(dump, sp + 1) == 60:
|
|
# ---- label line `ADDR <name>:` ----
|
|
laddr: uint64 = _tb_hex_span(dump, ls, sp)
|
|
if prev_is_call != 0:
|
|
_tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, laddr)
|
|
prev_is_call = 0
|
|
lb: sizet = sp + 2
|
|
j: sizet = lb
|
|
while j < le and _tb_cc(dump, j) != 62: j += 1
|
|
cur_func = _tb_dup_span(mb, dump + lb, j - lb)
|
|
if cur_func is not None: _tb_emit_entry_row(mb, d, cp, img_lo, laddr, cur_func)
|
|
continue
|
|
# ---- instruction line `ADDR: bytes mnemonic operands` ----
|
|
k: sizet = ls
|
|
while k < le and _tb_cc(dump, k) != 58: k += 1
|
|
if k >= le: continue
|
|
iaddr: uint64 = _tb_hex_span(dump, ls, k)
|
|
if prev_is_call != 0:
|
|
_tb_emit_call_row(mb, d, cp, img_lo, prev_func, prev_callee, iaddr)
|
|
prev_is_call = 0
|
|
# skip byte tokens (exactly 2 hex chars) and locate the mnemonic start
|
|
p: sizet = k + 1
|
|
while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1
|
|
ms: sizet = p
|
|
while p < le:
|
|
ts: sizet = p
|
|
while p < le and _tb_cc(dump, p) != 32 and _tb_cc(dump, p) != 9: p += 1
|
|
tl: sizet = p - ts
|
|
ishex: int = 0
|
|
if tl == 2 and _tb_hex_digit(_tb_cc(dump, ts)) >= 0 and _tb_hex_digit(_tb_cc(dump, ts + 1)) >= 0: ishex = 1
|
|
ms = ts
|
|
if ishex == 0: break
|
|
while p < le and (_tb_cc(dump, p) == 32 or _tb_cc(dump, p) == 9): p += 1
|
|
me: sizet = p
|
|
mlen: sizet = me - ms
|
|
is_call: int = 0
|
|
if mlen == 5 and string.strncmp(dump + ms, "callq", 5) == 0: is_call = 1
|
|
elif mlen == 4 and string.strncmp(dump + ms, "call", 4) == 0: is_call = 1
|
|
if is_call != 0:
|
|
os: sizet = me
|
|
while os < le and (_tb_cc(dump, os) == 32 or _tb_cc(dump, os) == 9): os += 1
|
|
if os + 1 < le and _tb_cc(dump, os) == 48 and _tb_cc(dump, os + 1) == 120:
|
|
# direct call: extract `<callee>`
|
|
ab: sizet = os
|
|
while ab < le and _tb_cc(dump, ab) != 60: ab += 1
|
|
if ab < le:
|
|
cb: sizet = ab + 1
|
|
ce: sizet = cb
|
|
while ce < le and _tb_cc(dump, ce) != 62: ce += 1
|
|
xe: sizet = cb
|
|
while xe < ce and _tb_cc(dump, xe) != 43: xe += 1
|
|
prev_callee = _tb_dup_span(mb, dump + cb, xe - cb)
|
|
prev_func = cur_func
|
|
prev_is_call = 1
|
|
return 0
|
|
|
|
|
|
def WriteStackMap(mb: memhub.MemBuddy*, exe_path: str) -> int:
|
|
"""--traceback: export <exe>.stackmap after a successful link. 0 = ok."""
|
|
if mb is None or exe_path is None:
|
|
VLogger.error("--traceback: WriteStackMap argument is null, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
|
|
lay: TbPeLayout* = _tb_read_pe_layout(mb, exe_path)
|
|
if lay is None or lay.Ok == 0:
|
|
VLogger.error(f"--traceback: only Windows PE output is supported, but {exe_path} is not a valid PE (required by --traceback)", "TRACEBACK")
|
|
return 1
|
|
size_of_image: uint64 = lay.SizeOfImage
|
|
if size_of_image == 0:
|
|
VLogger.error(f"--traceback: read PE.SizeOfImage is not positive ({size_of_image}), cannot bound RVA range (required by --traceback)", "TRACEBACK")
|
|
return 1
|
|
img_lo: uint64 = lay.ImageBase
|
|
img_hi: uint64 = lay.ImageBase + size_of_image
|
|
text_rva: uint64 = lay.TextRva
|
|
|
|
# ---- 1) run llvm-dwarfdump (bare name: CreateProcessA resolves it via PATH) ----
|
|
cmd_len: sizet = string.strlen(exe_path) + 64
|
|
cmd: bytes = mb.alloc(cmd_len)
|
|
if cmd is None:
|
|
VLogger.error("--traceback: failed to allocate dwarfdump command buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
viperlib.snprintf(cmd, cmd_len, "llvm-dwarfdump --debug-line --debug-info \"%s\"", exe_path)
|
|
res: subprocess.CompletedProcess* = subprocess.run(cmd, True, True)
|
|
if res is None or res.returncode != 0:
|
|
rc: int = -1
|
|
if res is not None: rc = res.returncode
|
|
VLogger.error(f"--traceback: failed to run llvm-dwarfdump (return code {rc}; ensure LLVM tools are in PATH), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
dump: str = res.stdout
|
|
if dump is None:
|
|
VLogger.error("--traceback: llvm-dwarfdump produced no output, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
|
|
d: TbDump* = _tb_parse_dump(mb, dump)
|
|
if d is None: return 1
|
|
|
|
# ---- 1b) our call-site rows via .vpcall + llvm-objdump ----
|
|
# DWARF rows only cover the linked mingw CRT; .vpcall + objdump add return-address -> call-site rows.
|
|
if _tb_vpcall_n > 0:
|
|
cp: TbCallParse* = _tb_cp_init(mb)
|
|
if cp is None:
|
|
VLogger.error("--traceback: failed to allocate call-site parse state, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
if _tb_load_vpcalls(mb, cp) != 0: return 1
|
|
if _tb_build_groups(mb, cp) != 0: return 1
|
|
odump: str = _tb_run_objdump(mb, exe_path)
|
|
if odump is None: return 1
|
|
our_before: int = d.RowCount
|
|
if _tb_scan_objdump(mb, odump, img_lo, d, cp) != 0: return 1
|
|
our_added: int = d.RowCount - our_before
|
|
if our_added > 0: VLogger.success(f"--traceback: from {_tb_vpcall_n} .vpcall files generated {our_added} own call-site records (F={cp.FrN} functions, C={cp.CrN} calls)", "TRACEBACK")
|
|
else: VLogger.warning(f"--traceback: read {_tb_vpcall_n} .vpcall files (F={cp.FrN}, C={cp.CrN}) but matched no objdump call sites -- own frame line numbers still missing, falling back to DWARF", "TRACEBACK")
|
|
else: VLogger.warning("--traceback: no .vpcall registered (own modules have no call-site records), falling back to DWARF behavior", "TRACEBACK")
|
|
|
|
if d.RawRowCount == 0:
|
|
VLogger.error(f"--traceback: could not parse any line records from {exe_path} (DWARF .debug_line is empty and there are no own .vpcall records; required by --traceback)", "TRACEBACK")
|
|
return 1
|
|
|
|
# ---- 2) rows: normalise to RVA, same rva keeps the largest line ----
|
|
n: int = d.RowCount
|
|
dropped_rows: int = 0
|
|
kept: int = 0
|
|
i: int = 0
|
|
while i < n:
|
|
a: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
|
|
if a < img_lo or a >= img_hi:
|
|
dropped_rows += 1
|
|
i += 1
|
|
continue
|
|
if kept != i:
|
|
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo)
|
|
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FILE, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
|
|
_tb_wr_u32(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_LINE, uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE)))
|
|
_tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC))
|
|
else: _tb_wr_u64(d.RowBuf, sizet(kept) * TB_ROW_SIZE, a - img_lo)
|
|
kept += 1
|
|
i += 1
|
|
tmp: bytes = mb.alloc(TB_ROW_SIZE)
|
|
if tmp is None:
|
|
VLogger.error("--traceback: failed to allocate line record sort buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
si2: int = 1
|
|
while si2 < kept:
|
|
string.memcpy(tmp, d.RowBuf + sizet(si2) * TB_ROW_SIZE, TB_ROW_SIZE)
|
|
sj: int = si2
|
|
while sj > 0:
|
|
if _tb_rd_u64(d.RowBuf, sizet(sj - 1) * TB_ROW_SIZE) <= _tb_rd_u64(tmp, 0): break
|
|
string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, d.RowBuf + sizet(sj - 1) * TB_ROW_SIZE, TB_ROW_SIZE)
|
|
sj -= 1
|
|
string.memcpy(d.RowBuf + sizet(sj) * TB_ROW_SIZE, tmp, TB_ROW_SIZE)
|
|
si2 += 1
|
|
uniq: int = 0
|
|
i = 0
|
|
while i < kept:
|
|
rv: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
|
|
bl: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
|
|
bp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
|
|
bf: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
|
|
j: int = i + 1
|
|
while j < kept and _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE) == rv:
|
|
lj: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
|
|
if lj > bl:
|
|
bl = lj
|
|
bp = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FILE)
|
|
bf = _tb_rd_u64(d.RowBuf, sizet(j) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
|
|
j += 1
|
|
if uniq != i:
|
|
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE, rv)
|
|
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FILE, bp)
|
|
_tb_wr_u32(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_LINE, bl)
|
|
_tb_wr_u64(d.RowBuf, sizet(uniq) * TB_ROW_SIZE + TB_ROW_OFF_FUNC, bf)
|
|
uniq += 1
|
|
i = j
|
|
if dropped_rows > 0: VLogger.warning(f"--traceback: dropped {dropped_rows} DWARF line records that cannot be normalised (address not in image VA range, mostly COMDAT dedup copies)", "TRACEBACK")
|
|
if uniq == 0:
|
|
VLogger.error(f"--traceback: all line records from {exe_path} cannot be normalised to runtime RVA range, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
|
|
# ---- 3) subprograms: normalise + sort by low_rva ----
|
|
fcount: int = d.FuncCount
|
|
fkept: int = 0
|
|
dropped_funcs: int = 0
|
|
fi: int = 0
|
|
while fi < fcount:
|
|
low: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE)
|
|
if low < img_lo or low >= img_hi:
|
|
dropped_funcs += 1
|
|
fi += 1
|
|
continue
|
|
high: uint64 = _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 8)
|
|
lo_rva: uint64 = low - img_lo
|
|
hi_rva: uint64 = high - img_lo
|
|
if hi_rva <= lo_rva: hi_rva = lo_rva + 1
|
|
if hi_rva > size_of_image: hi_rva = size_of_image
|
|
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE, lo_rva)
|
|
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 8, hi_rva)
|
|
_tb_wr_u64(d.FuncBuf, sizet(fkept) * TB_FUNC_SIZE + 16, _tb_rd_u64(d.FuncBuf, sizet(fi) * TB_FUNC_SIZE + 16))
|
|
fkept += 1
|
|
fi += 1
|
|
ftmp: bytes = mb.alloc(TB_FUNC_SIZE)
|
|
if ftmp is None:
|
|
VLogger.error("--traceback: failed to allocate subprogram sort buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
fj: int = 1
|
|
while fj < fkept:
|
|
string.memcpy(ftmp, d.FuncBuf + sizet(fj) * TB_FUNC_SIZE, TB_FUNC_SIZE)
|
|
fk: int = fj
|
|
while fk > 0:
|
|
if _tb_rd_u64(d.FuncBuf, sizet(fk - 1) * TB_FUNC_SIZE) <= _tb_rd_u64(ftmp, 0): break
|
|
string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, d.FuncBuf + sizet(fk - 1) * TB_FUNC_SIZE, TB_FUNC_SIZE)
|
|
fk -= 1
|
|
string.memcpy(d.FuncBuf + sizet(fk) * TB_FUNC_SIZE, ftmp, TB_FUNC_SIZE)
|
|
fj += 1
|
|
if dropped_funcs > 0: VLogger.warning(f"--traceback: dropped {dropped_funcs} DWARF subprograms that cannot be normalised (low_pc not in image VA range)", "TRACEBACK")
|
|
|
|
# ---- 4) string pool: exact upper bound, then intern (offset 0 = sentinel) ----
|
|
pool_need: sizet = 1
|
|
i = 0
|
|
while i < uniq:
|
|
pth: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
|
|
pool_need += string.strlen(pth) + 1
|
|
i += 1
|
|
# our rows carry the function name directly (string pool); DWARF rows take it from the subprogram table.
|
|
i = 0
|
|
while i < uniq:
|
|
fp: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
|
|
if fp != 0:
|
|
fns: str = rawptr(fp)
|
|
if fns is not None: pool_need += string.strlen(fns) + 1
|
|
i += 1
|
|
i = 0
|
|
while i < fkept:
|
|
nm0: str = rawptr(_tb_rd_u64(d.FuncBuf, sizet(i) * TB_FUNC_SIZE + 16))
|
|
if nm0 is not None: pool_need += string.strlen(nm0) + 1
|
|
i += 1
|
|
p: TbPool* = TbPool()
|
|
if p is None:
|
|
VLogger.error("--traceback: failed to allocate string pool object, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
p.Pool = mb.alloc(pool_need)
|
|
p.Offs = mb.alloc((sizet(uniq) * 2 + sizet(fkept)) * 8 + 64)
|
|
if p.Pool is None or p.Offs is None:
|
|
VLogger.error("--traceback: failed to allocate string pool buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
p.Pool[0] = '\0'
|
|
p.PoolLen = 1
|
|
_tb_wr_u64(p.Offs, 0, uint64(0))
|
|
p.OffCount = 1
|
|
# file_off / func_off per unique row (u32 each)
|
|
ro: bytes = mb.alloc(sizet(uniq) * 8 + 8)
|
|
if ro is None:
|
|
VLogger.error("--traceback: failed to allocate record offset buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
for i in range(uniq):
|
|
pth2: str = rawptr(_tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FILE))
|
|
foff: uint64 = _tb_intern(p, pth2)
|
|
noff: uint64 = 0
|
|
rv2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE)
|
|
fp2: uint64 = _tb_rd_u64(d.RowBuf, sizet(i) * TB_ROW_SIZE + TB_ROW_OFF_FUNC)
|
|
nm2: str = None
|
|
if fp2 != 0: nm2 = rawptr(fp2)
|
|
else: nm2 = _tb_func_name_for(d.FuncBuf, fkept, rv2)
|
|
if nm2 is not None: noff = _tb_intern(p, nm2)
|
|
_tb_wr_u32(ro, sizet(i) * 8, foff)
|
|
_tb_wr_u32(ro, sizet(i) * 8 + 4, noff)
|
|
|
|
# ---- 5) self-checks (mirror the runtime LoadMapFile validation) ----
|
|
prev_rva: uint64 = 0
|
|
outside: int = 0
|
|
plausible: int = 0
|
|
ci: int = 0
|
|
while ci < uniq:
|
|
cv: uint64 = _tb_rd_u64(d.RowBuf, sizet(ci) * TB_ROW_SIZE)
|
|
if ci > 0 and cv <= prev_rva:
|
|
VLogger.error(f"--traceback self-check failed: record RVAs not strictly ascending (record {ci} would break runtime binary search), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
prev_rva = cv
|
|
if cv >= size_of_image: outside += 1
|
|
if cv >= text_rva and cv < size_of_image: plausible += 1
|
|
ci += 1
|
|
if outside > 0:
|
|
VLogger.error(f"--traceback self-check failed: {outside} records have RVA outside [0, SizeOfImage=0x{size_of_image:x}), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
if plausible <= 0:
|
|
VLogger.error("--traceback self-check failed: no record RVA falls in the range possible at runtime (RVA basis likely wrong), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
# file_off must be >= 1: offset 0 is the empty-string sentinel and the runtime
|
|
# rejects a whole map whose record file_off is 0.
|
|
for ci in range(uniq):
|
|
fo: uint64 = uint64(_tb_rd_u32(ro, sizet(ci) * 8))
|
|
if fo == 0:
|
|
VLogger.error("--traceback self-check failed: a record has file_off==0 (empty path; runtime would reject that sidecar file), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
|
|
# ---- 6) serialise and write <exe>.stackmap ----
|
|
total: sizet = 20 + sizet(uniq) * 20 + sizet(p.PoolLen)
|
|
out: bytes = mb.alloc(total)
|
|
if out is None:
|
|
VLogger.error("--traceback: failed to allocate sidecar output buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
out[0] = 'T'
|
|
out[1] = 'P'
|
|
out[2] = 'C'
|
|
out[3] = 'S'
|
|
_tb_wr_u32(out, 4, uint64(1))
|
|
_tb_wr_u32(out, 8, uint64(uniq))
|
|
_tb_wr_u32(out, 12, uint64(p.OffCount))
|
|
_tb_wr_u32(out, 16, uint64(p.PoolLen))
|
|
for ri2 in range(uniq):
|
|
roff2: sizet = 20 + sizet(ri2) * 20
|
|
rv3: uint64 = _tb_rd_u64(d.RowBuf, sizet(ri2) * TB_ROW_SIZE)
|
|
ln3: uint64 = uint64(_tb_rd_u32(d.RowBuf, sizet(ri2) * TB_ROW_SIZE + TB_ROW_OFF_LINE))
|
|
_tb_wr_u64(out, roff2, rv3)
|
|
_tb_wr_u32(out, roff2 + 8, uint64(_tb_rd_u32(ro, sizet(ri2) * 8)))
|
|
_tb_wr_u32(out, roff2 + 12, ln3)
|
|
_tb_wr_u32(out, roff2 + 16, uint64(_tb_rd_u32(ro, sizet(ri2) * 8 + 4)))
|
|
string.memcpy(out + 20 + sizet(uniq) * 20, p.Pool, sizet(p.PoolLen))
|
|
|
|
map_len: sizet = string.strlen(exe_path) + 16
|
|
map_path: str = mb.alloc(map_len)
|
|
if map_path is None:
|
|
VLogger.error("--traceback: failed to allocate sidecar path buffer, aborting compilation", "TRACEBACK")
|
|
return 1
|
|
viperlib.snprintf(map_path, map_len, "%s.stackmap", exe_path)
|
|
mf: fileio.File* = fileio.File(map_path, fileio.MODE.W)
|
|
if mf.closed:
|
|
VLogger.error(f"--traceback: failed to write sidecar file (cannot open {map_path}), aborting compilation", "TRACEBACK")
|
|
return 1
|
|
mf.write(out, total)
|
|
mf.close()
|
|
VLogger.success(f"--traceback: wrote address-to-source-line sidecar {map_path} ({uniq} records, {total} bytes)", "TRACEBACK")
|
|
return 0
|
|
|
|
|
|
def run_pipeline(ir_buf: bytes, ir_len: sizet, temp_dir: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str, linker_cmd: str, linker_flags: str, linker_output: str, includes_binary_dir: str, linker_static_libs: str = None, linker_dynamic_libs: str = None) -> BuildResult*:
|
|
"""Run the full compile pipeline; returns a BuildResult object."""
|
|
result: BuildResult* = BuildResult()
|
|
if result is None: return None
|
|
|
|
# Step 0: ensure temp/output dirs exist (auto-create to avoid write failures)
|
|
ensure_dir(temp_dir)
|
|
ensure_dir(output_dir)
|
|
|
|
# Step 1: write the .ll file
|
|
ret: int = write_ir_to_file(ir_buf, ir_len, temp_dir, module_name)
|
|
if ret != 0:
|
|
result.Success = 0
|
|
result.ErrorMsg = "failed to write .ll file"
|
|
return result
|
|
|
|
# Step 2: llc-compile .ll -> .obj.
|
|
# build the sliced .ll file path
|
|
ir_path: str = build_sliced_path(temp_dir, module_name, "ll")
|
|
if ir_path is None:
|
|
result.Success = 0
|
|
result.ErrorMsg = "memory allocation failed"
|
|
return result
|
|
|
|
ret = compile_ll_to_obj(ir_path, output_dir, module_name, cc_cmd, cc_flags)
|
|
if ret != 0:
|
|
result.Success = 0
|
|
result.ErrorMsg = "llc compilation failed"
|
|
return result
|
|
|
|
# Step 3: clang++ link .obj -> .exe (plus includes.binary prebuilt .obj and static/dynamic libs)
|
|
ret = link_obj_to_exe(output_dir, module_name, linker_cmd, linker_flags, linker_output, includes_binary_dir, linker_static_libs, linker_dynamic_libs)
|
|
if ret != 0:
|
|
result.Success = 0
|
|
result.ErrorMsg = "link failed"
|
|
return result
|
|
|
|
result.Success = 1
|
|
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 |