from stdint import * import memhub import string import stdlib import viperlib import lib.core.VLogger as VLogger from json.__parser import parse as json_parse from json import JsonValue, JSON_OBJECT, JSON_INT import win32.fileio as fileio # Config - project configuration # Global mbuddy pointer _mbuddy: memhub.MemManager* # Config values (loaded from project.vpj); default None/0: callers must guard with `is not None` (else garbage pointer crash) SourceDir: str = None BuildDir: str = None TempDir: str = None # = {BuildDir}/temp (auto-computed) OutputDir: str = None # = {BuildDir}/output (auto-computed) ProjectName: str = None ProjectVersion: str = None CompilerCmd: str = None CompilerFlags: str = None # compiler flags (space-separated, joined from compiler.flags array) LinkerCmd: str = None LinkerFlags: str = None # linker flags (space-separated, joined from linker.flags array) LinkerOutput: str = None LinkerStaticLibs: str = None # explicit static libs (space-separated abs paths, from linker.static_libs array) LinkerDynamicLibs: str = None # explicit dynamic libs (space-separated abs paths, from linker.dynamic_libs array, .dll/.so) TargetTriple: str = None TargetDataLayout: str = None ProjectDir: str = None # directory containing project.vpj (set by resolve_paths) Sha1SliceLevel: int = 0 # SHA1 slice level (directory dispersion depth) StrictMode: int = 0 IncludesDir: str = None # includes dir path (first element of includes array, single-dir compat) IncludesDirs: str = None # all includes dirs (space-separated, multi-dir support) Qt6Path: str = None # runtime.qt6_path: Qt6 install root (optional) Qt6Dlls: str = None # runtime.qt6_dlls joined (space-separated DLL names; optional) Qt6Plugins: str = None # runtime.qt6_plugins joined (space-separated paths relative to /plugins; optional) # includes prebuilt cache dir suffix (asan mode uses .binary_asan to isolate instrumented obj) _inc_bin_suffix: str = ".binary" # Emission backend: single-track MLIR (since 2026-10-03). # The tree now uses includes/mlirlite; BuildPipeline always runs mlir-opt -> mlir-translate to lower back to LLVM IR. def Load_project_config(path: str) -> int: """Load the project config file (project.vpj); 0=success, non-zero=failure.""" global SourceDir, BuildDir, TempDir, OutputDir, ProjectName, ProjectVersion global CompilerCmd, CompilerFlags, LinkerCmd, LinkerFlags, LinkerOutput, LinkerStaticLibs, LinkerDynamicLibs global TargetTriple, TargetDataLayout, Sha1SliceLevel, StrictMode global IncludesDir, IncludesDirs global Qt6Path, Qt6Dlls, Qt6Plugins if path is None: return 1 if _mbuddy is None: return 1 # Open file f: fileio.File* = fileio.File(path, fileio.MODE.R) if f is None: return 1 if f.closed: return 1 # Allocate read buffer CFG_BUF_SIZE: sizet = 8192 buf: bytes = _mbuddy.alloc(CFG_BUF_SIZE) if buf is None: f.close() return 1 bytes_read: int64 = f.read_all(buf, CFG_BUF_SIZE) f.close() if bytes_read <= 0: return 1 # Parse JSON root: JsonValue* = json_parse(_mbuddy, buf) if root is None: return 1 if not root.is_object(): return 1 # Read top-level fields (explicit __getitem__ calls for old compiler binaries) sd_val: JsonValue* = root["source_dir"] SourceDir = sd_val.as_string() if sd_val is not None else None # build_dir: unified build dir (default ./__vpcbuild__); temp and output are subdirs bd_val: JsonValue* = root["build_dir"] if bd_val is not None and bd_val.is_string(): BuildDir = bd_val.as_string() else: BuildDir = "./__vpcbuild__" name_val: JsonValue* = root["name"] ProjectName = name_val.as_string() if name_val is not None else None ver_val: JsonValue* = root["version"] ProjectVersion = ver_val.as_string() if ver_val is not None else None # Read compiler sub-object compiler: JsonValue* = root["compiler"] if compiler is not None and compiler.is_object(): cc_cmd_val: JsonValue* = compiler["cmd"] CompilerCmd = cc_cmd_val.as_string() if cc_cmd_val is not None else None # Read compiler.flags array, join into a space-separated string cc_flags_val: JsonValue* = compiler["flags"] CompilerFlags = _join_flags_array(cc_flags_val) # Read linker sub-object linker: JsonValue* = root["linker"] if linker is not None and linker.is_object(): ld_cmd_val: JsonValue* = linker["cmd"] LinkerCmd = ld_cmd_val.as_string() if ld_cmd_val is not None else None ld_out_val: JsonValue* = linker["output"] LinkerOutput = ld_out_val.as_string() if ld_out_val is not None else None # Read linker.flags array, join into a space-separated string ld_flags_val: JsonValue* = linker["flags"] LinkerFlags = _join_flags_array(ld_flags_val) # mingw-w64:同时链接 -lmsvcrt 与 -lucrt 时,GNU ld 会从两个 import 库各拉出一个都定义 # __imp_ 的成员(现场:__imp_ldexp 同时出现在 libmsvcrt 与 # libapi-ms-win-crt-math),直接报 "multiple definition"。lld 按需解析 import 库不会冲突, # 仓库默认链接命令也一直是 lld;项目配置漏写 -fuse-ld 时在这里补齐,避免逐个测试改配置。 if LinkerCmd is not None and LinkerFlags is not None and string.strstr(LinkerCmd, "clang") is not None: if string.strstr(LinkerFlags, "-fuse-ld") is None and string.strstr(LinkerFlags, "-lucrt") is not None: lf_cap: sizet = string.strlen(LinkerFlags) + 32 lf_new: str = _mbuddy.alloc(lf_cap) if lf_new is not None: viperlib.snprintf(lf_new, lf_cap, "-fuse-ld=lld %s", LinkerFlags) LinkerFlags = lf_new # Read linker.static_libs array: explicit static libs (.a/.obj); joined raw here, # then resolved to abs paths against project_dir in resolve_paths. sl_val: JsonValue* = linker["static_libs"] LinkerStaticLibs = _join_flags_array(sl_val) # Read linker.dynamic_libs array: explicit dynamic libs (.dll/.so); joined raw here, # then resolved to abs paths against project_dir in resolve_paths. dl_val: JsonValue* = linker["dynamic_libs"] LinkerDynamicLibs = _join_flags_array(dl_val) # Read target sub-object target: JsonValue* = root["target"] if target is not None and target.is_object(): tgt_triple_val: JsonValue* = target["triple"] TargetTriple = tgt_triple_val.as_string() if tgt_triple_val is not None else None tgt_dl_val: JsonValue* = target["datalayout"] TargetDataLayout = tgt_dl_val.as_string() if tgt_dl_val is not None else None # Read options sub-object # Sha1SliceLevel: SHA1 slice level (default 1; each level uses 2 SHA1 chars: 0=none, 1=b7/, 2=b7/90/) options: JsonValue* = root["options"] Sha1SliceLevel = 1 StrictMode = 1 if options is not None and options.is_object(): sl: JsonValue* = options["sha1_slice_level"] if sl is not None and sl.is_int(): Sha1SliceLevel = int(sl.as_int()) sm: JsonValue* = options["strict_mode"] if sm is not None and sm.is_bool(): StrictMode = 1 if sm.as_bool() else 0 # Read includes array (multi-dir: first element -> IncludesDir, all joined -> IncludesDirs) includes_val: JsonValue* = root["includes"] if includes_val is not None and includes_val.is_array(): arr_len: sizet = len(includes_val) inc_buf: bytes = stdlib.malloc(2048) if inc_buf is not None: inc_buf[0] = '\0' inc_pos: sizet = 0 for inc_i in range(int(arr_len)): inc_item: JsonValue* = includes_val.get_item(inc_i) if inc_item is None or not inc_item.is_string(): continue inc_str: str = inc_item.as_string() inc_slen: sizet = string.strlen(inc_str) if inc_i == 0: IncludesDir = inc_str # Append into IncludesDirs (space-separated) if inc_pos + inc_slen + 1 < 2048: if inc_pos > 0: inc_buf[inc_pos] = ' ' inc_pos += 1 string.strcpy(inc_buf + inc_pos, inc_str) inc_pos += inc_slen inc_buf[inc_pos] = '\0' IncludesDirs = inc_buf if IncludesDir is None: IncludesDir = inc_buf # Read runtime sub-object (optional): declared runtime deps deployed next to the exe. # qt6_dlls / qt6_plugins join via _join_flags_array (space-separated, same口径 as linker.static_libs). runtime: JsonValue* = root["runtime"] if runtime is not None and runtime.is_object(): qp_val: JsonValue* = runtime["qt6_path"] Qt6Path = qp_val.as_string() if qp_val is not None else None qd_val: JsonValue* = runtime["qt6_dlls"] Qt6Dlls = _join_flags_array(qd_val) qpl_val: JsonValue* = runtime["qt6_plugins"] Qt6Plugins = _join_flags_array(qpl_val) return 0 def _join_flags_array(flags_val: JsonValue*) -> str: """Join a JSON string array into a space-separated string; None if empty/invalid.""" if flags_val is None: return None if not flags_val.is_array(): return None arr_len: sizet = len(flags_val) if arr_len == 0: return None # First pass: compute total length (all flags + space separators) total_len: sizet = 0 for i in range(int(arr_len)): item: JsonValue* = flags_val.get_item(i) if item is not None and item.is_string(): s: str = item.as_string() if s is not None: total_len += string.strlen(s) + 1 # +1 for space if total_len == 0: return None # Allocate buffer (total_len includes a trailing space per flag; last one becomes '\0') buf: str = _mbuddy.alloc(total_len + 1) if buf is None: return None # Second pass: join (flags separated by spaces) pos: sizet = 0 for i in range(int(arr_len)): item = flags_val.get_item(i) if item is not None and item.is_string(): s = item.as_string() if s is not None: slen: sizet = string.strlen(s) if pos > 0: buf[pos] = ' ' pos += 1 string.strcpy(buf + pos, s) pos += slen buf[pos] = '\0' return buf def _join_path(rel: str, project_dir: str) -> str: """Join a relative path with project_dir, expanding env vars and normalizing it.""" if rel is None: return None # Env var expansion: only when '%' present (fileio.ExpandEnvironmentPath returns None on # failure/no '%'); on failure keep the original string unchanged. ep: str = fileio.ExpandEnvironmentPath(rel) if ep is not None: rel = ep # Absolute paths (starting with / or \) stay unchanged if rel[0] == '/' or rel[0] == '\\': return rel # Skip joining when rel already has the project_dir prefix (avoids double paths); boundary at separator or end if project_dir is not None: pdl: sizet = string.strlen(project_dir) if pdl > 0 and string.strncmp(rel, project_dir, pdl) == 0: if rel[pdl] == '/' or rel[pdl] == '\\' or rel[pdl] == '\0': return rel rel_len: sizet = string.strlen(rel) dir_len: sizet = string.strlen(project_dir) # Handle ../ or ..\ prefix: join project_dir/../... if rel_len >= 3 and rel[0] == '.' and rel[1] == '.': if rel[2] == '/' or rel[2] == '\\': if dir_len == 0: return rel total_len: sizet = dir_len + 1 + rel_len + 1 buf: str = _mbuddy.alloc(total_len) if buf is None: return rel viperlib.snprintf(buf, total_len, "%s/%s", project_dir, rel) return _normalize_path(buf) # Handle ./ or .\ prefix: join project_dir + rel[2:] rel_off: sizet = 0 if rel_len >= 2 and rel[0] == '.': if rel[1] == '/' or rel[1] == '\\': rel_off = 2 else: return rel # not starting with ./: keep unchanged else: return rel # not starting with .: keep unchanged if dir_len == 0: # project_dir empty (current dir): just strip the ./ prefix return rel + rel_off # Join: project_dir + "/" + rel[rel_off:] total_len = dir_len + 1 + (rel_len - rel_off) + 1 buf = _mbuddy.alloc(total_len) if buf is None: return rel # allocation failed: return original path viperlib.snprintf(buf, total_len, "%s/%s", project_dir, rel + rel_off) return _normalize_path(buf) def _normalize_path(path: str) -> str: """Normalize a path by resolving . and .. segments; absolute paths pass through.""" if path is None: return None plen: sizet = string.strlen(path) if plen == 0: return path # Absolute paths (/ or \ start, or drive letter like C:) are not normalized if path[0] == '/' or path[0] == '\\': return path if plen >= 2 and path[1] == ':': return path # Output buffer (worst case = input length + possible ./ prefix) out: str = _mbuddy.alloc(plen + 3) if out is None: return path seg_start: sizet = 0 out_len: sizet = 0 has_seg: int = 0 for i in range(int(plen + 1)): ch: char = '\0' if i < plen: ch = path[i] if ch == '/' or ch == '\\' or ch == '\0': seg_len: sizet = i - seg_start if seg_len > 0: # Single "." segment: skip if seg_len == 1 and path[seg_start] == '.': pass # Single ".." segment: pop previous; leading ".." with nothing to pop must be kept (like Python normpath) elif seg_len == 2 and path[seg_start] == '.' and path[seg_start + 1] == '.': # Whether the last segment is already a leading ".." (standalone: at start or preceded by a separator) last_is_dotdot: int = 0 if out_len >= 2 and out[out_len - 1] == '.' and out[out_len - 2] == '.': if out_len == 2: last_is_dotdot = 1 elif out[out_len - 3] == '/' or out[out_len - 3] == '\\': last_is_dotdot = 1 if out_len > 0 and last_is_dotdot == 0: # Back up to the previous separator k: sizet = out_len while k > 0 and out[k - 1] != '/' and out[k - 1] != '\\': k -= 1 if k > 0: out_len = k - 1 else: out_len = 0 # Keep separator logic when segments remain if out_len > 0: has_seg = 1 else: has_seg = 0 else: # No normal segment to pop: append ".." as a normal segment (keep leading ..) if has_seg > 0: out[out_len] = '/' out_len += 1 out[out_len] = '.' out[out_len + 1] = '.' out_len += 2 has_seg = 1 else: # Normal segment: append separator + segment if has_seg > 0: out[out_len] = '/' out_len += 1 for jj in range(int(seg_len)): out[out_len] = path[seg_start + jj] out_len += 1 has_seg = 1 seg_start = i + 1 if has_seg == 0: # All segments consumed (e.g. ./..): return current dir return "./" # Re-add ./ prefix for relative paths (matches original style; except those starting with ..) if out[0] != '.' and out[0] != '/': # Shift right by 2 to make room for "./" (copy back-to-front to avoid overwriting) mv: sizet = out_len while mv > 0: mv -= 1 out[mv + 2] = out[mv] out[0] = '.' out[1] = '/' out_len += 2 out[out_len] = '\0' return out def resolve_paths(project_dir: str) -> int: """Resolve and normalize project paths (src/build/includes); 0=success.""" global SourceDir, BuildDir, TempDir, OutputDir, IncludesDir, IncludesDirs global ProjectDir, LinkerStaticLibs, LinkerDynamicLibs if project_dir is None: return 1 if _mbuddy is None: return 1 ProjectDir = project_dir SourceDir = _join_path(SourceDir, project_dir) BuildDir = _join_path(BuildDir, project_dir) # TempDir = {BuildDir}/temp; OutputDir = {BuildDir}/output if BuildDir is not None: bd_len: sizet = string.strlen(BuildDir) temp_buf: str = _mbuddy.alloc(bd_len + 8) if temp_buf is not None: viperlib.snprintf(temp_buf, bd_len + 8, "%s/temp", BuildDir) TempDir = temp_buf # OutputDir = {BuildDir}/output output_buf: str = _mbuddy.alloc(bd_len + 8) if output_buf is not None: viperlib.snprintf(output_buf, bd_len + 8, "%s/output", BuildDir) OutputDir = output_buf # includes paths may be of the form ../includes, resolved against project_dir. # Normalize IncludesDir (first element) and IncludesDirs (all, space-separated) item by item. if IncludesDirs is not None: id_total: sizet = string.strlen(IncludesDirs) id_out: str = _mbuddy.alloc(id_total * 3 + 64) if id_out is not None: id_out[0] = '\0' id_pos: sizet = 0 id_i: sizet = 0 while id_i < id_total: while id_i < id_total and IncludesDirs[id_i] == ' ': id_i += 1 if id_i >= id_total: break id_start: sizet = id_i while id_i < id_total and IncludesDirs[id_i] != ' ': id_i += 1 seg_len: sizet = id_i - id_start if seg_len > 0: seg_buf: str = _mbuddy.alloc(seg_len + 1) if seg_buf is not None: string.strncpy(seg_buf, IncludesDirs + id_start, seg_len) seg_buf[seg_len] = '\0' joined: str = _join_path(seg_buf, project_dir) if joined is not None: jl: sizet = string.strlen(joined) if id_pos > 0: id_out[id_pos] = ' ' id_pos += 1 string.strcpy(id_out + id_pos, joined) id_pos += jl id_out[id_pos] = '\0' IncludesDirs = id_out if IncludesDir is not None: IncludesDir = _join_path(IncludesDir, project_dir) # linker.static_libs explicit paths resolved segment-by-segment to abs paths against project_dir if LinkerStaticLibs is not None: sl_total: sizet = string.strlen(LinkerStaticLibs) sl_out: str = _mbuddy.alloc(sl_total * 3 + 64) if sl_out is not None: sl_out[0] = '\0' sl_pos: sizet = 0 sl_i: sizet = 0 while sl_i < sl_total: while sl_i < sl_total and LinkerStaticLibs[sl_i] == ' ': sl_i += 1 if sl_i >= sl_total: break sl_start: sizet = sl_i while sl_i < sl_total and LinkerStaticLibs[sl_i] != ' ': sl_i += 1 seg_len: sizet = sl_i - sl_start if seg_len > 0: seg_buf: str = _mbuddy.alloc(seg_len + 1) if seg_buf is not None: string.strncpy(seg_buf, LinkerStaticLibs + sl_start, seg_len) seg_buf[seg_len] = '\0' joined: str = _join_path(seg_buf, project_dir) if joined is not None: jl: sizet = string.strlen(joined) if sl_pos > 0: sl_out[sl_pos] = ' ' sl_pos += 1 string.strcpy(sl_out + sl_pos, joined) sl_pos += jl sl_out[sl_pos] = '\0' LinkerStaticLibs = sl_out # linker.dynamic_libs explicit files (.dll/.so) likewise resolved to abs paths against project_dir. if LinkerDynamicLibs is not None: dl_total: sizet = string.strlen(LinkerDynamicLibs) dl_out: str = _mbuddy.alloc(dl_total * 3 + 64) if dl_out is not None: dl_out[0] = '\0' dl_pos: sizet = 0 dl_i: sizet = 0 while dl_i < dl_total: while dl_i < dl_total and LinkerDynamicLibs[dl_i] == ' ': dl_i += 1 if dl_i >= dl_total: break dl_start: sizet = dl_i while dl_i < dl_total and LinkerDynamicLibs[dl_i] != ' ': dl_i += 1 seg_len: sizet = dl_i - dl_start if seg_len > 0: seg_buf: str = _mbuddy.alloc(seg_len + 1) if seg_buf is not None: string.strncpy(seg_buf, LinkerDynamicLibs + dl_start, seg_len) seg_buf[seg_len] = '\0' joined: str = _join_path(seg_buf, project_dir) if joined is not None: jl: sizet = string.strlen(joined) if dl_pos > 0: dl_out[dl_pos] = ' ' dl_pos += 1 string.strcpy(dl_out + dl_pos, joined) dl_pos += jl dl_out[dl_pos] = '\0' LinkerDynamicLibs = dl_out # -T