1121 lines
42 KiB
Plaintext
1121 lines
42 KiB
Plaintext
"""Compiler-side implementation of the `viperbuild` built-in macro library (--src Pass 0)."""
|
|
from stdint import *
|
|
from win32.win32base import *
|
|
import memhub
|
|
import string
|
|
import stdlib
|
|
import ast
|
|
import win32.win32file as win32file
|
|
import lib.Projectrans.Utils as Utils
|
|
import lib.Projectrans.Config as Config
|
|
import lib.core.VLogger as VLogger
|
|
import lib.core.IncludesScanner as IncludesScanner
|
|
|
|
# kernel32!GetEnvironmentVariableA (DWORD(LPCSTR, LPSTR, DWORD)) - declared here, same as includes/traceback.vp;
|
|
# win32base.vp omits it but kernel32 is linked anyway; only for selfmachine host detection.
|
|
extern def GetEnvironmentVariableA(lpName: LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD
|
|
|
|
# Path/source buffer caps (same convention as main.vp's SRC_BUF_SIZE)
|
|
define VB_PATH_CAP = 1024
|
|
define VB_SRC_CAP = 1048576
|
|
# Scratch pool size (BFS working set + constant tables; not part of main-pool reset)
|
|
define VB_SCRATCH = 8388608
|
|
# Visited cap (cycle defense; fatal if the closure greatly exceeds it)
|
|
define VB_MAX_VISIT = 4096
|
|
# Element separator for list constants: the char code of newline (source paths/flags never contain newlines).
|
|
# Must store the code then char(): in Viper 'c' literals are single-char strings (i8*), not assignable to a char slot.
|
|
define VB_SEP_CODE = 10
|
|
|
|
# Module state
|
|
_scratch: memhub.MemBuddy* = None
|
|
_entry_dir: str = None # entry file dir (default source; normalized to "." for bare filenames)
|
|
_search_dirs: str = None # closure search roots (space-separated): entry dir first, then "."
|
|
_work: list[str]* = None # BFS pending file paths
|
|
_visited: list[str]* = None # processed file paths
|
|
_pending: list[str]* = None # import module names collected in one file (pending BFS)
|
|
_proj_vars: list[str]* = None # variable names bound to a vb.Project(...) instance
|
|
_alias: str = None # local alias of viperbuild (import viperbuild as vb -> "vb")
|
|
_ctor_name: str = None # from viperbuild import Project as P -> "P"
|
|
|
|
# Per-file constant tables (valid only while scanning that file; pool-allocated)
|
|
_f_str_names: list[str]* = None
|
|
_f_str_vals: list[str]* = None
|
|
_f_list_names: list[str]* = None
|
|
_f_list_vals: list[str]* = None
|
|
|
|
# Constant-eval output slot
|
|
_g_out_str: str = None
|
|
|
|
# Extracted config (all strings stdlib.malloc)
|
|
_c_name: str = None
|
|
_c_version: str = None
|
|
_c_source: str = None
|
|
_c_buildroot: str = None
|
|
_c_includes: str = None
|
|
_c_compiler: str = None
|
|
_c_linker: str = None
|
|
_c_static_libs: str = None
|
|
_c_dynamic_libs: str = None
|
|
_c_output: str = None
|
|
_c_triple: str = None
|
|
_c_datalayout: str = None
|
|
_c_selfmachine: int = 0
|
|
_c_has_project: int = 0
|
|
_c_has_build: int = 0
|
|
|
|
# Whether `import viperbuild` / `from viperbuild import ...` appeared in the closure (diagnostics only)
|
|
_saw_viperbuild: int = 0
|
|
|
|
# Whether applied successfully (for main.vp to decide viperbuild's linker.output)
|
|
_applied: int = 0
|
|
|
|
|
|
# Basic helpers
|
|
def _DupStd(s: str) -> str:
|
|
"""Deep-copy a string with stdlib.malloc so it survives reset_to."""
|
|
if s is None: return None
|
|
n: sizet = string.strlen(s)
|
|
buf: str = stdlib.malloc(n + 1)
|
|
if buf is None: return None
|
|
string.strcpy(buf, s)
|
|
return buf
|
|
|
|
|
|
def _DupScratch(s: str) -> str:
|
|
"""Deep-copy a string into the scratch pool, unaffected by main-pool reset_to."""
|
|
if s is None or _scratch is None: return None
|
|
n: sizet = string.strlen(s)
|
|
buf: str = _scratch.alloc(n + 1)
|
|
if buf is None: return None
|
|
string.strcpy(buf, s)
|
|
return buf
|
|
|
|
|
|
def _Join2(a: str, b: str) -> str:
|
|
"""Join a and b with a slash; returns b when a is empty."""
|
|
if a is None: return b
|
|
if b is None: return a
|
|
al: sizet = string.strlen(a)
|
|
if al == 0: return b
|
|
bl: sizet = string.strlen(b)
|
|
buf: str = stdlib.malloc(al + 1 + bl + 1)
|
|
if buf is None: return None
|
|
string.strcpy(buf, a)
|
|
buf[al] = '/'
|
|
string.strcpy(buf + al + 1, b)
|
|
return buf
|
|
|
|
|
|
def _DirOf(path: str) -> str:
|
|
"""Return the directory part of path (no trailing separator); empty string if none."""
|
|
if path is None: return None
|
|
n: sizet = string.strlen(path)
|
|
i: sizet = n
|
|
while i > 0:
|
|
i -= 1
|
|
ch: char = path[i]
|
|
if ch == '/' or ch == '\\':
|
|
buf: str = stdlib.malloc(i + 1)
|
|
if buf is None: return None
|
|
string.strncpy(buf, path, i)
|
|
buf[i] = '\0'
|
|
return buf
|
|
return _DupStd("")
|
|
|
|
|
|
def _BaseNameOf(path: str) -> str:
|
|
"""Return the file name of path, including its extension."""
|
|
if path is None: return None
|
|
n: sizet = string.strlen(path)
|
|
start: sizet = 0
|
|
for i in range(int(n)):
|
|
ch: char = path[i]
|
|
if ch == '/' or ch == '\\': start = i + 1
|
|
return _DupStd(path + start)
|
|
|
|
|
|
def _IsDir(path: str) -> int:
|
|
"""Whether path is an existing directory; 1=yes."""
|
|
if path is None: return 0
|
|
attr: ULONG = win32file.GetFileAttributesA(path)
|
|
if attr == 0xFFFFFFFF: return 0
|
|
if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: return 1
|
|
return 0
|
|
|
|
|
|
def _HasPrefix(s: str, prefix: str) -> int:
|
|
"""Whether s starts with prefix; 1=yes (None/empty/"." counts as true)."""
|
|
if s is None: return 0
|
|
if prefix is None: return 1
|
|
if string.strcmp(prefix, ".") == 0: return 1
|
|
if string.strncmp(s, "./", 2) == 0: s = s + 2
|
|
if string.strncmp(prefix, "./", 2) == 0: prefix = prefix + 2
|
|
pl: sizet = string.strlen(prefix)
|
|
if pl == 0: return 1
|
|
sl: sizet = string.strlen(s)
|
|
if sl < pl: return 0
|
|
if string.strncmp(s, prefix, pl) == 0: return 1
|
|
return 0
|
|
|
|
|
|
# Constant tables
|
|
def _FindInList(names: list[str]*, name: str) -> int:
|
|
"""Linear-search names for name; return its index, or -1 if not found."""
|
|
if names is None or name is None: return -1
|
|
n: sizet = names.__len__()
|
|
for i in range(int(n)):
|
|
s: str = names.get(i)
|
|
if s is not None:
|
|
if string.strcmp(s, name) == 0: return int(i)
|
|
return -1
|
|
|
|
|
|
def _LookupStrConst(name: str) -> int:
|
|
"""Resolve a name to a string constant in _g_out_str; 0=failure."""
|
|
global _g_out_str
|
|
idx: int = _FindInList(_f_str_names, name)
|
|
if idx < 0: return 0
|
|
v: str = _f_str_vals.get(sizet(idx))
|
|
if v is None: return 0
|
|
_g_out_str = v
|
|
return 1
|
|
|
|
|
|
def _LookupListConst(name: str, out: list[str]*) -> int:
|
|
"""Resolve a name to a list constant, split into out; 0=failure."""
|
|
idx: int = _FindInList(_f_list_names, name)
|
|
if idx < 0: return 0
|
|
v: str = _f_list_vals.get(sizet(idx))
|
|
if v is None: return 0
|
|
return _SplitInto(out, v)
|
|
|
|
|
|
def _SplitInto(out: list[str]*, joined: str) -> int:
|
|
"""Split a VB_SEP_CODE-joined string into out; empty string means empty list."""
|
|
if out is None or joined is None: return 0
|
|
n: sizet = string.strlen(joined)
|
|
if n == 0: return 1
|
|
start: sizet = 0
|
|
for i in range(int(n + 1)):
|
|
if i == n or joined[i] == char(VB_SEP_CODE):
|
|
seg: str = stdlib.malloc(i - start + 1)
|
|
if seg is None: return 0
|
|
string.strncpy(seg, joined + start, i - start)
|
|
seg[i - start] = '\0'
|
|
out.append(_DupScratch(seg))
|
|
stdlib.free(seg)
|
|
start = i + 1
|
|
return 1
|
|
|
|
|
|
def _JoinSep(lst: list[str]*) -> str:
|
|
"""Join a list[str] with VB_SEP_CODE (pool-allocated); None for an empty list."""
|
|
if lst is None: return None
|
|
n: sizet = lst.__len__()
|
|
if n == 0: return None
|
|
# Measure total length first
|
|
total: sizet = 0
|
|
i: sizet = 0
|
|
while i < n:
|
|
s: str = lst.get(i)
|
|
if s is not None: total += string.strlen(s) + 1
|
|
i += 1
|
|
if total == 0: return None
|
|
buf: str = _scratch.alloc(total + 1)
|
|
if buf is None: return None
|
|
pos: sizet = 0
|
|
for i in range(int(n)):
|
|
s = lst.get(i)
|
|
if s is not None:
|
|
sl: sizet = string.strlen(s)
|
|
if pos > 0:
|
|
buf[pos] = char(VB_SEP_CODE)
|
|
pos += 1
|
|
string.strcpy(buf + pos, s)
|
|
pos += sl
|
|
buf[pos] = '\0'
|
|
return buf
|
|
|
|
|
|
def _JoinSpace(lst: list[str]*) -> str:
|
|
"""Join a list[str] with spaces (stdlib-allocated); None for an empty list."""
|
|
if lst is None: return None
|
|
n: sizet = lst.__len__()
|
|
if n == 0: return None
|
|
total: sizet = 0
|
|
i: sizet = 0
|
|
while i < n:
|
|
s: str = lst.get(i)
|
|
if s is not None: total += string.strlen(s) + 1
|
|
i += 1
|
|
if total == 0: return None
|
|
buf: str = stdlib.malloc(total + 1)
|
|
if buf is None: return None
|
|
pos: sizet = 0
|
|
for i in range(int(n)):
|
|
s = lst.get(i)
|
|
if s is not None:
|
|
sl: sizet = string.strlen(s)
|
|
if pos > 0:
|
|
buf[pos] = ' '
|
|
pos += 1
|
|
string.strcpy(buf + pos, s)
|
|
pos += sl
|
|
buf[pos] = '\0'
|
|
return buf
|
|
|
|
|
|
def _FirstSpaceTok(joined: str) -> str:
|
|
"""Return the first space-separated token (stdlib-allocated); None if empty."""
|
|
if joined is None: return None
|
|
n: sizet = string.strlen(joined)
|
|
if n == 0: return None
|
|
sp: sizet = n
|
|
for i in range(int(n)):
|
|
if joined[i] == ' ':
|
|
sp = i
|
|
break
|
|
seg: str = stdlib.malloc(sp + 1)
|
|
if seg is None: return None
|
|
string.strncpy(seg, joined, sp)
|
|
seg[sp] = '\0'
|
|
return seg
|
|
|
|
|
|
# Constant expression evaluation
|
|
def _EvalStr(node: ast.AST*) -> int:
|
|
"""Evaluate a string constant expression into _g_out_str; 0=failure."""
|
|
global _g_out_str
|
|
if node is None: return 0
|
|
k: int = node.kind()
|
|
if k == ast.ASTKind.Constant:
|
|
cn: ast.Constant* = ast.Constant*(node)
|
|
if cn is None or cn.const_kind != ast.CONST_STR: return 0
|
|
_g_out_str = cn.str_val
|
|
return 1
|
|
if k == ast.ASTKind.Name:
|
|
nm: ast.Name* = ast.Name*(node)
|
|
if nm is None or nm.id is None: return 0
|
|
return _LookupStrConst(nm.id)
|
|
if k == ast.ASTKind.BinOp:
|
|
bop: ast.BinOp* = ast.BinOp*(node)
|
|
if bop is None or bop.op != ast.OpKind.Add: return 0
|
|
if _EvalStr(bop.left) == 0: return 0
|
|
lv: str = _g_out_str
|
|
if _EvalStr(bop.right) == 0: return 0
|
|
rv: str = _g_out_str
|
|
_g_out_str = _ConcatStd(lv, rv)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _ConcatStd(a: str, b: str) -> str:
|
|
"""Concatenate two strings with stdlib allocation."""
|
|
if a is None: return b
|
|
if b is None: return a
|
|
al: sizet = string.strlen(a)
|
|
bl: sizet = string.strlen(b)
|
|
buf: str = stdlib.malloc(al + bl + 1)
|
|
if buf is None: return None
|
|
string.strcpy(buf, a)
|
|
string.strcpy(buf + al, b)
|
|
return buf
|
|
|
|
|
|
def _EvalList(node: ast.AST*, out: list[str]*) -> int:
|
|
"""Evaluate a list[str] constant expression into out; 0=failure."""
|
|
if node is None or out is None: return 0
|
|
k: int = node.kind()
|
|
if k == ast.ASTKind.Name:
|
|
nm: ast.Name* = ast.Name*(node)
|
|
if nm is None or nm.id is None: return 0
|
|
return _LookupListConst(nm.id, out)
|
|
if k == ast.ASTKind.List:
|
|
lst: ast.List* = ast.List*(node)
|
|
if lst is None or lst.elts is None: return 0
|
|
n: sizet = lst.elts.__len__()
|
|
for i in range(int(n)):
|
|
e: ast.AST* = lst.elts.get(i)
|
|
if e is None: return 0
|
|
if e.kind() == ast.ASTKind.Starred:
|
|
st: ast.Starred* = ast.Starred*(e)
|
|
if st is None or st.value is None: return 0
|
|
if _EvalList(st.value, out) == 0: return 0
|
|
else:
|
|
if _EvalStr(e) == 0: return 0
|
|
out.append(_DupScratch(_g_out_str))
|
|
return 1
|
|
if k == ast.ASTKind.BinOp:
|
|
bop: ast.BinOp* = ast.BinOp*(node)
|
|
if bop is None or bop.op != ast.OpKind.Add: return 0
|
|
if _EvalList(bop.left, out) == 0: return 0
|
|
if _EvalList(bop.right, out) == 0: return 0
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _EvalBool(node: ast.AST*) -> int:
|
|
"""Evaluate a bool constant; -1 means it is not a bool constant."""
|
|
if node is None: return -1
|
|
if node.kind() != ast.ASTKind.Constant: return -1
|
|
cn: ast.Constant* = ast.Constant*(node)
|
|
if cn is None or cn.const_kind != ast.CONST_BOOL: return -1
|
|
if cn.int_val != 0: return 1
|
|
return 0
|
|
|
|
|
|
# Macro-reference detection (reading is forbidden)
|
|
def _IsProjRef(node: ast.AST*) -> int:
|
|
"""Whether a node directly references a Project macro object; 1=yes."""
|
|
if node is None: return 0
|
|
k: int = node.kind()
|
|
if k == ast.ASTKind.Name:
|
|
nm: ast.Name* = ast.Name*(node)
|
|
if nm is None or nm.id is None: return 0
|
|
if _FindInList(_proj_vars, nm.id) >= 0: return 1
|
|
return 0
|
|
if k == ast.ASTKind.Attribute:
|
|
at: ast.Attribute* = ast.Attribute*(node)
|
|
if at is None or at.value is None: return 0
|
|
return _IsProjRef(at.value)
|
|
return 0
|
|
|
|
|
|
# Single-file scan
|
|
def _ResetFileTables(pool: memhub.MemBuddy*) -> int:
|
|
"""Rebuild the per-file constant tables (pool-allocated)."""
|
|
global _f_str_names, _f_str_vals, _f_list_names, _f_list_vals
|
|
_f_str_names = list[str](pool, 8)
|
|
_f_str_vals = list[str](pool, 8)
|
|
_f_list_names = list[str](pool, 8)
|
|
_f_list_vals = list[str](pool, 8)
|
|
if _f_str_names is None or _f_str_vals is None: return 1
|
|
if _f_list_names is None or _f_list_vals is None: return 1
|
|
return 0
|
|
|
|
|
|
def _AddPending(mod_name: str) -> None:
|
|
"""Append a module name to the BFS queue (deep-copied into the scratch pool)."""
|
|
if mod_name is None or _pending is None: return
|
|
if string.strlen(mod_name) == 0: return
|
|
_pending.append(_DupScratch(mod_name))
|
|
|
|
|
|
def _HandleImport(stmt: ast.Import*) -> int:
|
|
"""Handle `import X [as Y]`: detect viperbuild, queue other modules for BFS."""
|
|
global _alias, _saw_viperbuild
|
|
if stmt is None or stmt.names is None: return 0
|
|
n: sizet = stmt.names.__len__()
|
|
for i in range(int(n)):
|
|
al: ast.Alias* = ast.Alias*(stmt.names.get(i))
|
|
if al is not None and al.name is not None:
|
|
if string.strcmp(al.name, "viperbuild") == 0:
|
|
_saw_viperbuild = 1
|
|
if al.asname is not None: _alias = _DupScratch(al.asname)
|
|
else: _alias = _DupScratch("viperbuild")
|
|
else: _AddPending(al.name)
|
|
return 0
|
|
|
|
|
|
def _HandleImportFrom(stmt: ast.ImportFrom*) -> int:
|
|
"""Handle `from X import Y`: detect viperbuild's Project, queue other modules."""
|
|
global _ctor_name, _saw_viperbuild
|
|
if stmt is None: return 0
|
|
if stmt.level != 0:
|
|
# Relative imports are not part of the local closure (no package context: skip)
|
|
return 0
|
|
if stmt.module is None: return 0
|
|
if string.strcmp(stmt.module, "viperbuild") != 0:
|
|
_AddPending(stmt.module)
|
|
return 0
|
|
_saw_viperbuild = 1
|
|
if stmt.names is None: return 0
|
|
n: sizet = stmt.names.__len__()
|
|
for i in range(int(n)):
|
|
al: ast.Alias* = ast.Alias*(stmt.names.get(i))
|
|
if al is not None and al.name is not None:
|
|
if string.strcmp(al.name, "Project") == 0:
|
|
if al.asname is not None: _ctor_name = _DupScratch(al.asname)
|
|
else: _ctor_name = _DupScratch("Project")
|
|
return 0
|
|
|
|
|
|
def _IsProjectCtor(vnode: ast.AST*) -> int:
|
|
"""Whether an expression is a vb.Project(...) or Project(...) call; 1=yes."""
|
|
if vnode is None: return 0
|
|
if vnode.kind() != ast.ASTKind.Call: return 0
|
|
cl: ast.Call* = ast.Call*(vnode)
|
|
if cl is None or cl.func is None: return 0
|
|
fk: int = cl.func.kind()
|
|
if fk == ast.ASTKind.Attribute:
|
|
at: ast.Attribute* = ast.Attribute*(cl.func)
|
|
if at is None or at.attr is None or at.value is None: return 0
|
|
if at.value.kind() != ast.ASTKind.Name: return 0
|
|
bn: ast.Name* = ast.Name*(at.value)
|
|
if bn is None or bn.id is None or _alias is None: return 0
|
|
if string.strcmp(bn.id, _alias) != 0: return 0
|
|
if string.strcmp(at.attr, "Project") == 0: return 1
|
|
return 0
|
|
if fk == ast.ASTKind.Name:
|
|
nm: ast.Name* = ast.Name*(cl.func)
|
|
if nm is None or nm.id is None or _ctor_name is None: return 0
|
|
if string.strcmp(nm.id, _ctor_name) == 0: return 1
|
|
return 0
|
|
|
|
|
|
def _ApplyCtorArgs(cl: ast.Call*) -> int:
|
|
"""Extract name/version from Project(...) args into _c_name / _c_version."""
|
|
global _c_name, _c_version
|
|
if cl is None: return 0
|
|
if cl.args is not None:
|
|
an: sizet = cl.args.__len__()
|
|
if an > 2:
|
|
VLogger.error("viperbuild: Project(...) accepts at most 2 positional arguments (name, version)", "viperbuild")
|
|
return 1
|
|
if an >= 1:
|
|
if _EvalStr(cl.args.get(0)) == 0:
|
|
VLogger.error("viperbuild: Project(name) must be a string constant", "viperbuild")
|
|
return 1
|
|
_c_name = _DupStd(_g_out_str)
|
|
if an >= 2:
|
|
if _EvalStr(cl.args.get(1)) == 0:
|
|
VLogger.error("viperbuild: Project(version) must be a string constant", "viperbuild")
|
|
return 1
|
|
_c_version = _DupStd(_g_out_str)
|
|
if cl.keywords is None: return 0
|
|
n: sizet = cl.keywords.__len__()
|
|
for i in range(int(n)):
|
|
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
|
|
if kw is not None and kw.arg is not None and kw.value is not None:
|
|
if string.strcmp(kw.arg, "name") == 0:
|
|
if _EvalStr(kw.value) == 0:
|
|
VLogger.error("viperbuild: Project(name=...) must be a string constant", "viperbuild")
|
|
return 1
|
|
_c_name = _DupStd(_g_out_str)
|
|
elif string.strcmp(kw.arg, "version") == 0:
|
|
if _EvalStr(kw.value) == 0:
|
|
VLogger.error("viperbuild: Project(version=...) must be a string constant", "viperbuild")
|
|
return 1
|
|
_c_version = _DupStd(_g_out_str)
|
|
else:
|
|
VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for Project(...)", "viperbuild")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _SetField(fname: str, vnode: ast.AST*) -> int:
|
|
"""Apply a proj.<fname> assignment or config(<fname>=...) keyword; 0=success."""
|
|
global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker
|
|
global _c_static_libs, _c_dynamic_libs, _c_output, _c_name, _c_version
|
|
if fname is None or vnode is None: return 1
|
|
# ---- String fields ----
|
|
if string.strcmp(fname, "source") == 0:
|
|
if _EvalStr(vnode) == 0:
|
|
VLogger.error("viperbuild: source must be a string constant expression", "viperbuild")
|
|
return 1
|
|
_c_source = _DupStd(_g_out_str)
|
|
return 0
|
|
if string.strcmp(fname, "buildroot") == 0:
|
|
if _EvalStr(vnode) == 0:
|
|
VLogger.error("viperbuild: buildroot must be a string constant expression", "viperbuild")
|
|
return 1
|
|
_c_buildroot = _DupStd(_g_out_str)
|
|
return 0
|
|
if string.strcmp(fname, "output") == 0:
|
|
if _EvalStr(vnode) == 0:
|
|
VLogger.error("viperbuild: output must be a string constant expression", "viperbuild")
|
|
return 1
|
|
_c_output = _DupStd(_g_out_str)
|
|
return 0
|
|
if string.strcmp(fname, "name") == 0:
|
|
if _EvalStr(vnode) == 0:
|
|
VLogger.error("viperbuild: name must be a string constant expression", "viperbuild")
|
|
return 1
|
|
_c_name = _DupStd(_g_out_str)
|
|
return 0
|
|
if string.strcmp(fname, "version") == 0:
|
|
if _EvalStr(vnode) == 0:
|
|
VLogger.error("viperbuild: version must be a string constant expression", "viperbuild")
|
|
return 1
|
|
_c_version = _DupStd(_g_out_str)
|
|
return 0
|
|
# ---- List fields ----
|
|
if string.strcmp(fname, "includes") == 0: return _SetListField(0, vnode)
|
|
if string.strcmp(fname, "compiler") == 0: return _SetListField(1, vnode)
|
|
if string.strcmp(fname, "linker") == 0: return _SetListField(2, vnode)
|
|
if string.strcmp(fname, "static_libs") == 0: return _SetListField(3, vnode)
|
|
if string.strcmp(fname, "dynamic_libs") == 0: return _SetListField(4, vnode)
|
|
VLogger.error(f"viperbuild: unknown field proj.{fname}", "viperbuild")
|
|
return 1
|
|
|
|
|
|
def _SetListField(slot: int, vnode: ast.AST*) -> int:
|
|
"""Assign a list field (0=includes, 1=compiler, 2=linker, 3=static_libs, 4=dynamic_libs)."""
|
|
global _c_includes, _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs
|
|
if _scratch is None: return 1
|
|
tmp: list[str]* = list[str](_scratch, 8)
|
|
if tmp is None: return 1
|
|
if _EvalList(vnode, tmp) == 0:
|
|
VLogger.error("viperbuild: list fields must be list[str] constant expressions", "viperbuild")
|
|
return 1
|
|
joined: str = _JoinSpace(tmp)
|
|
if slot == 0: _c_includes = joined
|
|
elif slot == 1: _c_compiler = joined
|
|
elif slot == 2: _c_linker = joined
|
|
elif slot == 3: _c_static_libs = joined
|
|
else: _c_dynamic_libs = joined
|
|
return 0
|
|
|
|
|
|
def _HandleAssign(stmt: ast.Assign*) -> int:
|
|
"""Handle an assignment: constant, Project instance, or proj field."""
|
|
if stmt is None or stmt.targets is None or stmt.value is None: return 0
|
|
if stmt.targets.__len__() != 1: return 0
|
|
tgt: ast.AST* = stmt.targets.get(0)
|
|
if tgt is None: return 0
|
|
tk: int = tgt.kind()
|
|
if tk == ast.ASTKind.Name: return _HandleNameBind(ast.Name*(tgt), stmt.value)
|
|
if tk == ast.ASTKind.Attribute: return _HandleAttrAssign(ast.Attribute*(tgt), stmt.value)
|
|
return 0
|
|
|
|
|
|
def _HandleAnnAssign(stmt: ast.AnnAssign*) -> int:
|
|
"""Handle an annotated assignment (including define-desugared AnnAssign)."""
|
|
if stmt is None or stmt.target is None or stmt.value is None: return 0
|
|
if stmt.target.kind() != ast.ASTKind.Name: return 0
|
|
return _HandleNameBind(ast.Name*(stmt.target), stmt.value)
|
|
|
|
|
|
def _HandleNameBind(tgt: ast.Name*, vnode: ast.AST*) -> int:
|
|
"""Handle NAME = value: Project binding, string constant, or list constant."""
|
|
global _c_has_project
|
|
if tgt is None or vnode is None or tgt.id is None: return 0
|
|
# Reading a Project macro object on the RHS is fatal (reading forbidden)
|
|
if _IsProjRef(vnode) != 0:
|
|
VLogger.error(f"viperbuild: reading the Project macro object is forbidden ({tgt.id} = proj ...)", "viperbuild")
|
|
return 1
|
|
# Project instance binding
|
|
if _IsProjectCtor(vnode) != 0:
|
|
if _c_has_project != 0:
|
|
VLogger.error("viperbuild: only one Project object is allowed per dependency closure", "viperbuild")
|
|
return 1
|
|
_c_has_project = 1
|
|
_proj_vars.append(_DupScratch(tgt.id))
|
|
cl: ast.Call* = ast.Call*(vnode)
|
|
return _ApplyCtorArgs(cl)
|
|
# String constant
|
|
if _EvalStr(vnode) != 0:
|
|
_f_str_names.append(tgt.id)
|
|
_f_str_vals.append(_DupScratch(_g_out_str))
|
|
return 0
|
|
# List constant
|
|
if _scratch is not None:
|
|
tmp: list[str]* = list[str](_scratch, 8)
|
|
if tmp is not None:
|
|
if _EvalList(vnode, tmp) != 0:
|
|
_f_list_names.append(tgt.id)
|
|
_f_list_vals.append(_JoinSep(tmp))
|
|
return 0
|
|
tmp.clear()
|
|
return 0
|
|
|
|
|
|
def _HandleAttrAssign(tgt: ast.Attribute*, vnode: ast.AST*) -> int:
|
|
"""Handle proj.<field> = <constant expression>."""
|
|
if tgt is None or tgt.attr is None or tgt.value is None: return 0
|
|
if tgt.value.kind() != ast.ASTKind.Name: return 0
|
|
bn: ast.Name* = ast.Name*(tgt.value)
|
|
if bn is None or bn.id is None: return 0
|
|
if _FindInList(_proj_vars, bn.id) < 0: return 0
|
|
if _IsProjRef(vnode) != 0:
|
|
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
|
|
return 1
|
|
return _SetField(tgt.attr, vnode)
|
|
|
|
|
|
def _HandleConfigCall(cl: ast.Call*) -> int:
|
|
"""Handle a proj.config(kw=...) call."""
|
|
if cl is None or cl.keywords is None: return 0
|
|
n: sizet = cl.keywords.__len__()
|
|
for i in range(int(n)):
|
|
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
|
|
if kw is not None and kw.arg is not None and kw.value is not None:
|
|
if _SetField(kw.arg, kw.value) != 0: return 1
|
|
return 0
|
|
|
|
|
|
def _HandleBuildCall(cl: ast.Call*) -> int:
|
|
"""Handle a proj.build(triple=, datalayout=, selfmachine=) call."""
|
|
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_build
|
|
if cl is None: return 1
|
|
if _c_has_build != 0:
|
|
VLogger.error("viperbuild: proj.build() may be called only once", "viperbuild")
|
|
return 1
|
|
_c_has_build = 1
|
|
if cl.keywords is None: return 0
|
|
n: sizet = cl.keywords.__len__()
|
|
for i in range(int(n)):
|
|
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
|
|
if kw is not None and kw.arg is not None and kw.value is not None:
|
|
if string.strcmp(kw.arg, "triple") == 0:
|
|
if _EvalStr(kw.value) == 0:
|
|
VLogger.error("viperbuild: build(triple=...) must be a string constant", "viperbuild")
|
|
return 1
|
|
_c_triple = _DupStd(_g_out_str)
|
|
elif string.strcmp(kw.arg, "datalayout") == 0:
|
|
if _EvalStr(kw.value) == 0:
|
|
VLogger.error("viperbuild: build(datalayout=...) must be a string constant", "viperbuild")
|
|
return 1
|
|
_c_datalayout = _DupStd(_g_out_str)
|
|
elif string.strcmp(kw.arg, "selfmachine") == 0:
|
|
b: int = _EvalBool(kw.value)
|
|
if b < 0:
|
|
VLogger.error("viperbuild: build(selfmachine=...) must be a boolean constant", "viperbuild")
|
|
return 1
|
|
_c_selfmachine = b
|
|
else:
|
|
VLogger.error(f"viperbuild: unsupported keyword {kw.arg} for build(...)", "viperbuild")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def _CallArgsHaveProjRef(cl: ast.Call*) -> int:
|
|
"""Whether any call argument references a Project macro object; 1=yes."""
|
|
if cl is None: return 0
|
|
if cl.args is not None:
|
|
n: sizet = cl.args.__len__()
|
|
for i in range(int(n)):
|
|
if _IsProjRef(cl.args.get(i)) != 0: return 1
|
|
if cl.keywords is not None:
|
|
n = cl.keywords.__len__()
|
|
for i in range(int(n)):
|
|
kw: ast.Keyword* = ast.Keyword*(cl.keywords.get(i))
|
|
if kw is not None and kw.value is not None:
|
|
if _IsProjRef(kw.value) != 0: return 1
|
|
return 0
|
|
|
|
|
|
def _HandleExprStmt(stmt: ast.Expr*) -> int:
|
|
"""Handle an expression statement: proj.config/build calls, else check reads."""
|
|
if stmt is None or stmt.value is None: return 0
|
|
v: ast.AST* = stmt.value
|
|
if v.kind() != ast.ASTKind.Call: return 0
|
|
cl: ast.Call* = ast.Call*(v)
|
|
if cl is None or cl.func is None: return 0
|
|
# Non-Project method call: if an argument reads the macro object -> fatal (reading forbidden)
|
|
if cl.func.kind() != ast.ASTKind.Attribute:
|
|
if _CallArgsHaveProjRef(cl) != 0:
|
|
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
|
|
return 1
|
|
return 0
|
|
at: ast.Attribute* = ast.Attribute*(cl.func)
|
|
if at is None or at.attr is None or at.value is None: return 0
|
|
if at.value.kind() != ast.ASTKind.Name:
|
|
if _CallArgsHaveProjRef(cl) != 0:
|
|
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
|
|
return 1
|
|
return 0
|
|
bn: ast.Name* = ast.Name*(at.value)
|
|
if bn is None or bn.id is None: return 0
|
|
if _FindInList(_proj_vars, bn.id) < 0:
|
|
if _CallArgsHaveProjRef(cl) != 0:
|
|
VLogger.error("viperbuild: reading the Project macro object is forbidden", "viperbuild")
|
|
return 1
|
|
return 0
|
|
if string.strcmp(at.attr, "config") == 0: return _HandleConfigCall(cl)
|
|
if string.strcmp(at.attr, "build") == 0: return _HandleBuildCall(cl)
|
|
VLogger.error(f"viperbuild: unsupported method {at.attr}() on Project object", "viperbuild")
|
|
return 1
|
|
|
|
|
|
def _ScanFile(pool: memhub.MemBuddy*, path: str) -> int:
|
|
"""Parse and scan one .vp file; 0=ok, 1=failed, 2=skippable (unreadable/not .vp)."""
|
|
if path is None: return 2
|
|
sl: sizet = string.strlen(path)
|
|
if sl == 0: return 2
|
|
if Utils.is_vp_at(path, 0, sl) == 0:
|
|
# Not .vp: excluded from the closure (.vpi/.vpnci invisible to the compiler)
|
|
return 2
|
|
|
|
src_buf: bytes = Utils.read_file_to_buf(path, VB_SRC_CAP)
|
|
if src_buf is None:
|
|
VLogger.warning(f"viperbuild: cannot read dependency file {path}", "viperbuild")
|
|
return 2
|
|
|
|
if _ResetFileTables(pool) != 0:
|
|
stdlib.free(src_buf)
|
|
VLogger.error("viperbuild: constant table allocation failed", "viperbuild")
|
|
return 1
|
|
|
|
mark: sizet = pool.mark()
|
|
lx: ast.Lexer* = ast.new_lexer(pool)
|
|
if lx is None:
|
|
pool.reset_to(mark)
|
|
stdlib.free(src_buf)
|
|
VLogger.error("viperbuild: lexer allocation failed", "viperbuild")
|
|
return 1
|
|
ast._lexer_init(lx, src_buf, pool)
|
|
tokens: ast.Token* = ast.tokenize(lx)
|
|
if tokens is None:
|
|
pool.reset_to(mark)
|
|
stdlib.free(src_buf)
|
|
VLogger.error(f"viperbuild: tokenization failed {path}", "viperbuild")
|
|
return 1
|
|
tree: ast.AST* = ast.parse_tokens(pool, tokens)
|
|
if tree is None:
|
|
pool.reset_to(mark)
|
|
stdlib.free(src_buf)
|
|
VLogger.error(f"viperbuild: parsing failed {path}", "viperbuild")
|
|
return 1
|
|
|
|
body: list[ast.AST*]* = tree.children
|
|
rc: int = 0
|
|
if body is not None:
|
|
cn: sizet = len(body)
|
|
for ci in range(int(cn)):
|
|
child: ast.AST* = body.get(ci)
|
|
if child is not None:
|
|
kd: int = child.kind()
|
|
if kd == ast.ASTKind.Import: _HandleImport(ast.Import*(child))
|
|
elif kd == ast.ASTKind.ImportFrom: _HandleImportFrom(ast.ImportFrom*(child))
|
|
elif kd == ast.ASTKind.Assign: rc = _HandleAssign(ast.Assign*(child))
|
|
elif kd == ast.ASTKind.AnnAssign: rc = _HandleAnnAssign(ast.AnnAssign*(child))
|
|
elif kd == ast.ASTKind.Expr: rc = _HandleExprStmt(ast.Expr*(child))
|
|
if rc != 0: break
|
|
|
|
pool.reset_to(mark)
|
|
stdlib.free(src_buf)
|
|
return rc
|
|
|
|
|
|
# Dependency closure BFS
|
|
def _ResolveModuleToFile(module_name: str) -> str:
|
|
"""Resolve a module name to a local project file path; None if not found."""
|
|
if module_name is None or _search_dirs is None: return None
|
|
if string.strlen(module_name) == 0: return None
|
|
out_abs: rawptr = stdlib.malloc(8)
|
|
out_rel: rawptr = stdlib.malloc(8)
|
|
out_inc: rawptr = stdlib.malloc(8)
|
|
if out_abs is None or out_rel is None or out_inc is None:
|
|
if out_abs is not None: stdlib.free(out_abs)
|
|
if out_rel is not None: stdlib.free(out_rel)
|
|
if out_inc is not None: stdlib.free(out_inc)
|
|
return None
|
|
abs_slot: sizet* = (sizet*)(out_abs)
|
|
rel_slot: sizet* = (sizet*)(out_rel)
|
|
inc_slot: sizet* = (sizet*)(out_inc)
|
|
abs_slot[0] = 0
|
|
rel_slot[0] = 0
|
|
inc_slot[0] = 0
|
|
rcode: int = IncludesScanner.ResolveModuleToPath(_search_dirs, module_name, out_abs, out_rel, out_inc)
|
|
result: str = None
|
|
if rcode == 0 and abs_slot[0] != 0:
|
|
abs_path: str = rawptr(abs_slot[0])
|
|
if abs_path is not None: result = _DupScratch(abs_path)
|
|
if abs_slot[0] != 0: stdlib.free(rawptr(abs_slot[0]))
|
|
if rel_slot[0] != 0: stdlib.free(rawptr(rel_slot[0]))
|
|
if inc_slot[0] != 0: stdlib.free(rawptr(inc_slot[0]))
|
|
stdlib.free(out_abs)
|
|
stdlib.free(out_rel)
|
|
stdlib.free(out_inc)
|
|
return result
|
|
|
|
|
|
def _Enqueue(path: str) -> None:
|
|
"""Add a file path to the BFS queue (deduped against worklist and visited)."""
|
|
if path is None or _work is None: return
|
|
if _FindInList(_visited, path) >= 0: return
|
|
if _FindInList(_work, path) >= 0: return
|
|
_work.append(_DupScratch(path))
|
|
|
|
|
|
def _RunBfs(pool: memhub.MemBuddy*, entry_path: str) -> int:
|
|
"""Expand the dependency closure from the entry file; 0=success."""
|
|
global _pending
|
|
_work.append(_DupScratch(entry_path))
|
|
while _work.__len__() > 0:
|
|
cur: str = _work.pop()
|
|
if cur is None: continue
|
|
if _FindInList(_visited, cur) >= 0: continue
|
|
if _visited.__len__() >= VB_MAX_VISIT:
|
|
VLogger.error(f"viperbuild: dependency closure exceeds {VB_MAX_VISIT} files, likely a cycle", "viperbuild")
|
|
return 1
|
|
_visited.append(_DupScratch(cur))
|
|
|
|
_pending = list[str](_scratch, 8)
|
|
if _pending is None:
|
|
VLogger.error("viperbuild: BFS queue allocation failed", "viperbuild")
|
|
return 1
|
|
rc: int = _ScanFile(pool, cur)
|
|
if rc == 1: return 1
|
|
# Resolve module names collected from this file
|
|
pn: sizet = _pending.__len__()
|
|
for pi in range(int(pn)):
|
|
mod: str = _pending.get(pi)
|
|
if mod is not None:
|
|
dep: str = _ResolveModuleToFile(mod)
|
|
if dep is not None: _Enqueue(dep)
|
|
return 0
|
|
|
|
|
|
# Defaults / validation / apply
|
|
def _DetectHost() -> int:
|
|
"""Detect the host triple/datalayout via PROCESSOR_ARCHITECTURE; 0=success."""
|
|
global _c_triple, _c_datalayout
|
|
buf: CHARPTR = stdlib.malloc(64)
|
|
if buf is None:
|
|
VLogger.error("viperbuild: selfmachine probe buffer allocation failed", "viperbuild")
|
|
return 1
|
|
buf[0] = char(0)
|
|
n: DWORD = GetEnvironmentVariableA("PROCESSOR_ARCHITECTURE", buf, DWORD(64))
|
|
if n == 0 or n >= DWORD(64):
|
|
stdlib.free(buf)
|
|
VLogger.error("viperbuild: selfmachine probe failed (PROCESSOR_ARCHITECTURE unavailable)", "viperbuild")
|
|
return 1
|
|
buf[int(n)] = char(0)
|
|
# "AMD64\0" (A=65 M=77 D=68 6=54 4=52)
|
|
is_amd64: int = 0
|
|
if buf[0] == char(65) and buf[1] == char(77) and buf[2] == char(68) and buf[3] == char(54) and buf[4] == char(52) and buf[5] == char(0): is_amd64 = 1
|
|
if is_amd64 != 0:
|
|
_c_triple = "x86_64-pc-windows-gnu"
|
|
_c_datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
|
|
stdlib.free(buf)
|
|
return 0
|
|
stdlib.free(buf)
|
|
VLogger.error("viperbuild: selfmachine does not support the host architecture yet (currently only AMD64)", "viperbuild")
|
|
return 1
|
|
|
|
|
|
def _ProbeIncludes() -> str:
|
|
"""Probe up to 3 levels above the exe dir for an includes directory; else None."""
|
|
exe_buf: bytes = stdlib.malloc(VB_PATH_CAP)
|
|
if exe_buf is None: return None
|
|
exe_buf[0] = '\0'
|
|
n_exe: ULONG = win32file.GetModuleFileNameA(None, exe_buf, ULONG(VB_PATH_CAP))
|
|
if n_exe <= 0:
|
|
stdlib.free(exe_buf)
|
|
return None
|
|
el: int = int(n_exe)
|
|
if el >= VB_PATH_CAP: el = VB_PATH_CAP - 1
|
|
exe_buf[el] = '\0'
|
|
exe_dir: str = _DirOf(exe_buf)
|
|
stdlib.free(exe_buf)
|
|
if exe_dir is None: return None
|
|
for k in range(int(3 + 1)):
|
|
base: str = _DupStd(exe_dir)
|
|
for j in range(int(k)):
|
|
base = _Join2(base, "..")
|
|
cand: str = _Join2(base, "includes")
|
|
if _IsDir(cand) != 0: return cand
|
|
return None
|
|
|
|
|
|
def _DeriveOutputFromEntry(entry_path: str) -> str:
|
|
"""Derive the default output name from the entry file name."""
|
|
base: str = _BaseNameOf(entry_path)
|
|
if base is None: return "app.exe"
|
|
n: sizet = string.strlen(base)
|
|
cut: sizet = n
|
|
for i in range(int(n)):
|
|
if base[i] == '.':
|
|
cut = i
|
|
break
|
|
head: str = stdlib.malloc(cut + 5)
|
|
if head is None: return "app.exe"
|
|
string.strncpy(head, base, cut)
|
|
head[cut] = '.'
|
|
head[cut + 1] = 'e'
|
|
head[cut + 2] = 'x'
|
|
head[cut + 3] = 'e'
|
|
head[cut + 4] = '\0'
|
|
return head
|
|
|
|
|
|
def _FinalizeCfg(entry_path: str) -> int:
|
|
"""Fill defaults, validate and write back into Config; 0=success."""
|
|
global _c_source, _c_buildroot, _c_includes, _c_compiler, _c_linker
|
|
global _c_static_libs, _c_dynamic_libs, _c_output, _c_triple, _c_datalayout
|
|
global _c_selfmachine
|
|
|
|
if _c_has_project == 0:
|
|
VLogger.error("viperbuild: no vb.Project(...) definition found in the dependency closure", "viperbuild")
|
|
return 1
|
|
if _c_has_build == 0:
|
|
VLogger.error("viperbuild: proj.build(...) was not called, cannot determine the build target", "viperbuild")
|
|
return 1
|
|
|
|
# ---- Defaults ----
|
|
if _c_source is None: _c_source = _DupStd(_entry_dir)
|
|
if _c_buildroot is None: _c_buildroot = _Join2(_c_source, "__vpcbuild__")
|
|
if _c_includes is None:
|
|
probed: str = _ProbeIncludes()
|
|
if probed is None:
|
|
VLogger.error("viperbuild: could not probe an includes directory (no includes up to 0..3 levels above the exe directory)", "viperbuild")
|
|
return 1
|
|
_c_includes = probed
|
|
if _c_compiler is None: _c_compiler = _DupStd("llc -filetype=obj -relocation-model=pic")
|
|
if _c_linker is None:
|
|
# Built-in default = the verified link args from this repo's ViperC/project.vpj.
|
|
# Do NOT add -lgcc: msys64/mingw64 + lld has no libgcc and fails with "unable to find library -lgcc".
|
|
_c_linker = _DupStd("clang++ -fuse-ld=lld -lmsvcrt -lucrt -lpthread -lmingwex -lkernel32 -Wl,--stack,0x1000000 -Wl,--no-insert-timestamp -Wl,--export-all-symbols -g")
|
|
if _c_output is None: _c_output = _DeriveOutputFromEntry(entry_path)
|
|
|
|
# ---- Entry file must be under source ----
|
|
if _HasPrefix(entry_path, _c_source) == 0:
|
|
VLogger.error(f"viperbuild: entry file is not under the source directory\n entry: {entry_path}\n source: {_c_source}", "viperbuild")
|
|
return 1
|
|
|
|
# ---- Target platform ----
|
|
explicit_triple: int = 0
|
|
if _c_triple is not None or _c_datalayout is not None: explicit_triple = 1
|
|
if _c_selfmachine != 0 and explicit_triple != 0:
|
|
VLogger.error("viperbuild: build(selfmachine=True) is mutually exclusive with an explicit triple/datalayout", "viperbuild")
|
|
return 1
|
|
if explicit_triple != 0:
|
|
if _c_triple is None or _c_datalayout is None:
|
|
VLogger.error("viperbuild: build() triple and datalayout must be given as a pair", "viperbuild")
|
|
return 1
|
|
else:
|
|
# Neither given, or selfmachine=True: detect the host at runtime
|
|
if _DetectHost() != 0: return 1
|
|
|
|
# ---- Write back to Config ----
|
|
Config.ProjectName = _c_name
|
|
Config.ProjectVersion = _c_version
|
|
Config.SourceDir = _c_source
|
|
Config.BuildDir = _c_buildroot
|
|
Config.IncludesDirs = _c_includes
|
|
Config.IncludesDir = _FirstSpaceTok(_c_includes)
|
|
if Config.IncludesDir is None: Config.IncludesDir = _c_includes
|
|
Config.TargetTriple = _c_triple
|
|
Config.TargetDataLayout = _c_datalayout
|
|
Config.LinkerStaticLibs = _c_static_libs
|
|
Config.LinkerDynamicLibs = _c_dynamic_libs
|
|
Config.LinkerOutput = _c_output
|
|
Config.Sha1SliceLevel = 1
|
|
Config.StrictMode = 1
|
|
_SplitToolCmd(_c_compiler, 0)
|
|
_SplitToolCmd(_c_linker, 1)
|
|
|
|
Config.resolve_paths(_c_source)
|
|
return 0
|
|
|
|
|
|
def _SplitToolCmd(joined: str, which: int) -> None:
|
|
"""Split a space-joined tool command into Config Cmd/Flags (0=compiler, 1=linker)."""
|
|
if joined is None: return
|
|
n: sizet = string.strlen(joined)
|
|
sp: sizet = n
|
|
for i in range(int(n)):
|
|
if joined[i] == ' ':
|
|
sp = i
|
|
break
|
|
cmd: str = stdlib.malloc(sp + 1)
|
|
if cmd is not None:
|
|
string.strncpy(cmd, joined, sp)
|
|
cmd[sp] = '\0'
|
|
flags: str = None
|
|
if sp < n: flags = _DupStd(joined + sp + 1)
|
|
if which == 0:
|
|
Config.CompilerCmd = cmd
|
|
Config.CompilerFlags = flags
|
|
else:
|
|
Config.LinkerCmd = cmd
|
|
Config.LinkerFlags = flags
|
|
|
|
|
|
# Entry point
|
|
def _ResetAll() -> None:
|
|
"""Reset module state before each DiscoverAndApply call."""
|
|
global _alias, _ctor_name, _g_out_str, _applied, _entry_dir, _search_dirs
|
|
global _c_name, _c_version, _c_source, _c_buildroot, _c_includes
|
|
global _c_compiler, _c_linker, _c_static_libs, _c_dynamic_libs, _c_output
|
|
global _c_triple, _c_datalayout, _c_selfmachine, _c_has_project, _c_has_build
|
|
global _saw_viperbuild
|
|
_alias = None
|
|
_ctor_name = None
|
|
_g_out_str = None
|
|
_applied = 0
|
|
_entry_dir = None
|
|
_search_dirs = None
|
|
_saw_viperbuild = 0
|
|
_c_name = None
|
|
_c_version = None
|
|
_c_source = None
|
|
_c_buildroot = None
|
|
_c_includes = None
|
|
_c_compiler = None
|
|
_c_linker = None
|
|
_c_static_libs = None
|
|
_c_dynamic_libs = None
|
|
_c_output = None
|
|
_c_triple = None
|
|
_c_datalayout = None
|
|
_c_selfmachine = 0
|
|
_c_has_project = 0
|
|
_c_has_build = 0
|
|
|
|
|
|
def DiscoverAndApply(mb: memhub.MemBuddy*, entry_path: str) -> int:
|
|
"""Pass 0 for --src: discover and apply viperbuild config; 0=success, else failed."""
|
|
global _scratch, _work, _visited, _pending, _proj_vars, _entry_dir, _applied
|
|
global _search_dirs
|
|
|
|
if mb is None or entry_path is None:
|
|
VLogger.error("viperbuild: argument is null", "viperbuild")
|
|
return 1
|
|
|
|
_ResetAll()
|
|
|
|
# Entry dir (default source + closure search root)
|
|
_entry_dir = _DirOf(entry_path)
|
|
if _entry_dir is None:
|
|
VLogger.error(f"viperbuild: cannot resolve the entry file directory {entry_path}", "viperbuild")
|
|
return 1
|
|
# Bare filename (no separator) -> _DirOf returns ""; must normalize to ".", otherwise
|
|
# ResolveModuleToPath("") tries no candidate and same-dir deps (often the viperbuild config) never resolve.
|
|
if string.strlen(_entry_dir) == 0: _entry_dir = _DupStd(".")
|
|
# Closure search roots: entry dir first, then "." (the config module may live elsewhere)
|
|
_search_dirs = _ConcatStd(_DupStd(_entry_dir), _DupStd(" ."))
|
|
|
|
# Scratch pool (BFS working set; not affected by main-pool reset)
|
|
arena: bytes = stdlib.malloc(VB_SCRATCH)
|
|
if arena is None:
|
|
VLogger.error("viperbuild: scratch memory allocation failed", "viperbuild")
|
|
return 1
|
|
# Heap-allocate the manager: `_scratch` is a module-level global, so a frame-local
|
|
# `MemBuddy` temp would hand out a dangling address once this frame returns (LifetimeError).
|
|
# `hlife` is explicit so NLL frame-demotion cannot apply.
|
|
_scratch = new heap hlife memhub.MemBuddy(arena, VB_SCRATCH, 0, 0)
|
|
if _scratch is None:
|
|
VLogger.error("viperbuild: scratch pool initialization failed", "viperbuild")
|
|
return 1
|
|
|
|
_work = list[str](_scratch, 8)
|
|
_visited = list[str](_scratch, 8)
|
|
_pending = list[str](_scratch, 8)
|
|
_proj_vars = list[str](_scratch, 8)
|
|
if _work is None or _visited is None or _pending is None or _proj_vars is None:
|
|
VLogger.error("viperbuild: working set allocation failed", "viperbuild")
|
|
return 1
|
|
|
|
# Keyword/operator tables must be built before the main-pool reset window (else freed -> dangling crash)
|
|
ast._init_tables(mb)
|
|
|
|
if _RunBfs(mb, entry_path) != 0: return 1
|
|
|
|
if _proj_vars.__len__() == 0:
|
|
if _saw_viperbuild == 0: VLogger.error(f"viperbuild: `import viperbuild` not found in the entry file dependency closure (entry: {entry_path}; scanned {_visited.__len__()} files)", "viperbuild")
|
|
else: VLogger.error(f"viperbuild: found `import viperbuild` but the dependency closure has no `proj = vb.Project(...)` binding (entry: {entry_path})", "viperbuild")
|
|
return 1
|
|
|
|
if _FinalizeCfg(entry_path) != 0: return 1
|
|
|
|
_applied = 1
|
|
return 0
|
|
|
|
|
|
def IsApplied() -> int:
|
|
"""Whether viperbuild config was applied in this process; 1=yes."""
|
|
return _applied
|