Files
TransPyC/lib/Projectrans/Phase2Translator.py
2026-07-26 20:32:26 +08:00

3063 lines
166 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import sys
import os
import re
import shutil
import subprocess
import ast
import traceback
import json
import pickle
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED
from lib.core.VLogger import get_logger as _vlog
from lib.constants.config import mode as _config_mode
from lib.core.Handles.HandlesBase import CTypeInfo
from lib.includes import t
from lib.core.SymbolUtils import AnnotationContainsName
from lib.Projectrans.Utils import compute_sha1, _check_annotation_for_export, find_reachable_files_from_entries, topological_sort_files, parse_python_file
from lib.Projectrans.Phase1Generator import _TryGlobalCache, _SaveToGlobalCache, _GLOBAL_CACHE_DIR
import TransPyC
from StubGen import PythonToStubConverter
def _ExtractCDefineConstantsFromPyi(pyi_path: str, all_dc: dict[str, object]) -> None:
"""从 .pyi 文件中提取 CDefine 常量到 all_dc。文件不存在时静默跳过。"""
if not os.path.exists(pyi_path):
return
try:
# 复用 parse_python_file 的 AST 缓存(.pyi 文件被多处解析)
_, tree = parse_python_file(pyi_path)
if tree is None:
return
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
if AnnotationContainsName(node.annotation, 'CDefine') and node.value:
val = None
if isinstance(node.value, ast.Constant):
val = node.value.value
elif isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id == 'float' and node.value.args and isinstance(node.value.args[0], ast.Constant):
val = node.value.args[0].value
if val is not None:
all_dc[f"{node.target.id}"] = val
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
def _parallel_translate_worker(src_path: str, out_path: str, src_root: str, temp_dir: str, output_dir: str,
include_dirs: list[str], triple: str | None, datalayout: str | None,
sha1_map: dict[str, str], sig_files: dict[str, str],
stub_files: dict[str, str], include_py_map: dict[str, str], slice_level: int,
shared_sym_pickle_path: str, struct_sha1_map: dict[str, str],
prebuilt_ModuleSha1Map: dict[str, str] | None = None) -> tuple[str, str, str]:
try:
trans = Phase2Translator(src_root, temp_dir, output_dir,
compile_cmd='llc', include_dirs=include_dirs,
target_triple=triple, target_datalayout=datalayout)
trans.sha1_map = sha1_map
trans.sig_files = sig_files
trans.stub_files = stub_files
trans.include_py_map = include_py_map
trans.slice_level = slice_level
trans.struct_sha1_map = struct_sha1_map
if shared_sym_pickle_path and os.path.exists(shared_sym_pickle_path):
# 使用 pickle.load(f) 流式加载,避免 pickle.loads(f.read()) 一次性
# 将整个 pickle 文件读入内存(磁盘副本 + 反序列化对象同时占用内存)导致 MemoryError。
# 同时提高递归限制,防止深层嵌套对象反序列化失败。
sys.setrecursionlimit(max(sys.getrecursionlimit(), 100000))
with open(shared_sym_pickle_path, 'rb') as f:
shared_data = pickle.load(f)
trans._shared_symbol_table = shared_data['symbol_table']
trans._shared_source_module_sig_files = shared_data['source_module_sig_files']
trans._shared_all_dc = shared_data['all_dc']
trans._shared_export_extern_funcs = shared_data['export_extern_funcs']
trans.inline_func_symbols = shared_data['inline_func_symbols']
trans.function_default_args = shared_data['function_default_args']
trans.function_param_names = shared_data.get('function_param_names', {})
trans._shared_generic_class_templates = shared_data.get('generic_class_templates', {})
trans._shared_doc_meta = shared_data.get('shared_doc_meta', {})
trans._shared_class_vtable = shared_data.get('class_vtable', set())
trans._shared_cross_module_vtable = shared_data.get('cross_module_vtable', set())
trans._shared_class_methods = shared_data.get('class_methods', {})
trans._shared_cross_module_novtable = shared_data.get('cross_module_novtable', set())
else:
trans._precollect_inline_symbols()
trans._build_shared_symbol_data()
trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=prebuilt_ModuleSha1Map)
return ('ok', src_path, '')
except Exception as e:
return ('error', src_path, f'{e}\n{traceback.format_exc()}')
def _count_braces_outside_strings(text: str) -> int:
"""统计花括号差值,跳过 c"..." 字符串字面量内部的括号"""
count = 0
i = 0
while i < len(text):
if text[i:i+2] == 'c"' and (i == 0 or text[i-1] != '\\'):
# 跳过 c"..." 字面量
i += 2
while i < len(text):
if text[i] == '"' and (i == 0 or text[i-1] != '\\'):
i += 1
break
i += 1
continue
if text[i] == '{':
count += 1
elif text[i] == '}':
count -= 1
i += 1
return count
class Phase2Translator:
"""阶段二:使用声明接口翻译源文件"""
INCLUDE_LIB_EXTENSIONS: tuple[str, ...] = ('.dll', '.ll', '.so', '.o', '.a', '.lib')
INCLUDE_SRC_EXTENSIONS: tuple[str, ...] = ('.c', '.cpp', '.cc', '.cxx', '.m', '.mm')
INCLUDE_DIRS: list[str] = [
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'includes'),
]
def __init__(self, src_root: str, temp_dir: str, output_dir: str, compile_cmd: str = 'llc', compile_flags: list[str] | None = None, linker_cmd: str | None = None, linker_flags: list[str] | None = None, linker_output: str | None = None, include_dirs: list[str] | None = None, entry_files: list[str] | None = None, target_triple: str | None = None, target_datalayout: str | None = None, force_recompile_includes: bool = False) -> None:
self.src_root = os.path.abspath(src_root)
self.temp_dir = os.path.abspath(temp_dir)
self.output_dir = os.path.abspath(output_dir)
self.compile_cmd = compile_cmd
self.compile_flags = compile_flags or ['-filetype=obj']
self.triple = target_triple
self.datalayout = target_datalayout
if not self.triple:
for i, flag in enumerate(self.compile_flags):
if flag.startswith('-mtriple='):
self.triple = flag[len('-mtriple='):]
elif flag == '-mtriple' and i + 1 < len(self.compile_flags):
self.triple = self.compile_flags[i + 1]
self.linker_cmd = linker_cmd
self.linker_flags = linker_flags or []
self.linker_output = linker_output
self.slice_level = 3
self.sha1_map: dict[str, str] = {}
self.sig_files: dict[str, str] = {}
self.stub_files: dict[str, str] = {}
self.include_py_map: dict[str, str] = {}
self.used_includes: set[str] = set()
self._last_ErrorStack: list | None = None
self.function_default_args: dict[str, list] = {}
self.function_param_names: dict[str, list] = {}
self.inline_func_symbols: dict[str, CTypeInfo] = {}
self.extra_link_files: list[str] = []
self.extra_compile_files: list[str] = []
self.extra_py_files: list[str] = []
self._include_sha1s: set[str] = set()
# 持有 ProcessPoolExecutor 引用,避免局部变量 GC 时 finalizer 阻塞 ~99s
# join_executor_internals 在 GC 时同步等待 worker 退出队列)
self._executor_pool: ProcessPoolExecutor | None = None
self.entry_files: list[str] | None = entry_files
self.force_recompile_includes: bool = force_recompile_includes
self._shared_symbol_table: object | None = None
self._shared_source_module_sig_files: dict[str, str] = {}
self._shared_all_dc: dict[str, dict] = {}
self._shared_export_extern_funcs: dict[str, str] = {}
self.include_dirs: list[str] = []
self.struct_sha1_map: dict[str, str] = {}
self._shared_generic_class_templates: dict[str, dict] = {}
self._shared_doc_meta: dict[str, dict[str, str]] = {}
# 共享 vtable 状态跨模块(修复 pool.alloc(48) 虚表分派失败导致段错误)
self._shared_class_vtable: set[str] = set()
self._shared_cross_module_vtable: set[str] = set()
self._shared_class_methods: dict[str, list[str]] = {}
self._shared_cross_module_novtable: set[str] = set()
default_dirs = [d for d in self.INCLUDE_DIRS if os.path.isdir(d)]
if include_dirs:
seen = set()
self.include_dirs = []
for d in list(include_dirs) + default_dirs:
d = os.path.abspath(d)
if d not in seen and os.path.isdir(d):
self.include_dirs.append(d)
seen.add(d)
else:
self.include_dirs = default_dirs
self._LoadSha1Map()
def _LoadSha1Map(self) -> None:
"""从 temp_dir 加载 SHA1 映射、签名文件和 stub 声明文件列表。
temp_dir 中缺失的文件会从全局缓存补充。
"""
if not os.path.exists(self.temp_dir):
_vlog().error(f"声明目录不存在: {self.temp_dir}")
return
for fname in os.listdir(self.temp_dir):
fpath = os.path.join(self.temp_dir, fname)
if fname.startswith('_'):
continue
if fname.endswith('.pyi'):
sha1 = fname[:-4]
self.sig_files[sha1] = fpath
elif fname.endswith('.stub.ll'):
sha1 = fname[:-8]
self.stub_files[sha1] = fpath
# 从全局缓存补充 temp_dir 中缺失的 .pyi / .stub.ll
if os.path.isdir(_GLOBAL_CACHE_DIR):
for fname in os.listdir(_GLOBAL_CACHE_DIR):
if fname.startswith('_'):
continue
if fname.endswith('.pyi'):
sha1 = fname[:-4]
if sha1 not in self.sig_files:
self.sig_files[sha1] = os.path.join(_GLOBAL_CACHE_DIR, fname)
elif fname.endswith('.stub.ll'):
sha1 = fname[:-8]
if sha1 not in self.stub_files:
self.stub_files[sha1] = os.path.join(_GLOBAL_CACHE_DIR, fname)
# 从内存存储器读取 SHA1 映射_sha1_map.txt 仅人类可读,不再作为机器数据源)
# 延迟导入避免循环依赖Config 导入 Phase2Translator
from lib.Projectrans.Config import get_sha1_map_store
mem_store: dict[str, str] = get_sha1_map_store()
if mem_store:
for sha1, rel in mem_store.items():
self.sha1_map[sha1] = rel
if rel.startswith('includes/'):
module_name = os.path.splitext(os.path.basename(rel))[0]
self.include_py_map[module_name] = sha1
else:
# 内存存储器为空(独立运行 Phase2 的回退):从 stub 文件名推断
for sha1, stub_path in self.stub_files.items():
self.sha1_map[sha1] = sha1
def _build_struct_sha1_map(self) -> dict[str, str]:
struct_sha1_map = {}
valid_sha1_keys = set(self.sha1_map.keys())
# 收集 temp_dir 和全局缓存中的 .pyi 文件
pyi_dirs: list[str] = [self.temp_dir]
if os.path.isdir(_GLOBAL_CACHE_DIR):
pyi_dirs.append(_GLOBAL_CACHE_DIR)
for scan_dir in pyi_dirs:
if not os.path.isdir(scan_dir):
continue
for fname in os.listdir(scan_dir):
if not fname.endswith('.pyi'):
continue
sha1_key = fname.replace('.pyi', '')
if sha1_key not in valid_sha1_keys:
continue
if sha1_key in struct_sha1_map:
continue
pyi_path = os.path.join(scan_dir, fname)
# 复用 parse_python_file 的 AST 缓存(.pyi 文件可能被多处解析)
_, tree = parse_python_file(pyi_path)
if tree is None:
continue
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
struct_sha1_map[node.name] = sha1_key
return struct_sha1_map
def run(self) -> None:
"""扫描源目录,翻译并生成 .ll 含代码文件(导入遍历)"""
import sys as _sys
if self.entry_files:
py_files = list(self.entry_files)
else:
main_py = os.path.join(self.src_root, 'main.py')
if os.path.exists(main_py):
py_files = [main_py]
else:
py_files = []
for root, dirs, files in os.walk(self.src_root):
dirs[:] = [d for d in dirs if d not in ('__pycache__',)]
for file in files:
if file.endswith('.py'):
py_files.append(os.path.join(root, file))
if not py_files:
_vlog().info("未找到入口文件或源文件")
return
reachable = find_reachable_files_from_entries(self.src_root, py_files)
py_files = list(reachable)
py_files = topological_sort_files(py_files, self.src_root)
_vlog().info(f"找到 {len(py_files)} 个可达源文件(已按依赖排序)")
_vlog().info("处理顺序:")
for i, f in enumerate(py_files[:10], 1):
rel = os.path.relpath(f, self.src_root)
_vlog().info(f" {i}. {rel}")
os.makedirs(self.output_dir, exist_ok=True)
valid_sha1s = set(self.sha1_map.keys())
old_stub_count = len(self.stub_files)
old_sig_count = len(self.sig_files)
self.stub_files = {k: v for k, v in self.stub_files.items() if k in valid_sha1s}
self.sig_files = {k: v for k, v in self.sig_files.items() if k in valid_sha1s}
if old_stub_count != len(self.stub_files) or old_sig_count != len(self.sig_files):
_vlog().info(f"过滤旧文件: stub {old_stub_count}->{len(self.stub_files)}, sig {old_sig_count}->{len(self.sig_files)}")
active_sha1s = set()
self._precollect_inline_symbols()
self._build_shared_symbol_data()
self._precollect_vtable_state()
self.struct_sha1_map = self._build_struct_sha1_map()
need_translate = []
for i, src_path in enumerate(py_files, 1):
rel = os.path.relpath(src_path, self.src_root)
# 复用 parse_python_file 的 AST 缓存get_file_dependencies 已 parse 过)
content, tree = parse_python_file(src_path)
sha1 = compute_sha1(content)
active_sha1s.add(sha1)
out_path = os.path.join(self.output_dir, f"{sha1}.ll")
current_ModuleSha1Map = self._build_current_ModuleSha1Map(sha1)
if tree is not None:
try:
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
self.used_includes.add(alias.name.split('.')[0])
elif isinstance(node, ast.ImportFrom):
if node.module:
self.used_includes.add(node.module.split('.')[0])
# 检测 list/dict 容器使用,自动添加 _list/_dict/json 到 used_includes
# 以便 _scan_include_libraries 能发现并编译这些库文件
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name):
if node.value.id == 'dict':
self.used_includes.add('_dict')
self.used_includes.add('json')
elif node.value.id == 'list':
self.used_includes.add('_list')
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
if node.func.id == 'dict':
self.used_includes.add('_dict')
self.used_includes.add('json')
elif node.func.id == 'list':
self.used_includes.add('_list')
# 检测类型注解 d: dict / l: list (非泛型形式)
if isinstance(node, ast.AnnAssign) and isinstance(node.annotation, ast.Name):
if node.annotation.id == 'dict':
self.used_includes.add('_dict')
self.used_includes.add('json')
elif node.annotation.id == 'list':
self.used_includes.add('_list')
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"收集源文件导入的 include 模块失败: {_e}", "Exception")
if os.path.isfile(out_path):
deps_changed = self._check_deps_changed(sha1, current_ModuleSha1Map)
if not deps_changed:
_vlog().info(f"[{i}/{len(py_files)}] 跳过(缓存): {rel} -> {sha1}.ll")
continue
else:
if self._recombine_ll(sha1, current_ModuleSha1Map):
_vlog().info(f"[{i}/{len(py_files)}] 重组(依赖变化): {rel} -> {sha1}.ll")
continue
_vlog().info(f"[{i}/{len(py_files)}] 重译(依赖变化): {rel} -> {sha1}.ll")
else:
stub_cache = os.path.join(self.output_dir, f"{sha1}.stub.ll")
text_cache = os.path.join(self.output_dir, f"{sha1}.text.ll")
if os.path.isfile(stub_cache) and os.path.isfile(text_cache):
if self._recombine_ll(sha1, current_ModuleSha1Map):
_vlog().info(f"[{i}/{len(py_files)}] 重组(缓存恢复): {rel} -> {sha1}.ll")
continue
need_translate.append((i, src_path, out_path, sha1, current_ModuleSha1Map))
if need_translate:
# 提前从 include 文件源码中提取函数参数名,供主项目编译时 keyword 参数匹配使用
self._extract_include_param_names_from_dirs()
# 提前从 App 源文件中提取函数默认参数,供跨模块调用参数检查使用
# (避免并行编译时,被调用函数的文件尚未编译完,导致默认参数信息缺失)
self._extract_app_default_args(need_translate)
_vlog().info(f"\n需要翻译 {len(need_translate)} 个文件")
t_start = time.time()
n_workers = min(8, len(need_translate))
if n_workers > 1 and len(need_translate) > 1:
_vlog().info(f" 并行翻译 (workers={n_workers})")
shared_pickle_path = os.path.join(self.temp_dir, '_shared_sym.json')
try:
shared_data = {
'symbol_table': self._shared_symbol_table,
'source_module_sig_files': self._shared_source_module_sig_files,
'all_dc': self._shared_all_dc,
'export_extern_funcs': self._shared_export_extern_funcs,
'inline_func_symbols': self.inline_func_symbols,
'function_default_args': self.function_default_args,
'function_param_names': self.function_param_names,
'generic_class_templates': self._shared_generic_class_templates,
'shared_doc_meta': self._shared_doc_meta,
'class_vtable': self._shared_class_vtable,
'cross_module_vtable': self._shared_cross_module_vtable,
'class_methods': self._shared_class_methods,
'cross_module_novtable': self._shared_cross_module_novtable,
}
with open(shared_pickle_path, 'wb') as f:
f.write(pickle.dumps(shared_data))
_vlog().info(f" 共享符号表已序列化 ({os.path.getsize(shared_pickle_path)//1024}KB)")
except Exception as e:
_vlog().warning(f"符号表序列化失败({e})worker将自行构建")
shared_pickle_path = ''
errors = []
# 复用 self._executor_pool避免局部变量 GC 时 finalizer 阻塞 ~99s
# join_executor_internals 在 GC 时同步等待 worker 退出队列)
if self._executor_pool is None:
self._executor_pool = ProcessPoolExecutor(max_workers=n_workers)
executor = self._executor_pool
try:
futures = {}
for i, src_path, out_path, sha1, msm in need_translate:
rel = os.path.relpath(src_path, self.src_root)
future = executor.submit(
_parallel_translate_worker,
src_path, out_path,
self.src_root, self.temp_dir, self.output_dir,
self.include_dirs, self.triple, self.datalayout,
self.sha1_map, self.sig_files, self.stub_files,
self.include_py_map, self.slice_level,
shared_pickle_path, self.struct_sha1_map,
prebuilt_ModuleSha1Map=msm
)
futures[future] = (i, rel)
done_count = 0
for future in as_completed(futures):
i, rel = futures[future]
done_count += 1
try:
status, _, err = future.result()
if status == 'ok':
_vlog().info(f" [{done_count}/{len(need_translate)}] 完成: {rel}")
else:
_vlog().error(f" {rel}: {err}")
errors.append((rel, err))
except Exception as e:
_vlog().error(f" {rel}: {e}")
errors.append((rel, str(e)))
except Exception:
# 异常时立即清理,正常流程不 shutdown留给 includes 阶段复用)
if self._executor_pool is not None:
self._executor_pool.shutdown(wait=False, cancel_futures=True)
self._executor_pool = None
raise
if errors:
_vlog().error(f"\n{len(errors)} 个文件翻译失败,立即终止编译")
for rel, err in errors:
_vlog().error(f" {rel}: {err}")
raise RuntimeError(f"{len(errors)} 个文件翻译失败,已终止编译")
else:
for idx, (i, src_path, out_path, sha1, msm) in enumerate(need_translate):
rel = os.path.relpath(src_path, self.src_root)
t0 = time.time()
try:
self._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm)
t1 = time.time()
_vlog().info(f" [{idx+1}/{len(need_translate)}] {rel} ({t1-t0:.1f}s)")
except Exception as e:
_vlog().error(f" {rel}: {e}")
traceback.print_exc()
raise RuntimeError(f"翻译 {rel} 失败,已终止编译: {e}") from e
t_end = time.time()
_vlog().info(f" 翻译总耗时: {t_end-t_start:.1f}s")
_vlog().info(f"\n[阶段二完成] .ll 文件生成到: {self.output_dir}")
self._scan_include_libraries()
self._compile_include_py_files(active_sha1s)
self._compile_ll_files(active_sha1s)
self._compile_include_sources()
if self.linker_cmd:
self._link_obj_files(active_sha1s)
# 清理 ProcessPoolExecutor所有翻译已完成shutdown(wait=False) 让 worker
# 后台退出。避免 GC finalizer 在程序末尾阻塞 ~99sjoin_executor_internals
if self._executor_pool is not None:
self._executor_pool.shutdown(wait=False, cancel_futures=True)
self._executor_pool = None
def _build_current_ModuleSha1Map(self, self_sha1: str | None) -> dict[str, str]:
"""构建当前模块的依赖 SHA1 映射(与 _translate_file 中逻辑一致)"""
ModuleSha1Map = {}
for sha1_key, sig_path in self.sig_files.items():
if sha1_key == self_sha1:
continue
rel_path = self.sha1_map.get(sha1_key, sha1_key)
if rel_path.startswith('includes/'):
inc_mod_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
inc_mod_name = inc_mod_name[len('includes/'):]
inc_short_name = inc_mod_name.split('.')[-1] if '.' in inc_mod_name else inc_mod_name
actual_sha1 = sha1_key
for inc_dir in self.include_dirs:
if os.path.isdir(inc_dir):
py_path = os.path.join(inc_dir, rel_path[len('includes/'):].replace(os.sep, '/'))
py_path = os.path.splitext(py_path)[0] + '.py'
if os.path.isfile(py_path):
with open(py_path, 'r', encoding='utf-8') as f:
py_sha1 = compute_sha1(f.read())
actual_sha1 = py_sha1
break
ModuleSha1Map[inc_mod_name] = actual_sha1
ModuleSha1Map[inc_short_name] = actual_sha1
# Also add parent package name (e.g., 'hashlib' for 'hashlib.__init__')
if inc_short_name == '__init__' and '.' in inc_mod_name:
parent_pkg = inc_mod_name.rsplit('.', 1)[0]
ModuleSha1Map[parent_pkg] = actual_sha1
continue
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
ModuleSha1Map[module_name] = sha1_key
short_name = module_name.split('.')[-1] if '.' in module_name else module_name
ModuleSha1Map[short_name] = sha1_key
# 包入口文件:注册父包名(如 'os.__init__' -> 'os'),使 import os 能解析到包级符号
if short_name == '__init__' and '.' in module_name:
parent_pkg = module_name.rsplit('.', 1)[0]
ModuleSha1Map[parent_pkg] = sha1_key
return ModuleSha1Map
@staticmethod
def _extract_llvm_type_name(line: str) -> str | None:
"""从 LLVM IR 类型定义行提取类型名(字符串操作,不使用正则)
支持 list[str]、dict[str, int] 等含特殊字符的类型名。
例如:
%"18e6990aa81cc5b6.list[str]" = type { ... } → "18e6990aa81cc5b6.list[str]"
%struct.foo = type { ... } → "struct.foo"
"""
stripped = line.strip()
if not stripped.startswith('%'):
return None
# 处理带引号的情况: %"name" = type ...
if stripped.startswith('%"'):
end_quote = stripped.find('"', 2)
if end_quote > 0:
return stripped[2:end_quote]
return None
# 处理不带引号的情况: %name = type ...
eq_pos = stripped.find('=')
if eq_pos > 1:
return stripped[1:eq_pos].strip()
return None
@staticmethod
def _find_all_type_refs(text: str) -> set[str]:
"""查找文本中所有 %"..."/%name 类型引用(字符串操作,不使用正则)
支持 list[str]、dict[str, int] 等含特殊字符的类型名。
"""
refs: set[str] = set()
i: int = 0
n: int = len(text)
while i < n:
if text[i] == '%':
if i + 1 < n and text[i + 1] == '"':
# 带引号: %"name"
end: int = text.find('"', i + 2)
if end > 0:
name: str = text[i + 2:end]
if name:
refs.add(name)
i = end + 1
continue
else:
# 不带引号: %name (仅匹配字母、数字、下划线、点)
j: int = i + 1
while j < n and (text[j].isalnum() or text[j] in '._'):
j += 1
if j > i + 1:
refs.add(text[i + 1:j])
i = j
continue
i += 1
return refs
@staticmethod
def _split_ll(ll_content: str) -> tuple[str, str]:
"""将 .ll 内容拆分为 stub声明和 text代码两部分
stub: target, 注释, %type = type, declare, @xxx = external global
text: define ... { ... }, @xxx = global/constant (带初始化器)
"""
stub_lines = []
text_lines = []
in_define = False
brace_depth = 0
module_sha1 = None
defined_names = set()
for line in ll_content.splitlines(True):
stripped = line.strip()
if stripped.startswith('define '):
dm = re.match(r'define\s+[^@]*@"?([a-f0-9]+)\.', stripped)
if dm and module_sha1 is None:
module_sha1 = dm.group(1)
dm2 = re.match(r'define\s+[^@]*@"?([^"\s(]+)"?', stripped)
if dm2:
defined_names.add(dm2.group(1))
in_global_def = False
global_var_name = None
global_type_parts = []
global_brace_depth = 0
for line in ll_content.splitlines(True):
stripped = line.strip()
if in_global_def:
text_lines.append(line)
global_type_parts.append(stripped)
global_brace_depth += _count_braces_outside_strings(stripped)
if global_brace_depth <= 0:
full_type = ' '.join(global_type_parts).strip()
depth = 0
type_end = len(full_type)
for ci, cc in enumerate(full_type):
if cc in ('{', '['):
depth += 1
elif cc in ('}', ']'):
depth -= 1
elif depth == 0 and cc == ' ':
type_end = ci
break
var_type = full_type[:type_end].strip()
if var_type:
stub_lines.append(f'{global_var_name} = external global {var_type}\n')
in_global_def = False
global_var_name = None
global_type_parts = []
global_brace_depth = 0
continue
if in_define:
text_lines.append(line)
brace_depth += _count_braces_outside_strings(stripped)
if brace_depth <= 0:
in_define = False
continue
if stripped.startswith('define '):
text_lines.append(line)
in_define = True
brace_depth = _count_braces_outside_strings(stripped)
decl_line = re.sub(r'^define\s+', 'declare ', stripped)
decl_line = re.sub(r'\b(linkonce_odr|weak_odr|linkonce|weak|common|appending|internal|private)\s+', '', decl_line)
decl_line = re.sub(r'\balwaysinline\s+', '', decl_line)
paren_pos = decl_line.rfind(')')
if paren_pos >= 0:
decl_line = decl_line[:paren_pos + 1]
stub_lines.append(decl_line + '\n')
continue
if stripped == '{' and text_lines and not in_define:
prev_stripped = text_lines[-1].strip() if text_lines else ''
if prev_stripped.startswith('define ') or prev_stripped.endswith('noredzone') or prev_stripped.endswith(')'):
text_lines.append(line)
brace_depth = 1
in_define = True
if prev_stripped.startswith('define '):
decl_line = re.sub(r'^define\s+', 'declare ', prev_stripped)
decl_line = re.sub(r'\b(linkonce_odr|weak_odr|linkonce|weak|common|appending|internal|private)\s+', '', decl_line)
decl_line = re.sub(r'\balwaysinline\s+', '', decl_line)
paren_pos = decl_line.rfind(')')
if paren_pos >= 0:
decl_line = decl_line[:paren_pos + 1]
stub_lines.append(decl_line + '\n')
continue
if stripped.startswith('@') and ' global ' in stripped and 'external global' not in stripped:
text_lines.append(line)
if ' internal ' not in stripped and ' private ' not in stripped:
name_match = re.match(r'(@\S+)', stripped)
if name_match:
var_name = name_match.group(1)
global_pos = stripped.find(' global ')
after_global = stripped[global_pos + 8:]
depth = 0
type_end = len(after_global)
for ci, cc in enumerate(after_global):
if cc in ('{', '['):
depth += 1
elif cc in ('}', ']'):
depth -= 1
elif depth == 0 and cc == ' ':
type_end = ci
break
var_type = after_global[:type_end].strip()
brace_depth_gv = 0
for cc in var_type:
if cc == '{':
brace_depth_gv += 1
elif cc == '}':
brace_depth_gv -= 1
if brace_depth_gv > 0:
in_global_def = True
global_var_name = var_name
global_type_parts = [var_type]
global_brace_depth = brace_depth_gv
elif var_type:
stub_lines.append(f'{var_name} = external global {var_type}\n')
continue
if stripped.startswith('@') and ' constant ' in stripped:
text_lines.append(line)
continue
if stripped.startswith('declare'):
dm = re.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', stripped)
if dm:
fname = dm.group(1)
if fname in defined_names:
stub_lines.append(line)
continue
dot_pos = fname.find('.')
if dot_pos > 0:
fname_sha1 = fname[:dot_pos]
if module_sha1 and fname_sha1 != module_sha1 and len(fname_sha1) >= 16:
continue
if re.match(r'^[a-f0-9]{16}\.', fname):
continue
stub_lines.append(line)
continue
stub_lines.append(line)
return ''.join(stub_lines), ''.join(text_lines)
def _save_deps(self, sha1: str, ModuleSha1Map: dict[str, str]) -> None:
"""保存依赖指纹到 .deps.json"""
deps_path = os.path.join(self.output_dir, f"{sha1}.deps.json")
deps = {}
for mod_name, dep_sha1 in ModuleSha1Map.items():
deps[mod_name] = dep_sha1
with open(deps_path, 'w', encoding='utf-8') as f:
json.dump(deps, f)
def _LoadDeps(self, sha1: str) -> dict[str, str] | None:
"""加载依赖指纹"""
deps_path = os.path.join(self.output_dir, f"{sha1}.deps.json")
if os.path.isfile(deps_path):
try:
with open(deps_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"加载依赖指纹 JSON 失败: {_e}", "Exception")
return None
def _check_deps_changed(self, sha1: str, current_ModuleSha1Map: dict[str, str]) -> bool:
"""检查依赖指纹是否变化"""
saved_deps = self._LoadDeps(sha1)
if saved_deps is None:
return True
for mod_name, dep_sha1 in current_ModuleSha1Map.items():
if saved_deps.get(mod_name) != dep_sha1:
return True
for mod_name, dep_sha1 in saved_deps.items():
if current_ModuleSha1Map.get(mod_name) != dep_sha1:
return True
return False
def _recombine_ll(self, sha1: str, current_ModuleSha1Map: dict[str, str]) -> bool:
"""依赖变化时重新组合 .ll更新 .stub.ll 中的 SHA1 前缀,拼接 .stub.ll + .text.ll"""
saved_deps = self._LoadDeps(sha1)
if saved_deps is None:
return False
stub_path = os.path.join(self.output_dir, f"{sha1}.stub.ll")
text_path = os.path.join(self.output_dir, f"{sha1}.text.ll")
ll_path = os.path.join(self.output_dir, f"{sha1}.ll")
if not os.path.isfile(stub_path) or not os.path.isfile(text_path):
return False
with open(stub_path, 'r', encoding='utf-8') as f:
stub_content = f.read()
with open(text_path, 'r', encoding='utf-8') as f:
text_content = f.read()
sha1_replacements = {}
for mod_name, old_sha1 in saved_deps.items():
new_sha1 = current_ModuleSha1Map.get(mod_name)
if new_sha1 and old_sha1 != new_sha1:
sha1_replacements[old_sha1] = new_sha1
if sha1_replacements:
for old_sha1, new_sha1 in sha1_replacements.items():
stub_content = stub_content.replace(old_sha1, new_sha1)
text_content = text_content.replace(old_sha1, new_sha1)
combined = stub_content
if not combined.endswith('\n'):
combined += '\n'
combined += text_content
with open(ll_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(combined)
with open(stub_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(stub_content)
with open(text_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(text_content)
self._save_deps(sha1, current_ModuleSha1Map)
obj_path = os.path.join(self.output_dir, f"{sha1}.obj")
if os.path.isfile(obj_path):
os.remove(obj_path)
return True
def _inject_auto_imports(self, code: str, src_path: str) -> str:
"""检测源码中对 list/dict 容器的使用,自动注入 import _list / import _dict。
当使用 dict 时,同时注入 import json (用于 dict 的 JSON 处理功能)。
检测模式:
- list[T] / dict[V] 作为类型注解 (ast.Subscript, value=Name(id='list'/'dict'))
- list[T](...) / dict[V](...) 作为构造函数调用 (ast.Call, func=Subscript)
- list(...) / dict(...) 作为构造函数调用 (ast.Call, func=Name)
"""
try:
tree = ast.parse(code)
except SyntaxError:
return code
# 检查是否已经导入了 _list / _dict / json
has_list_import: bool = False
has_dict_import: bool = False
has_json_import: bool = False
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name == '_list':
has_list_import = True
elif alias.name == '_dict':
has_dict_import = True
elif alias.name == 'json':
has_json_import = True
elif isinstance(node, ast.ImportFrom):
if node.module == '_list':
has_list_import = True
elif node.module == '_dict':
has_dict_import = True
elif node.module == 'json':
has_json_import = True
if has_list_import and has_dict_import and has_json_import:
break
# 检查是否使用了 list / dict 容器
uses_list: bool = False
uses_dict: bool = False
for node in ast.walk(tree):
if isinstance(node, ast.Subscript):
if isinstance(node.value, ast.Name):
if node.value.id == 'list':
uses_list = True
elif node.value.id == 'dict':
uses_dict = True
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
if node.func.id == 'list':
uses_list = True
elif node.func.id == 'dict':
uses_dict = True
# 检测类型注解 d: dict / l: list (非泛型形式)
if isinstance(node, ast.AnnAssign) and isinstance(node.annotation, ast.Name):
if node.annotation.id == 'dict':
uses_dict = True
elif node.annotation.id == 'list':
uses_list = True
# 注入 import 到源码开头
prefix: str = ''
if uses_list and not has_list_import:
prefix += 'import _list\n'
if uses_dict and not has_dict_import:
prefix += 'import _dict\n'
if uses_dict and not has_json_import:
prefix += 'import json\n'
if prefix:
return prefix + code
return code
def _translate_file(self, src_path: str, out_path: str, prebuilt_ModuleSha1Map: dict[str, str] | None = None) -> None:
"""翻译单个源文件,嵌入相关 .stub.ll 声明"""
with open(src_path, 'r', encoding='utf-8') as f:
code = f.read()
# SHA1 必须在自动导入注入之前计算,以保持与 Phase1 生成的文件名/函数名前缀一致
sha1 = compute_sha1(code)
# 自动导入: 检测 list 容器使用,自动注入 import _list
code = self._inject_auto_imports(code, src_path)
if prebuilt_ModuleSha1Map is not None:
ModuleSha1Map = prebuilt_ModuleSha1Map
else:
ModuleSha1Map = self._build_current_ModuleSha1Map(sha1)
trans = TransPyC.TransPyC(code=code, triple=self.triple, datalayout=self.datalayout)
trans.translator.CurrentFile = src_path
trans.SliceLevel = self.slice_level
trans.translator.SliceLevel = self.slice_level
trans.translator.SliceCount = 0
trans.translator.SliceInfos = []
trans.translator.LlvmGen = None
trans.translator._module_sha1 = sha1
trans.translator._ModuleSha1Map = ModuleSha1Map
trans.translator._struct_sha1_map = self.struct_sha1_map
trans.translator._global_function_default_args = self.function_default_args
trans.translator._global_function_param_names = self.function_param_names
trans.translator._temp_dir = self.temp_dir
trans.translator._shared_doc_meta = self._shared_doc_meta
# 传递共享 vtable 状态
trans.translator._shared_class_vtable = self._shared_class_vtable
trans.translator._shared_cross_module_vtable = self._shared_cross_module_vtable
trans.translator._shared_class_methods = self._shared_class_methods
trans.translator._shared_cross_module_novtable = self._shared_cross_module_novtable
if self._shared_symbol_table is not None:
trans.translator.SymbolTable = self._shared_symbol_table
# 确保共享符号表的 translator 指向当前 translator以便 import 别名解析正确
if hasattr(self._shared_symbol_table, 'translator'):
self._shared_symbol_table.translator = trans.translator
trans.translator._source_module_sig_files = self._shared_source_module_sig_files
trans.translator._all_define_constants = self._shared_all_dc
trans.translator._export_extern_funcs = self._shared_export_extern_funcs
if self._shared_generic_class_templates:
if not hasattr(trans.translator.ClassHandler, '_generic_class_templates'):
trans.translator.ClassHandler._generic_class_templates = {}
trans.translator.ClassHandler._generic_class_templates.update(self._shared_generic_class_templates)
else:
export_extern_funcs = {}
for sha1_key, sig_path in self.sig_files.items():
if sha1_key == sha1:
continue
try:
with open(sig_path, 'r', encoding='utf-8') as f:
sig_content = f.read()
sig_tree = ast.parse(sig_content)
for node in ast.iter_child_nodes(sig_tree):
if isinstance(node, ast.FunctionDef):
if _check_annotation_for_export(node.returns):
export_extern_funcs[node.name] = sha1_key
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"收集导出外部函数声明失败: {_e}", "Exception")
if export_extern_funcs:
trans.translator._export_extern_funcs = export_extern_funcs
for includes_dir in self.include_dirs:
if os.path.isdir(includes_dir):
for pyi_file in os.listdir(includes_dir):
if pyi_file.endswith('.pyi'):
pyi_path = os.path.join(includes_dir, pyi_file)
module_name = os.path.splitext(pyi_file)[0]
trans.translator.SymbolTable.LoadModuleSymbols(pyi_path, module_name, lineno=0)
for py_file in os.listdir(includes_dir):
if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'):
module_name = os.path.splitext(py_file)[0]
sha1_key = self.include_py_map.get(module_name)
if sha1_key and sha1_key in self.sig_files:
sig_path = self.sig_files[sha1_key]
trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0)
# 收集需要重导出的包,等所有模块符号加载完后再处理
_pending_reexports = []
for sha1_key, sig_path in self.sig_files.items():
rel_path = self.sha1_map.get(sha1_key, sha1_key)
if rel_path.startswith('includes/'):
mod_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
mod_name = mod_name[len('includes/'):]
trans.translator.SymbolTable.LoadModuleSymbols(sig_path, mod_name, lineno=0)
if mod_name.endswith('.__init__'):
package_name = mod_name[:-len('.__init__')]
_pending_reexports.append((sig_path, package_name))
# 所有 includes 模块符号已加载,现在处理包重导出
for sig_path, package_name in _pending_reexports:
self._register_package_reexports(trans, sig_path, package_name)
for sha1_key, sig_path in self.sig_files.items():
if sha1_key == sha1:
continue
rel_path = self.sha1_map.get(sha1_key, sha1_key)
if rel_path.startswith('includes/'):
continue
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0)
trans.translator._source_module_sig_files[module_name] = sig_path
short_name = module_name.split('.')[-1] if '.' in module_name else module_name
if short_name != module_name:
trans.translator._source_module_sig_files[short_name] = sig_path
all_dc = {}
for sha1_key, stub_path in self.stub_files.items():
if sha1_key == sha1:
continue
if os.path.exists(stub_path):
try:
with open(stub_path, 'r', encoding='utf-8') as f:
stub_content = f.read()
for line in stub_content.splitlines():
line = line.strip()
if line.startswith('@') and '= global' in line and 'i32' in line and not line.startswith('@llvm'):
var_name = line.split('=')[0].strip().lstrip('@')
val_part = line.split('i32')[-1].strip().rstrip(']').lstrip('[').lstrip('i32')
try:
val = int(val_part)
all_dc[var_name] = val
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
for sha1_key, pyi_path in self.sig_files.items():
if sha1_key == sha1:
continue
_ExtractCDefineConstantsFromPyi(pyi_path, all_dc)
if all_dc:
trans.translator._all_define_constants = all_dc
for sym_name, sym_info in self.inline_func_symbols.items():
if sym_name not in trans.translator.SymbolTable:
trans.translator.SymbolTable[sym_name] = sym_info
else:
existing = trans.translator.SymbolTable[sym_name]
if not getattr(existing, 'InlineBody', None) and getattr(sym_info, 'InlineBody', None):
existing.IsInline = sym_info.IsInline
existing.InlineBody = sym_info.InlineBody
existing.InlineParams = sym_info.InlineParams
try:
result = trans.Convert(
OutputFilename=out_path,
SourceFilename=src_path,
target='llvm'
)
except Exception as e:
error_stack = getattr(trans.translator, '_ErrorStack', [])
if error_stack:
self._last_ErrorStack = error_stack
chain_lines = []
for entry in reversed(error_stack):
if len(entry) >= 3:
exc_msg, line_info = entry[1], entry[2]
chain_lines.append(" %s\n %s" % (line_info, exc_msg))
if chain_lines:
enriched = str(e) + "\n" + "\n".join(chain_lines)
raise type(e)(enriched) from e
node_info = ""
if hasattr(trans.translator, 'LlvmGen') and trans.translator.LlvmGen:
node_info = trans.translator.LlvmGen._get_node_info()
if node_info:
enriched = "%s\n %s" % (str(e), node_info.strip())
raise type(e)(enriched) from e
raise
if result and isinstance(result, str):
for func_name, func_def in trans.translator.FunctionDefCache.items():
if hasattr(func_def, 'args'):
mangled = trans.translator.LlvmGen._mangle_func_name(func_name) if trans.translator.LlvmGen else func_name
# 提取参数名
if hasattr(func_def.args, 'args'):
param_names = [arg.arg for arg in func_def.args.args]
self.function_param_names[mangled] = param_names
self.function_param_names[func_name] = param_names
# 提取默认值
if hasattr(func_def.args, 'defaults') and func_def.args.defaults:
defaults = []
for d in func_def.args.defaults:
if isinstance(d, ast.Constant):
defaults.append(d.value)
else:
defaults.append(None)
self.function_default_args[mangled] = defaults
self.function_default_args[func_name] = defaults
for sym_name, sym_info in trans.translator.SymbolTable.items():
if isinstance(sym_info, CTypeInfo) and getattr(sym_info, 'IsInline', False) and getattr(sym_info, 'InlineBody', None):
self.inline_func_symbols[sym_name] = sym_info
out_dir = os.path.dirname(out_path)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
# 修复 LLVM 22+: external global 不能有初值,改为 linkonce
result = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', result, flags=re.MULTILINE)
# 后处理:添加 llvmlite 不支持的 LLVM 属性(如 willreturn, mustprogress
pending_str_attrs = getattr(trans.translator.LlvmGen, '_pending_str_attrs', None)
if pending_str_attrs:
for func_name, attrs in pending_str_attrs.items():
attrs_str = ' '.join(attrs)
# 匹配 define ... @func_name(...) ... { 并在 { 之前添加属性
pattern = rf'(define\s+[^@]*@{re.escape(func_name)}\s*\([^)]*\))([^{{]*)(\{{)'
result, n = re.subn(pattern, rf'\1\2 {attrs_str}\3', result, count=1)
if n == 0:
_vlog().warning(f"后处理添加属性失败: 未找到函数 {func_name} 的定义")
with open(out_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(result)
stub_content, text_content = self._split_ll(result)
base = os.path.splitext(out_path)[0]
with open(base + '.stub.ll', 'w', encoding='utf-8', newline='\n') as f:
f.write(stub_content)
with open(base + '.text.ll', 'w', encoding='utf-8', newline='\n') as f:
f.write(text_content)
self._save_deps(sha1, ModuleSha1Map)
_vlog().info(f" -> {os.path.basename(out_path)}")
else:
stub_path = self.stub_files.get(sha1)
if stub_path and os.path.exists(stub_path):
out_dir = os.path.dirname(out_path)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
with open(stub_path, 'r', encoding='utf-8') as f:
stub_content = f.read()
with open(out_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(stub_content)
_vlog().info(f" -> {os.path.basename(out_path)} (仅声明)")
def _register_package_reexports(self, trans, init_pyi_path: str, package_name: str) -> None:
"""解析 __init__.pyi 中的 from .xxx import yyy 或 from __xxx import yyy在包级别注册导出符号"""
try:
# 复用 parse_python_file 的 AST 缓存
_, tree = parse_python_file(init_pyi_path)
if tree is None:
return
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
sub_module = node.module.lstrip('.')
for alias in node.names:
symbol_name = alias.name
exported_name = alias.asname if alias.asname else symbol_name
source_keys = [
f"{package_name}.{sub_module}.{symbol_name}",
f"{package_name}.__{sub_module}.{symbol_name}" if not sub_module.startswith('__') else None,
symbol_name,
]
target_key = f"{package_name}.{exported_name}"
if target_key not in trans.translator.SymbolTable:
for source_key in source_keys:
if source_key and source_key in trans.translator.SymbolTable:
trans.translator.SymbolTable[target_key] = trans.translator.SymbolTable[source_key]
break
if exported_name not in trans.translator.SymbolTable:
for source_key in source_keys:
if source_key and source_key in trans.translator.SymbolTable:
trans.translator.SymbolTable[exported_name] = trans.translator.SymbolTable[source_key]
break
except Exception as e:
_vlog().warning(f" [警告] 解析包重导出失败 {init_pyi_path}: {e}")
def _compile_ll_files(self, active_sha1s: set[str]) -> None:
"""将 output_dir 中属于 active_sha1s 的 .ll 文件编译为 .obj跳过已编译过的 include 文件)"""
# 第一次 walk清理 stale 文件(.obj/.deps.json/纯 .ll排除 .stub.ll/.text.ll
stale_files = []
for root, dirs, files in os.walk(self.output_dir):
for file in files:
if file.endswith('.obj'):
sha1 = file[:-4]
if sha1 not in active_sha1s:
stale_files.append(os.path.join(root, file))
elif file.endswith('.deps.json'):
sha1 = file[:-10]
if sha1 not in active_sha1s:
stale_files.append(os.path.join(root, file))
elif file.endswith('.ll') and not file.endswith('.stub.ll') and not file.endswith('.text.ll'):
sha1 = file[:-3]
if sha1 not in active_sha1s:
stale_files.append(os.path.join(root, file))
if stale_files:
for fpath in stale_files:
try:
os.remove(fpath)
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
_vlog().info(f"[清理] 删除 {len(stale_files)} 个过时文件")
# 第二次 walk收集需要编译的 .ll 文件(排除 .stub.ll/.text.ll
ll_files = []
for root, dirs, files in os.walk(self.output_dir):
for file in files:
if file.endswith('.ll') and not file.endswith('.stub.ll') and not file.endswith('.text.ll'):
sha1 = file[:-3]
if sha1 in active_sha1s:
fpath = os.path.join(root, file)
obj_path = os.path.join(root, file[:-3] + '.obj')
if not os.path.isfile(obj_path):
ll_files.append(fpath)
if not ll_files:
_vlog().info("[编译] 无 .ll 文件需要重新编译")
else:
_vlog().info(f"\n[编译] 找到 {len(ll_files)} 个 .ll 文件需要编译")
# 第三次 walk收集 all_ll_files用于 alwaysinline 检测,排除 include sha1s
all_ll_files = []
for root, dirs, files in os.walk(self.output_dir):
for file in files:
if file.endswith('.ll') and not file.endswith('.stub.ll') and not file.endswith('.text.ll'):
sha1 = file[:-3]
if sha1 in active_sha1s and sha1 not in self._include_sha1s:
all_ll_files.append(os.path.join(root, file))
has_inline = False
for ll_path in all_ll_files:
try:
with open(ll_path, 'r', encoding='utf-8') as f:
content = f.read()
if 'alwaysinline' in content:
has_inline = True
break
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
if has_inline:
llvm_link = shutil.which('llvm-link') or shutil.which('llvm-link.exe')
opt_cmd = shutil.which('opt') or shutil.which('opt.exe')
if llvm_link and opt_cmd:
_vlog().info(" [内联] 检测到 alwaysinline 函数,执行跨模块内联优化...")
for ll_path in all_ll_files:
try:
with open(ll_path, 'r', encoding='utf-8') as f:
content = f.read()
type_defs = []
type_def_indices = set()
for i, line in enumerate(content.splitlines()):
stripped = line.strip()
if stripped.startswith('%') and '= type' in stripped:
type_defs.append(line)
type_def_indices.add(i)
if type_def_indices:
lines = content.splitlines(True)
content_no_types = ''.join(line for i, line in enumerate(lines) if i not in type_def_indices)
header_end = 0
for i, line in enumerate(content_no_types.splitlines()):
stripped = line.strip()
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
header_end = i + 1
continue
break
lines2 = content_no_types.splitlines(True)
fixed = ''.join(lines2[:header_end]) + '\n'.join(type_defs) + '\n' + ''.join(lines2[header_end:])
with open(ll_path, 'w', encoding='utf-8') as f:
f.write(fixed)
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
merged_ll = os.path.join(self.output_dir, '_merged.ll')
optimized_ll = os.path.join(self.output_dir, '_merged_opt.ll')
try:
link_cmd = [llvm_link] + all_ll_files + ['-o', merged_ll]
result = subprocess.run(link_cmd, capture_output=True, text=True)
if result.returncode != 0:
_vlog().warning(f" llvm-link 失败: {result.stderr.strip()},回退到单独编译")
has_inline = False
else:
opt_result_cmd = [opt_cmd, '-passes=always-inline', merged_ll, '-S', '-o', optimized_ll]
result = subprocess.run(opt_result_cmd, capture_output=True, text=True)
if result.returncode != 0:
_vlog().warning(f" opt 失败: {result.stderr.strip()},回退到单独编译")
has_inline = False
else:
_vlog().info(" [内联] 跨模块内联优化完成")
with open(optimized_ll, 'r', encoding='utf-8') as f:
opt_content = f.read()
opt_content = re.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', opt_content, flags=re.MULTILINE)
opt_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local|@)', r'\1 dso_local ', opt_content, flags=re.MULTILINE)
# LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义
opt_content = re.sub(r'(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', opt_content, flags=re.MULTILINE)
opt_content = re.sub(r'(@[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)', r'\1 = \2 dso_local \3', opt_content)
opt_content = re.sub(r'(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)', r'\1 = dso_local \2', opt_content)
with open(optimized_ll, 'w', encoding='utf-8') as f:
f.write(opt_content)
merged_obj = os.path.join(self.output_dir, '_merged.obj')
try:
cmd = [self.compile_cmd] + self.compile_flags + ['-o', merged_obj, optimized_ll]
result = subprocess.run(cmd, capture_output=True, text=True,
cwd=self.output_dir)
if result.returncode == 0:
_vlog().success(f" 合并模块编译完成")
for ll_path in ll_files:
obj_path = ll_path[:-3] + '.obj'
sha1_obj = os.path.join(self.output_dir, os.path.basename(ll_path)[:-3] + '.obj')
if os.path.isfile(sha1_obj):
os.remove(sha1_obj)
return
else:
_vlog().warning(f" 合并模块编译失败: {result.stderr.strip()},回退到单独编译")
has_inline = False
except Exception as e:
_vlog().warning(f" 合并模块编译异常: {e},回退到单独编译")
has_inline = False
except FileNotFoundError:
_vlog().warning(f" 找不到 llvm-link 或 opt回退到单独编译")
has_inline = False
else:
_vlog().warning(" 未找到 llvm-link/opt 工具回退到单独编译alwaysinline 可能不生效)")
def _compile_one(ll_path: str) -> tuple[str, str, str]:
rel = os.path.relpath(ll_path, self.output_dir)
obj_path = ll_path[:-3] + '.obj'
if os.path.isfile(obj_path):
return (rel, 'cached', '')
try:
with open(ll_path, 'r', encoding='utf-8') as f:
ll_content = f.read()
sha1 = os.path.basename(ll_path)[:-3]
deps = self._LoadDeps(sha1)
stub_prefix = ''
if deps:
# 去重:同一个全局变量如果同时有 external 声明和实际定义,
# 移除 external 声明,避免 "redefinition of global" 错误
# 注意:此去重必须在收集现有符号之前执行,否则行号偏移会导致
# lines_to_remove 误删其他行(如全局变量定义)
ll_lines_for_dedup = ll_content.splitlines()
external_globals: dict[str, int] = {}
non_external_globals: dict[str, int] = {}
for i, line in enumerate(ll_lines_for_dedup):
stripped = line.strip()
if stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped):
gm = re.match(r'(@\S+)', stripped)
if gm:
gname = gm.group(1)
is_external = re.match(r'@\S+\s*=\s*external\s+', stripped) is not None
if is_external:
external_globals[gname] = i
else:
non_external_globals[gname] = i
lines_to_remove_global_dedup: set[int] = set()
for gname in external_globals:
if gname in non_external_globals:
lines_to_remove_global_dedup.add(external_globals[gname])
if lines_to_remove_global_dedup:
ll_content = '\n'.join(
line for i, line in enumerate(ll_lines_for_dedup)
if i not in lines_to_remove_global_dedup
) + '\n'
existing_symbols: dict[str, str] = {}
opaque_line_indices: dict[str, int] = {}
existing_declares: set[str] = set()
existing_declare_lines: dict[str, set[int]] = {}
for i, line in enumerate(ll_content.splitlines()):
stripped = line.strip()
if stripped.startswith('%') and '= type' in stripped:
name = Phase2Translator._extract_llvm_type_name(stripped)
if name:
if '= type opaque' in stripped:
existing_symbols[name] = 'opaque'
opaque_line_indices[name] = i
else:
existing_symbols[name] = 'full'
elif stripped.startswith('declare'):
dm = re.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', stripped)
if dm:
fname = dm.group(1)
existing_declares.add(fname)
existing_declare_lines.setdefault(fname, set()).add(i)
seen_stub_sha1: set[str] = set()
replaced_opaque_names: set[str] = set()
replaced_declare_names: set[str] = set()
all_stub_lines: list[str | None] = []
stub_type_map: dict[str, tuple[bool, int]] = {}
stub_declare_names: set[str] = set()
stub_declare_lines: dict[str, str] = {}
existing_defines: set[str] = set()
existing_global_defs: set[str] = set()
stub_global_defs: set[str] = set()
for line in ll_content.splitlines():
stripped = line.strip()
if stripped.startswith('define'):
dm = re.match(r'define\s+[^@]*@"?([^"\s(]+)"?', stripped)
if dm:
existing_defines.add(dm.group(1))
elif stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped):
gm = re.match(r'(@\S+)', stripped)
if gm:
existing_global_defs.add(gm.group(1))
for dep_name, dep_sha1 in deps.items():
if dep_sha1 in seen_stub_sha1:
continue
seen_stub_sha1.add(dep_sha1)
stub_path = os.path.join(self.output_dir, f"{dep_sha1}.stub.ll")
if not os.path.isfile(stub_path):
stub_path = os.path.join(self.temp_dir, f"{dep_sha1}.stub.ll")
if os.path.isfile(stub_path):
with open(stub_path, 'r', encoding='utf-8') as sf:
stub_content = sf.read()
for line in stub_content.splitlines():
stripped = line.strip()
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
continue
if stripped.startswith('declare'):
dm = re.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', stripped)
if dm:
fname = dm.group(1)
if 'zdef_alloc' in fname:
_vlog().debug(f"[DEBUG] stub declare for {fname}: existing_declares={fname in existing_declares}, stub_declare_names={fname in stub_declare_names}, has_struct={'%' in stripped}, existing={existing_declare_lines.get(fname)}")
if fname in existing_defines:
continue
if fname in existing_declares or fname in stub_declare_names:
existing_decl_line = stub_declare_lines.get(fname)
if existing_decl_line is not None:
has_struct = '%' in stripped
existing_has_struct = '%' in existing_decl_line if existing_decl_line else False
if has_struct and not existing_has_struct:
for i, l in enumerate(all_stub_lines):
if l is not None and l.strip().startswith('declare'):
em = re.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', l.strip())
if em and em.group(1) == fname:
all_stub_lines[i] = line
break
continue
elif fname in existing_declares:
has_struct = '%' in stripped
if has_struct:
replaced_declare_names.add(fname)
stub_declare_names.add(fname)
stub_declare_lines[fname] = line
all_stub_lines.append(line)
continue
continue
stub_declare_names.add(fname)
stub_declare_lines[fname] = line
all_stub_lines.append(line)
continue
if stripped.startswith('%') and '= type' in stripped:
name = Phase2Translator._extract_llvm_type_name(stripped)
if name:
is_opaque = '= type opaque' in stripped
if name in existing_symbols:
if existing_symbols[name] == 'opaque' and not is_opaque:
replaced_opaque_names.add(name)
existing_symbols[name] = 'full'
idx = len(all_stub_lines)
all_stub_lines.append(line)
stub_type_map[name] = (is_opaque, idx)
continue
if name in stub_type_map:
prev_opaque, prev_idx = stub_type_map[name]
if prev_opaque and not is_opaque:
all_stub_lines[prev_idx] = None
idx = len(all_stub_lines)
all_stub_lines.append(line)
stub_type_map[name] = (is_opaque, idx)
continue
idx = len(all_stub_lines)
all_stub_lines.append(line)
stub_type_map[name] = (is_opaque, idx)
else:
all_stub_lines.append(line)
elif stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped):
gm = re.match(r'(@\S+)', stripped)
if gm and (gm.group(1) in existing_global_defs or gm.group(1) in stub_global_defs):
continue
if gm:
stub_global_defs.add(gm.group(1))
all_stub_lines.append(line)
else:
all_stub_lines.append(line)
filtered = [l for l in all_stub_lines if l is not None]
if filtered:
stub_prefix = '\n'.join(filtered) + '\n'
lines_to_remove = set()
if replaced_opaque_names:
for name in replaced_opaque_names:
if name in opaque_line_indices:
lines_to_remove.add(opaque_line_indices[name])
if replaced_declare_names:
_vlog().debug(f"[DEBUG] replaced_declare_names: {replaced_declare_names}")
for fname in replaced_declare_names:
if fname in existing_declare_lines:
lines_to_remove.update(existing_declare_lines[fname])
_vlog().debug(f"[DEBUG] removing lines {existing_declare_lines[fname]} for {fname}")
else:
_vlog().debug(f"[DEBUG] fname {fname} not in existing_declare_lines: {list(existing_declare_lines.keys())}")
if lines_to_remove:
ll_lines = ll_content.splitlines(True)
ll_content = ''.join(line for i, line in enumerate(ll_lines) if i not in lines_to_remove)
type_defs_in_ll = []
type_def_line_indices = set()
for i, line in enumerate(ll_content.splitlines()):
stripped = line.strip()
if stripped.startswith('%') and '= type' in stripped:
type_defs_in_ll.append(line)
type_def_line_indices.add(i)
if type_def_line_indices:
ll_lines = ll_content.splitlines(True)
ll_content = ''.join(line for i, line in enumerate(ll_lines) if i not in type_def_line_indices)
if type_defs_in_ll:
stub_prefix += '\n'.join(type_defs_in_ll) + '\n'
if stub_prefix:
header_end = 0
for i, line in enumerate(ll_content.splitlines()):
stripped = line.strip()
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
header_end = i + 1
continue
break
ll_lines = ll_content.splitlines(True)
ll_content = ''.join(ll_lines[:header_end]) + stub_prefix + ''.join(ll_lines[header_end:])
ll_content = re.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', ll_content, flags=re.MULTILINE)
ll_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local|@)', r'\1 dso_local ', ll_content, flags=re.MULTILINE)
# LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义
ll_content = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', ll_content, flags=re.MULTILINE)
ll_content = re.sub(r'^(@[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)', r'\1 = \2 dso_local \3', ll_content, flags=re.MULTILINE)
ll_content = re.sub(r'^(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)', r'\1 = dso_local \2', ll_content, flags=re.MULTILINE)
# LLVM 22+: 移除同一模块中已有 define 的 declare 语句LLVM 22 将 declare+define 视为重复定义)
defined_funcs: set[str] = set()
for m in re.finditer(r'^define\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(', ll_content, re.MULTILINE):
defined_funcs.add(m.group(1))
if defined_funcs:
lines: list[str] = ll_content.splitlines(True)
kept_lines: list[str] = []
for line in lines:
m = re.match(r'^declare\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(', line)
if m and m.group(1) in defined_funcs:
continue
kept_lines.append(line)
ll_content = ''.join(kept_lines)
# 移除重复的 declare 语句(同一函数多次 declare 但没有 define
seen_declares: set[str] = set()
dedup_lines: list[str] = ll_content.splitlines(True)
dedup_kept: list[str] = []
for line in dedup_lines:
dm = re.match(r'^declare\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(', line)
if dm:
fname = dm.group(1)
if fname in seen_declares:
continue
seen_declares.add(fname)
dedup_kept.append(line)
ll_content = ''.join(dedup_kept)
final_type_defs = []
final_type_def_indices = set()
seen_type_names_main = set()
for i, line in enumerate(ll_content.splitlines()):
stripped = line.strip()
if stripped.startswith('%') and '= type' in stripped:
type_name_main = stripped.split('=')[0].strip()
final_type_def_indices.add(i)
if type_name_main in seen_type_names_main:
continue
seen_type_names_main.add(type_name_main)
final_type_defs.append(line)
if final_type_def_indices:
final_lines = ll_content.splitlines(True)
ll_content_no_types = ''.join(line for i, line in enumerate(final_lines) if i not in final_type_def_indices)
header_end = 0
for i, line in enumerate(ll_content_no_types.splitlines()):
stripped = line.strip()
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
header_end = i + 1
continue
break
final_lines2 = ll_content_no_types.splitlines(True)
ll_content = ''.join(final_lines2[:header_end]) + '\n'.join(final_type_defs) + '\n' + ''.join(final_lines2[header_end:])
dso_ll_path = ll_path[:-3] + '_dso.ll'
with open(dso_ll_path, 'w', encoding='utf-8') as f:
f.write(ll_content)
cmd = [self.compile_cmd] + self.compile_flags + ['-o', obj_path, dso_ll_path]
result = subprocess.run(cmd, capture_output=True, text=True,
cwd=os.path.dirname(ll_path) or '.')
if result.returncode == 0:
return (rel, 'ok', '')
else:
return (rel, 'error', result.stderr.strip())
except FileNotFoundError:
return (rel, 'error', f'找不到编译器: {self.compile_cmd}')
except Exception as e:
return (rel, 'error', str(e))
need_compile = []
for ll_path in ll_files:
obj_path = ll_path[:-3] + '.obj'
if not os.path.isfile(obj_path):
need_compile.append(ll_path)
if need_compile:
n_workers = min(os.cpu_count() or 4, len(need_compile), 8)
_vlog().info(f" 并行编译 {len(need_compile)} 个文件 (workers={n_workers})")
errors = []
with ThreadPoolExecutor(max_workers=n_workers) as executor:
futures = {executor.submit(_compile_one, ll_path): ll_path for ll_path in need_compile}
for future in as_completed(futures):
rel, status, err = future.result()
if status == 'ok':
_vlog().success(f" {rel}")
elif status == 'error':
_vlog().error(f" {rel}: {err}")
errors.append((rel, err))
elif status == 'cached':
pass
if errors:
_vlog().error(f"\n{len(errors)} 个文件编译失败")
sys.exit(1)
def _precollect_vtable_state(self) -> None:
"""预扫描 includes .py 文件,收集 @t.CVTable 类的虚表信息。
并行编译ProcessPoolExecutor创建独立内存的 worker 进程,
vtable 状态必须预收集并通过 pickle 共享。
复用 parse_python_file 的 AST 缓存。
"""
for includes_dir in self.include_dirs:
if not os.path.isdir(includes_dir):
continue
for root, dirs, files in os.walk(includes_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if not fname.endswith('.py'):
continue
py_path: str = os.path.join(root, fname)
code, tree = parse_python_file(py_path)
# 字符串预过滤:不含 CVTable 的文件跳过 AST 遍历
if tree is None or 'CVTable' not in code:
continue
try:
for node in ast.iter_child_nodes(tree):
if not isinstance(node, ast.ClassDef):
continue
has_cvtable: bool = False
for dec in node.decorator_list:
if isinstance(dec, ast.Attribute):
if getattr(dec.value, 'id', None) == 't' and dec.attr == 'CVTable':
has_cvtable = True
elif isinstance(dec, ast.Name):
if dec.id == 'CVTable':
has_cvtable = True
if not has_cvtable:
continue
class_name: str = node.name
has_methods: bool = False
if class_name not in self._shared_class_methods:
self._shared_class_methods[class_name] = []
for item in node.body:
if isinstance(item, ast.FunctionDef):
has_methods = True
method_name: str = item.name
# 构造函数不放入 vtable避免子类与基类 vtable 大小不一致破坏多态分派)
if method_name in ('__new__', '__init__', '__before_init__'):
continue
if method_name == '__init__':
func_full_name: str = f"{class_name}.__init__"
elif method_name == '__call__':
func_full_name = f"{class_name}.__call__"
else:
func_full_name = f"{class_name}.{method_name}"
if func_full_name not in self._shared_class_methods[class_name]:
self._shared_class_methods[class_name].append(func_full_name)
if has_methods:
self._shared_class_vtable.add(class_name)
self._shared_cross_module_vtable.add(class_name)
except Exception:
pass
def _build_include_py_index(self, includes_dir: str) -> set[str]:
"""预扫描 includes 目录,构建所有 .py 文件路径集合
用 set 查询替代 os.path.isfile避免 3490 次磁盘 stat83s→<0.1s)。
同时规范化路径为小写Windows 不区分大小写)以加速查找。
"""
py_index: set[str] = set()
for root, dirs, files in os.walk(includes_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py'):
py_index.add(os.path.normcase(os.path.join(root, fname)))
return py_index
def _scan_include_libraries(self) -> None:
"""扫描 includes 目录,检测被使用的模块对应的库文件和源文件"""
if not self.used_includes:
return
for includes_dir in self.include_dirs:
if not os.path.isdir(includes_dir):
continue
# 预构建 .py 文件索引,避免递归发现时反复 os.path.isfile
py_index = self._build_include_py_index(includes_dir)
for module_name in self.used_includes:
module_dir = os.path.join(includes_dir, module_name)
if os.path.isdir(module_dir):
self._scan_dir_for_libs(module_dir, module_name)
for root, dirs, files in os.walk(module_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py'):
py_path = os.path.join(root, fname)
if py_path not in self.extra_py_files:
self.extra_py_files.append(py_path)
_vlog().info(f" [includes] 发现 Python 包文件: {os.path.relpath(py_path, includes_dir)}")
for ext in self.INCLUDE_LIB_EXTENSIONS:
lib_path = os.path.join(includes_dir, module_name + ext)
if os.path.isfile(lib_path):
self.extra_link_files.append(lib_path)
_vlog().info(f" [includes] 发现库文件: {os.path.relpath(lib_path, includes_dir)}")
for ext in self.INCLUDE_SRC_EXTENSIONS:
src_path = os.path.join(includes_dir, module_name + ext)
if os.path.isfile(src_path):
self.extra_compile_files.append(src_path)
_vlog().info(f" [includes] 发现源文件: {os.path.relpath(src_path, includes_dir)}")
py_path = os.path.join(includes_dir, module_name + '.py')
if os.path.normcase(py_path) in py_index:
self.extra_py_files.append(py_path)
_vlog().info(f" [includes] 发现 Python 源文件: {os.path.relpath(py_path, includes_dir)}")
discovered = set()
for ef in list(self.extra_py_files):
self._discover_transitive_deps(ef, includes_dir, discovered, py_index)
if self.extra_link_files:
_vlog().info(f"\n[includes] 需要链接的库文件: {len(self.extra_link_files)}")
if self.extra_compile_files:
_vlog().info(f"[includes] 需要编译的源文件: {len(self.extra_compile_files)}")
def _scan_dir_for_libs(self, directory: str, module_name: str) -> None:
"""递归扫描目录中的库文件和源文件"""
for root, dirs, files in os.walk(directory):
for fname in files:
fpath = os.path.join(root, fname)
_, ext = os.path.splitext(fname)
ext = ext.lower()
if ext in self.INCLUDE_LIB_EXTENSIONS:
self.extra_link_files.append(fpath)
_vlog().info(f" [includes] 发现库文件: {os.path.relpath(fpath, directory)}")
elif ext in self.INCLUDE_SRC_EXTENSIONS:
self.extra_compile_files.append(fpath)
_vlog().info(f" [includes] 发现源文件: {os.path.relpath(fpath, directory)}")
def _discover_transitive_deps(self, py_path: str, includes_dir: str, discovered: set[str], py_index: set[str] | None = None) -> None:
"""递归发现 Python 文件的间接依赖
复用 parse_python_file 的 AST 缓存,避免重复 open+read+ast.parse。
py_index 为预构建的 .py 文件路径集合,避免 os.path.isfile 磁盘 stat。
"""
if py_path in discovered:
return
discovered.add(py_path)
_, tree = parse_python_file(py_path)
if tree is None:
return
try:
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
self._try_add_include_dep(alias.name, includes_dir, discovered, py_index)
elif isinstance(node, ast.ImportFrom):
if node.module and node.level == 0:
self._try_add_include_dep(node.module, includes_dir, discovered, py_index)
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"发现 include 传递依赖失败: {_e}", "Exception")
def _try_add_include_dep(self, module_name: str, includes_dir: str, discovered: set, py_index: set[str] | None = None):
"""尝试将模块添加为 include 依赖(支持子包,如 w32.win32base
py_index 非空时用 set 查询替代 os.path.isfileO(1) vs 24ms/次)。
"""
# 候选路径子包路径w32/win32base.py、顶层模块w32.py、子包 __init__.py
candidates: list[str] = []
# 子包路径w32.win32base → w32/win32base.py
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep) + '.py'))
# 子包 __init__.pyw32.win32base → w32/win32base/__init__.py
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep), '__init__.py'))
# 顶层模块:取第一个组件 → w32.py
top_module = module_name.split('.')[0]
if top_module != module_name:
candidates.append(os.path.join(includes_dir, top_module + '.py'))
for py_path in candidates:
exists: bool
if py_index is not None:
exists = os.path.normcase(py_path) in py_index
else:
exists = os.path.isfile(py_path)
if exists and py_path not in self.extra_py_files:
self.extra_py_files.append(py_path)
_vlog().info(f" [includes] 发现间接依赖: {os.path.relpath(py_path, includes_dir)}")
self._discover_transitive_deps(py_path, includes_dir, discovered, py_index)
return # 找到第一个匹配即可
def _is_decl_only_file(self, src_path: str) -> bool:
"""复用 parse_python_file 的 AST 缓存"""
_, tree = parse_python_file(src_path)
if tree is None:
return False
try:
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
return False
if isinstance(node, ast.FunctionDef):
body = node.body
if len(body) == 1:
stmt = body[0]
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and stmt.value.value is ...:
continue
if isinstance(stmt, ast.Pass):
continue
return False
return True
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"判断是否为声明文件失败: {_e}", "Exception")
return False
def _sort_include_files_by_deps(self, file_list: list[str]) -> tuple[list[str], dict[str, set[str]]]:
"""按依赖关系对 include 文件进行拓扑排序,确保被依赖的文件先编译。
返回 (排序后的文件列表, 依赖字典)。
依赖字典: {fpath: set(依赖的fpath)},仅包含 file_list 中的依赖。
"""
# 构建文件名到路径的映射
name_to_path = {}
for fpath in file_list:
basename = os.path.splitext(os.path.basename(fpath))[0]
name_to_path[basename] = fpath
# 也支持包内模块: vpsdk/window -> path
for fpath in file_list:
# 尝试从 includes 目录推断模块全名
for includes_dir in self.include_dirs:
try:
rel = os.path.relpath(fpath, includes_dir)
mod_name = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
name_to_path[mod_name] = fpath
except ValueError:
pass
# 分析每个文件的 import 依赖(复用 parse_python_file 的 AST 缓存)
deps = {} # path -> set of paths it depends on
for fpath in file_list:
deps[fpath] = set()
try:
_, tree = parse_python_file(fpath)
if tree is None:
continue
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
# 尝试完整路径和各级前缀,确保同包内子模块依赖能被正确解析
# 例如 import zlib.zhuff -> 应先匹配 zlib.zhuff再回退到 zlib
parts = alias.name.split('.')
for i in range(len(parts), 0, -1):
mod = '.'.join(parts[:i])
if mod in name_to_path and name_to_path[mod] != fpath:
deps[fpath].add(name_to_path[mod])
break
elif isinstance(node, ast.ImportFrom):
if node.module and node.level == 0:
# 绝对导入:同样尝试完整路径和各级前缀
parts = node.module.split('.')
for i in range(len(parts), 0, -1):
mod = '.'.join(parts[:i])
if mod in name_to_path and name_to_path[mod] != fpath:
deps[fpath].add(name_to_path[mod])
break
elif node.level > 0:
# 相对导入from .xxx import yyy 或 from ..xxx import yyy
# 根据当前文件位置推断目标模块的完整路径
current_dir = os.path.dirname(fpath)
# node.level == 1 表示当前包node.level == 2 表示上一级包
package_dir = current_dir
for _ in range(node.level - 1):
package_dir = os.path.dirname(package_dir)
# 推断当前包名(从 includes 目录的相对路径)
package_name: str | None = None
for includes_dir in self.include_dirs:
try:
rel = os.path.relpath(package_dir, includes_dir)
if rel and rel != '.':
package_name = rel.replace(os.sep, '.').replace('/', '.')
break
except ValueError:
pass
# 构建目标模块名并查找
target_mod: str | None = None
if node.module:
target_mod = f"{package_name}.{node.module}" if package_name else node.module
else:
# from . import yyyyyy 是子模块,在 names 中
target_mod = package_name
if target_mod:
parts = target_mod.split('.')
for i in range(len(parts), 0, -1):
mod = '.'.join(parts[:i])
if mod in name_to_path and name_to_path[mod] != fpath:
deps[fpath].add(name_to_path[mod])
break
# from . import yyyyyy 可能是子模块
if node.level > 0 and not node.module:
for alias in node.names:
sub_mod = f"{package_name}.{alias.name}" if package_name else alias.name
if sub_mod in name_to_path and name_to_path[sub_mod] != fpath:
deps[fpath].add(name_to_path[sub_mod])
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"分析 include 文件导入依赖失败: {_e}", "Exception")
# 拓扑排序 (Kahn's algorithm)
# in_degree 只计入 file_list 中的依赖——decl-only 文件(如 stdint.py
# 不在 file_list 中,不应计入入度,否则依赖它们的文件入度永远不为 0
# 被误判为循环依赖通过 fallback 添加,导致编译顺序错误。
file_set = set(file_list)
in_degree = {fpath: sum(1 for dep in deps[fpath] if dep in file_set) for fpath in file_list}
# 反向邻接表:如果 A 依赖 B则 B -> A
reverse_adj = {fpath: [] for fpath in file_list}
for fpath in file_list:
for dep in deps[fpath]:
if dep in reverse_adj:
reverse_adj[dep].append(fpath)
result = []
queue = [fpath for fpath in file_list if in_degree[fpath] == 0]
# 对队列排序以保持确定性
queue.sort(key=lambda x: x)
while queue:
current = queue.pop(0)
result.append(current)
for neighbor in reverse_adj[current]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
queue.sort(key=lambda x: x)
# 如果有循环依赖,把剩余文件也加入
for fpath in file_list:
if fpath not in result:
result.append(fpath)
# 仅保留 file_list 内的依赖(用于波次并行翻译)
file_set_result = set(file_list)
deps_in_set = {fpath: (deps[fpath] & file_set_result) for fpath in file_list}
return result, deps_in_set
def _get_precompiled_obj_path(self, src_path: str, sha1: str) -> str | None:
"""计算 include 文件在 includes.binary/ 中的预编译 .obj 路径。
预编译目录位于 include 目录同级,命名为 {basename}.binary/。
文件命名为 {sha1}.{triple_safe}.{rel_model}.obj区分目标平台和重定位模型。
"""
AbsSrcPath: str = os.path.abspath(src_path)
RelModel: str = 'static'
for _flag in self.compile_flags:
if _flag.startswith('-relocation-model='):
RelModel = _flag[len('-relocation-model='):]
break
for inc_dir in self.include_dirs:
abs_inc: str = os.path.abspath(inc_dir)
try:
rel: str = os.path.relpath(AbsSrcPath, abs_inc)
if rel.startswith('..'):
continue
inc_parent: str = os.path.dirname(abs_inc)
inc_base: str = os.path.basename(abs_inc)
precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary')
triple_safe: str = (self.triple or 'default').replace(os.sep, '_').replace('/', '_').replace('\\', '_').replace(':', '_')
return os.path.join(precompiled_dir, f"{sha1}.{triple_safe}.{RelModel}.obj")
except ValueError:
continue
return None
def _compile_include_py_files(self, active_sha1s: set[str]) -> None:
"""通过 TransPyC 翻译器编译 includes 目录中发现的 .py 文件
优化流程:预注册符号 → 并行翻译 → 串行后处理 → 并行编译 → 注册+缓存
"""
if not self.extra_py_files:
return
# 按依赖关系排序 include 文件,确保被依赖的文件先编译
sorted_files, file_deps = self._sort_include_files_by_deps(self.extra_py_files)
_vlog().info(f"\n[includes 编译] 找到 {len(sorted_files)} 个 Python 源文件")
# 构建 include 文件的模块 SHA1 映射(预计算所有文件,构建完整映射)
include_ModuleSha1Map = self._build_current_ModuleSha1Map(None)
need_translate = [] # [(src_path, sha1, module_name, ll_path, obj_path, precompiled_obj)]
for src_path in sorted_files:
module_name = os.path.splitext(os.path.basename(src_path))[0]
# 复用 parse_python_file 缓存的 content避免重复 open+read
py_content, _ = parse_python_file(src_path)
sha1 = compute_sha1(py_content)
self.include_py_map[module_name] = sha1
active_sha1s.add(sha1)
self._include_sha1s.add(sha1)
for includes_dir in self.include_dirs:
try:
rel = os.path.relpath(src_path, includes_dir)
mod_full = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
include_ModuleSha1Map[mod_full] = sha1
include_ModuleSha1Map[module_name] = sha1
# 包入口文件:注册父包名(如 'os.__init__' -> 'os'
if module_name == '__init__' and '.' in mod_full:
parent_pkg = mod_full.rsplit('.', 1)[0]
include_ModuleSha1Map[parent_pkg] = sha1
except ValueError:
pass
include_ModuleSha1Map[module_name] = sha1
ll_path = os.path.join(self.output_dir, f"{sha1}.ll")
obj_path = os.path.join(self.output_dir, f"{sha1}.obj")
precompiled_obj = self._get_precompiled_obj_path(src_path, sha1)
if self._is_decl_only_file(src_path):
_vlog().info(f" 跳过(声明文件): {module_name}.py")
# 声明文件(如 stdint.py虽不编译为 .obj但其 typedef/类型别名
# 必须注册到共享符号表,否则依赖它的 include 文件编译时
# 类型解析失败(如 UINT32PTR 被当作命名结构体而非 i32*
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
self._collect_inline_symbols(src_path)
continue
# 优先检查 includes.binary 预编译缓存(跨项目共享)
if not self.force_recompile_includes and precompiled_obj and os.path.isfile(precompiled_obj):
_vlog().info(f" 跳过(预编译): {module_name}.py -> {os.path.basename(precompiled_obj)}")
# 预编译跳过的文件仍需注册 .pyi/.stub.ll 声明和符号,
# 否则依赖它的 include 文件(如 vpui.py import vpsdk.window
# 在 _EmitModuleDeclarationsLlvm 中找不到声明,导致 Undefined symbol
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
self._collect_inline_symbols(src_path)
self.extra_link_files.append(precompiled_obj)
continue
if not self.force_recompile_includes and os.path.isfile(obj_path):
_vlog().info(f" 跳过(缓存): {module_name}.py -> {sha1}.obj")
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
self._collect_inline_symbols(src_path)
self.extra_link_files.append(obj_path)
continue
need_translate.append((src_path, sha1, module_name, ll_path, obj_path, precompiled_obj))
if not need_translate:
return
# 预注册所有待翻译文件的符号(从 Phase1 缓存加载 .pyi / .stub.ll
# 这样并行翻译时所有跨模块符号都已可用
for src_path, sha1, module_name, ll_path, obj_path, precompiled_obj in need_translate:
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
# 预收集 stub_decls所有文件共用避免重复扫描 .stub.ll
stub_decls = []
for other_sha1, other_stub in self.stub_files.items():
if os.path.exists(other_stub):
try:
with open(other_stub, 'r', encoding='utf-8') as sf:
for sline in sf:
sl = sline.strip()
if sl.startswith('%') and '= type' in sl and 'opaque' not in sl:
struct_name = Phase2Translator._extract_llvm_type_name(sl)
if struct_name:
stub_decls.append(sl)
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
all_type_defs = {}
for sdecl in stub_decls:
tname = Phase2Translator._extract_llvm_type_name(sdecl)
if tname:
all_type_defs[tname] = sdecl
# ========== 阶段1并行翻译 ==========
_vlog().info(f" 翻译 {len(need_translate)} 个文件")
if len(need_translate) > 1:
# 序列化共享符号表供 worker 使用
shared_pickle_path = os.path.join(self.output_dir, '_shared_sym_includes.pickle')
try:
shared_data = {
'symbol_table': self._shared_symbol_table,
'source_module_sig_files': self._shared_source_module_sig_files,
'all_dc': self._shared_all_dc,
'export_extern_funcs': self._shared_export_extern_funcs,
'inline_func_symbols': self.inline_func_symbols,
'function_default_args': self.function_default_args,
'function_param_names': self.function_param_names,
'generic_class_templates': self._shared_generic_class_templates,
'shared_doc_meta': self._shared_doc_meta,
'class_vtable': self._shared_class_vtable,
'cross_module_vtable': self._shared_cross_module_vtable,
'class_methods': self._shared_class_methods,
'cross_module_novtable': self._shared_cross_module_novtable,
}
with open(shared_pickle_path, 'wb') as f:
f.write(pickle.dumps(shared_data))
except Exception as e:
_vlog().warning(f"符号表序列化失败({e})include将串行翻译")
shared_pickle_path = ''
if shared_pickle_path and os.path.exists(shared_pickle_path):
n_workers = min(8, len(need_translate))
errors = []
# 波次并行翻译:按依赖关系分批提交,确保被依赖的文件先翻译完成。
# 根因_TryLoadStructFromStub 扫描 output 目录加载跨模块 struct 定义,
# 若依赖文件尚未完成翻译output stub 未写入),只能取到 Phase1 temp
# 中的不完整 stub缺少跨模块继承字段导致运行时崩溃。
item_by_path = {item[0]: item for item in need_translate}
translate_paths = set(item_by_path.keys())
# 每个文件的依赖(仅限本轮待翻译集合内)
translate_deps = {
p: (file_deps.get(p, set()) & translate_paths)
for p in translate_paths
}
# 复用 self._executor_pool与 App 源文件阶段共享),避免 GC 阻塞
if self._executor_pool is None:
self._executor_pool = ProcessPoolExecutor(max_workers=n_workers)
executor = self._executor_pool
try:
completed_paths: set[str] = set()
remaining = list(need_translate) # 保持拓扑序
in_progress: dict = {} # future -> item
done_count = 0
while remaining or in_progress:
# 提交所有依赖已完成的文件
ready = [
item for item in remaining
if translate_deps[item[0]] <= completed_paths
]
for item in ready:
remaining.remove(item)
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
future = executor.submit(
_parallel_translate_worker,
src_path, ll_path,
self.src_root, self.temp_dir, self.output_dir,
self.include_dirs, self.triple, self.datalayout,
self.sha1_map, self.sig_files, self.stub_files,
self.include_py_map, self.slice_level,
shared_pickle_path, self.struct_sha1_map,
include_ModuleSha1Map
)
in_progress[future] = item
if not in_progress:
# 无可提交且无进行中:循环依赖 fallback
for item in remaining:
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
future = executor.submit(
_parallel_translate_worker,
src_path, ll_path,
self.src_root, self.temp_dir, self.output_dir,
self.include_dirs, self.triple, self.datalayout,
self.sha1_map, self.sig_files, self.stub_files,
self.include_py_map, self.slice_level,
shared_pickle_path, self.struct_sha1_map,
include_ModuleSha1Map
)
in_progress[future] = item
remaining.clear()
# 等待至少一个完成
done_set, _ = wait(in_progress, return_when=FIRST_COMPLETED)
for future in done_set:
item = in_progress.pop(future)
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
completed_paths.add(src_path)
done_count += 1
try:
status, _, err = future.result()
if status == 'ok':
_vlog().info(f" [{done_count}/{len(need_translate)}] 翻译完成: {module_name}.py")
else:
_vlog().error(f" {module_name}.py: {err}")
errors.append((module_name, err))
except Exception as e:
_vlog().error(f" {module_name}.py: {e}")
errors.append((module_name, str(e)))
except Exception:
if self._executor_pool is not None:
self._executor_pool.shutdown(wait=False, cancel_futures=True)
self._executor_pool = None
raise
if errors:
_vlog().error(f"\n{len(errors)} 个 include 文件翻译失败")
sys.exit(1)
else:
# 串行回退
for item in need_translate:
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
_vlog().info(f" 翻译: {module_name}.py -> {sha1}.ll")
self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map)
else:
item = need_translate[0]
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
_vlog().info(f" 翻译: {module_name}.py -> {sha1}.ll")
self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map)
# ========== 阶段2串行后处理dso_local + stub_decls 注入)==========
compile_tasks = [] # [(dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content)]
for item in need_translate:
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
if not os.path.isfile(ll_path):
_vlog().error(f" 翻译未生成 .ll 文件: {module_name}.py")
continue
with open(ll_path, 'r', encoding='utf-8') as f:
ll_content = f.read()
# dso_local 正则标注(使用 ^ + MULTILINE 避免匹配字符串常量内容中的 define/declare
ll_content = re.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', ll_content, flags=re.MULTILINE)
ll_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local)', r'\1 dso_local ', ll_content, flags=re.MULTILINE)
# LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义
ll_content = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', ll_content, flags=re.MULTILINE)
ll_content = re.sub(r'^(@[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)', r'\1 = \2 dso_local \3', ll_content, flags=re.MULTILINE)
ll_content = re.sub(r'^(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)', r'\1 = dso_local \2', ll_content, flags=re.MULTILINE)
# stub_decls 注入(复用预收集的 stub_decls
if stub_decls:
for sdecl in stub_decls:
sname = Phase2Translator._extract_llvm_type_name(sdecl)
if sname:
short = sname.split('.')[-1] if '.' in sname else sname
ll_content = re.sub(
r'%"?' + re.escape(sname) + r'"?\s*=\s*type\s*opaque',
sdecl, ll_content)
if short != sname:
ll_content = re.sub(
r'%"?' + re.escape(short) + r'"?\s*=\s*type\s*opaque',
sdecl, ll_content)
max_iterations = 10
for _ in range(max_iterations):
defined_types = set()
for line in ll_content.splitlines():
sl = line.strip()
if sl.startswith('%') and '= type' in sl:
tname = Phase2Translator._extract_llvm_type_name(sl)
if tname:
defined_types.add(tname)
# 同时添加短名,防止 missing 循环因全名/短名不匹配而重复添加
if '.' in tname:
parts = tname.split('.', 1)
if len(parts[0]) == 16 and all(c in '0123456789abcdef' for c in parts[0]):
defined_types.add(parts[1])
referenced_types = Phase2Translator._find_all_type_refs(ll_content)
missing = referenced_types - defined_types
new_defs = []
for tname in sorted(missing):
if tname in all_type_defs:
new_defs.append(all_type_defs[tname])
else:
short_key = tname.split('.')[-1] if '.' in tname else tname
for full_name, defn in all_type_defs.items():
if full_name.split('.')[-1] == short_key:
if short_key not in defined_types:
new_defs.append(defn)
break
else:
# 未在 stub 中找到定义(如泛型类 list[T]
# 声明为 opaque 以满足 llc 的类型前向声明要求
new_defs.append(f'%"{tname}" = type opaque')
if not new_defs:
break
inject_block = '\n'.join(new_defs) + '\n'
lines = ll_content.splitlines(True)
insert_pos = 0
for i, line in enumerate(lines):
stripped = line.strip()
if stripped and not stripped.startswith(';') and not stripped.startswith('target ') and not stripped.startswith('source_filename') and not stripped.startswith('declare ') and not stripped.startswith('@') and not stripped.startswith('define ') and not stripped.startswith('attributes ') and not stripped.startswith('!') and not stripped.startswith('module '):
insert_pos = i
break
if insert_pos == 0:
insert_pos = len(lines)
ll_content = ''.join(lines[:insert_pos]) + inject_block + ''.join(lines[insert_pos:])
# 将所有类型定义移到 declares 之前
# llc 要求 by-value struct 参数的类型在 declare 前定义,否则报 invalid type for function argument
lines = ll_content.splitlines(True)
type_def_lines = []
other_lines = []
seen_type_names_inc = set()
for line in lines:
stripped = line.strip()
if stripped.startswith('%') and '= type' in stripped:
type_name_inc = stripped.split('=')[0].strip()
if type_name_inc in seen_type_names_inc:
continue
seen_type_names_inc.add(type_name_inc)
type_def_lines.append(line)
else:
other_lines.append(line)
if type_def_lines:
insert_pos = 0
for i, line in enumerate(other_lines):
stripped = line.strip()
if stripped and not stripped.startswith(';') and not stripped.startswith('target ') and not stripped.startswith('source_filename') and not stripped.startswith('!'):
insert_pos = i
break
if insert_pos == 0:
insert_pos = len(other_lines)
ll_content = ''.join(other_lines[:insert_pos]) + ''.join(type_def_lines) + ''.join(other_lines[insert_pos:])
dso_ll_path = ll_path[:-3] + '_dso.ll'
with open(dso_ll_path, 'w', encoding='utf-8') as f:
f.write(ll_content)
compile_tasks.append((dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content))
if not compile_tasks:
return
# ========== 阶段3并行编译llc==========
_vlog().info(f" 编译 {len(compile_tasks)} 个 .ll 文件")
compile_results = {} # task -> result
def _compile_one_include(task):
dso_ll_path, obj_path = task[0], task[1]
cmd = [self.compile_cmd] + self.compile_flags + ['-o', obj_path, dso_ll_path]
result = subprocess.run(cmd, capture_output=True, text=True, cwd=self.output_dir)
return (task, result)
if len(compile_tasks) > 1:
n_workers = min(8, len(compile_tasks))
with ThreadPoolExecutor(max_workers=n_workers) as executor:
futures = {executor.submit(_compile_one_include, task): task for task in compile_tasks}
done_count = 0
for future in as_completed(futures):
done_count += 1
try:
task, result = future.result()
module_name = task[4]
if result.returncode == 0:
_vlog().success(f" [{done_count}/{len(compile_tasks)}] 编译成功: {module_name}.py")
else:
_vlog().warning(f" [{done_count}/{len(compile_tasks)}] 编译失败: {module_name}.py: {result.stderr.strip()}")
compile_results[task] = result
except Exception as e:
task = futures[future]
module_name = task[4]
_vlog().error(f" [{done_count}/{len(compile_tasks)}] 编译异常: {module_name}.py: {e}")
compile_results[task] = None
else:
task = compile_tasks[0]
task_result, result = _compile_one_include(task)
module_name = task[4]
if result.returncode == 0:
_vlog().success(f" 编译成功: {module_name}.py")
else:
_vlog().warning(f" 编译失败: {module_name}.py: {result.stderr.strip()}")
compile_results[task] = result
# ========== 阶段4注册 + 缓存 ==========
has_error = False
for task in compile_tasks:
dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content = task
result = compile_results.get(task)
if result and result.returncode == 0:
self.extra_link_files.append(obj_path)
# 复制到 includes.binary 预编译目录
if precompiled_obj:
precompiled_dir = os.path.dirname(precompiled_obj)
try:
if not os.path.exists(precompiled_dir):
os.makedirs(precompiled_dir, exist_ok=True)
shutil.copy2(obj_path, precompiled_obj)
except Exception as _e:
if _config_mode == "strict":
_vlog().warning(f"预编译缓存写入失败: {_e}")
# 注册符号
self._register_compiled_include(src_path, sha1, ll_content, include_ModuleSha1Map)
# 清理临时文件(仅成功时清理,失败时保留以便调试)
# DEBUG: 保留 .ll 文件以便静态分析
# for ext in ['.ll', '_dso.ll']:
# p = ll_path[:-3] + ext
# if os.path.isfile(p):
# os.remove(p)
else:
has_error = True
_vlog().warning(f" 保留失败文件以便调试: {ll_path}")
if has_error:
_vlog().error(f"\nincludes 文件编译失败")
sys.exit(1)
# 从 include 文件源码中提取函数参数名,供主项目编译时 keyword 参数匹配使用
self._extract_include_param_names(sorted_files)
def _extract_include_param_names_from_dirs(self) -> None:
"""从 include 目录的 Python 源码中提取函数参数名,存入 function_param_names
优化:单次递归遍历 AST维护当前类上下文O(N) 复杂度。
复用 parse_python_file 的 AST 缓存。
"""
for includes_dir in self.include_dirs:
if not os.path.isdir(includes_dir):
continue
for filename in os.listdir(includes_dir):
if not filename.endswith('.py'):
continue
src_path = os.path.join(includes_dir, filename)
try:
source, tree = parse_python_file(src_path)
if tree is None:
continue
sha1 = compute_sha1(source)
self._collect_function_params(tree, sha1)
except Exception as e:
_vlog().warning(f" 提取参数名失败 {src_path}: {e}")
def _collect_function_params(self, tree: ast.AST, sha1: str | None) -> None:
"""单次递归遍历 AST 收集函数参数名含类方法O(N) 复杂度
遍历时维护 current_class 上下文:遇到 ClassDef 进入类作用域,
遇到 FunctionDef 记录参数名并标注所属类,函数内部的嵌套定义
不再属于外层类。
"""
def visit(node: ast.AST, current_class: str | None) -> None:
for child in ast.iter_child_nodes(node):
if isinstance(child, ast.ClassDef):
# 进入类作用域,递归处理类体
visit(child, child.name)
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
func_name = child.name
param_names = [arg.arg for arg in child.args.args]
self.function_param_names[func_name] = param_names
if sha1:
self.function_param_names[f'{sha1}.{func_name}'] = param_names
if current_class:
self.function_param_names[f'{current_class}.{func_name}'] = param_names
if sha1:
self.function_param_names[f'{sha1}.{current_class}.{func_name}'] = param_names
# 函数内部的嵌套定义不属于外层类
visit(child, None)
else:
visit(child, current_class)
visit(tree, None)
def _extract_app_default_args(self, need_translate: list) -> None:
"""从 App 源文件中提取函数默认参数,存入 function_default_args
在 Phase 2 并行编译开始前调用,确保跨模块调用参数检查能正确识别默认参数。
need_translate 中每个元素是 (i, src_path, out_path, sha1, current_ModuleSha1Map)
复用 parse_python_file 的 AST 缓存run() 循环已 parse 过)。
"""
for entry in need_translate:
src_path = entry[1]
sha1 = entry[3]
_, tree = parse_python_file(src_path)
if tree is None:
continue
try:
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
func_name = node.name
defaults = getattr(node.args, 'defaults', [])
if defaults:
default_vals = []
for d in defaults:
if isinstance(d, ast.Constant):
default_vals.append(d.value)
else:
default_vals.append(None)
# 存储多种键格式,确保跨模块调用检查能找到
self.function_default_args[func_name] = default_vals
self.function_default_args[f'{sha1}.{func_name}'] = default_vals
# 也更新参数名(预扫描时补充)
param_names = [arg.arg for arg in node.args.args]
if func_name not in self.function_param_names:
self.function_param_names[func_name] = param_names
if f'{sha1}.{func_name}' not in self.function_param_names:
self.function_param_names[f'{sha1}.{func_name}'] = param_names
except Exception as e:
_vlog().warning(f" 提取默认参数失败 {src_path}: {e}")
def _extract_include_param_names(self, sorted_files: list[str]) -> None:
"""从 include 文件的 Python 源码中提取函数参数名,存入 function_param_names
优化:复用 _collect_function_params 的 O(N) 单次遍历 + parse_python_file 缓存。
"""
for src_path in sorted_files:
if self._is_decl_only_file(src_path):
continue
try:
_, tree = parse_python_file(src_path)
if tree is None:
continue
module_name = os.path.splitext(os.path.basename(src_path))[0]
sha1 = self.include_py_map.get(module_name)
self._collect_function_params(tree, sha1)
except Exception as e:
_vlog().warning(f" 提取参数名失败 {src_path}: {e}")
def _register_compiled_include(self, src_path: str, sha1: str, ll_content: str, include_ModuleSha1Map: dict[str, str]) -> None:
"""编译 include 文件成功后,生成签名和 stub 文件并注册到符号表中"""
module_name = os.path.splitext(os.path.basename(src_path))[0]
# 确定完整模块名(如 vpsdk.window
mod_full = module_name
for includes_dir in self.include_dirs:
try:
rel = os.path.relpath(src_path, includes_dir)
mod_full = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
break
except ValueError:
pass
# 1. 生成 .pyi 签名文件(如果还没有的话)
sig_path = os.path.join(self.temp_dir, f"{sha1}.pyi")
if not os.path.isfile(sig_path):
# 全局缓存检查(不被 --clean 清除)
_TryGlobalCache(sha1, 'pyi', sig_path)
if not os.path.isfile(sig_path):
try:
with open(src_path, 'r', encoding='utf-8') as f:
py_content = f.read()
sig_content = PythonToStubConverter.convert(py_content, mod_full)
os.makedirs(self.temp_dir, exist_ok=True)
with open(sig_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(sig_content)
_SaveToGlobalCache(sha1, 'pyi', sig_path)
except Exception as e:
if _config_mode == "strict":
raise
_vlog().warning(f"签名生成失败不阻塞编译: {e}", "Exception")
# 注册到 sig_files 和 _source_module_sig_files
if os.path.isfile(sig_path):
self.sig_files[sha1] = sig_path
self.sha1_map[sha1] = f"includes/{mod_full.replace('.', '/')}.py"
if self._shared_source_module_sig_files is not None:
self._shared_source_module_sig_files[mod_full] = sig_path
self._shared_source_module_sig_files[module_name] = sig_path
# 将签名信息加载到共享符号表中
if self._shared_symbol_table is not None:
try:
self._shared_symbol_table.LoadModuleSymbols(sig_path, mod_full, lineno=0)
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"加载模块签名到共享符号表失败: {_e}", "Exception")
# 2. 生成 .stub.ll 文件
stub_path = os.path.join(self.output_dir, f"{sha1}.stub.ll")
if not os.path.isfile(stub_path):
# 全局缓存检查(不被 --clean 清除)
_TryGlobalCache(sha1, 'stub.ll', stub_path)
if not os.path.isfile(stub_path) and ll_content:
try:
stub_content, _ = self._split_ll(ll_content)
with open(stub_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(stub_content)
_SaveToGlobalCache(sha1, 'stub.ll', stub_path)
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"生成 .stub.ll 文件失败: {_e}", "Exception")
# 注册到 stub_files
if os.path.isfile(stub_path):
self.stub_files[sha1] = stub_path
elif os.path.isfile(sig_path):
# 如果 stub 文件不存在,也尝试从 temp 目录查找
temp_stub = os.path.join(self.temp_dir, f"{sha1}.stub.ll")
if os.path.isfile(temp_stub):
self.stub_files[sha1] = temp_stub
else:
# 最后尝试从全局缓存直接读取
_TryGlobalCache(sha1, 'stub.ll', temp_stub)
if os.path.isfile(temp_stub):
self.stub_files[sha1] = temp_stub
def _precollect_inline_symbols(self) -> None:
"""在主翻译循环之前扫描includes目录中used_includes相关的模块收集内联函数AST"""
if not self.used_includes:
return
for includes_dir in self.include_dirs:
if not os.path.isdir(includes_dir):
continue
for module_name in self.used_includes:
module_dir = os.path.join(includes_dir, module_name)
if os.path.isdir(module_dir):
for root, dirs, files in os.walk(module_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py'):
src_path = os.path.join(root, fname)
self._collect_inline_symbols(src_path)
py_path = os.path.join(includes_dir, module_name + '.py')
if os.path.isfile(py_path):
self._collect_inline_symbols(py_path)
for ef in self.extra_py_files:
self._collect_inline_symbols(ef)
def _collect_inline_symbols(self, src_path: str):
"""解析源文件收集内联函数的AST信息到inline_func_symbols
复用 parse_python_file 的 AST 缓存,避免重复 open+read+ast.parse。
"""
try:
_, tree = parse_python_file(src_path)
if tree is None:
return
module_name = os.path.splitext(os.path.basename(src_path))[0]
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.FunctionDef) and node.returns:
is_inline = False
if isinstance(node.returns, ast.BinOp) and isinstance(node.returns.op, ast.BitOr):
for part in [node.returns.left, node.returns.right]:
if isinstance(part, ast.Attribute) and part.attr == 'CInline':
is_inline = True
break
if isinstance(part, ast.Name) and part.id == 'CInline':
is_inline = True
break
elif isinstance(node.returns, ast.Attribute) and node.returns.attr == 'CInline':
is_inline = True
elif isinstance(node.returns, ast.Name) and node.returns.id == 'CInline':
is_inline = True
if is_inline:
func_name = node.name
sym_key = f"{module_name}.{func_name}"
info = CTypeInfo()
info.Name = func_name
info.IsFunction = True
info.IsInline = True
info.InlineBody = node.body
info.InlineParams = [arg.arg for arg in node.args.args]
info.Storage = t.CInline()
self.inline_func_symbols[func_name] = info
self.inline_func_symbols[sym_key] = info
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"预收集内联函数符号失败: {_e}", "Exception")
def _build_shared_symbol_data(self) -> None:
"""一次性构建所有文件共享的符号表数据,避免每个文件重复加载"""
t0 = time.time()
_vlog().info("[缓存] 构建共享符号表数据...")
base_trans = TransPyC.TransPyC(code="pass", triple=self.triple, datalayout=self.datalayout)
base_trans.translator.LlvmGen = None
base_trans.translator._ModuleSha1Map = {}
base_trans.translator._global_function_default_args = self.function_default_args
base_trans.translator._global_function_param_names = self.function_param_names
for includes_dir in self.include_dirs:
if os.path.isdir(includes_dir):
for pyi_file in os.listdir(includes_dir):
if pyi_file.endswith('.pyi'):
pyi_path = os.path.join(includes_dir, pyi_file)
module_name = os.path.splitext(pyi_file)[0]
base_trans.translator.SymbolTable.LoadModuleSymbols(pyi_path, module_name, lineno=0)
for py_file in os.listdir(includes_dir):
if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'):
module_name = os.path.splitext(py_file)[0]
sha1_key = self.include_py_map.get(module_name)
if sha1_key and sha1_key in self.sig_files:
sig_path = self.sig_files[sha1_key]
base_trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0)
# 收集需要重导出的包,等所有模块符号加载完后再处理
_pending_reexports = []
for sha1_key, sig_path in self.sig_files.items():
rel_path = self.sha1_map.get(sha1_key, sha1_key)
if rel_path.startswith('includes/'):
mod_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
mod_name = mod_name[len('includes/'):]
base_trans.translator.SymbolTable.LoadModuleSymbols(sig_path, mod_name, lineno=0)
if mod_name.endswith('.__init__'):
package_name = mod_name[:-len('.__init__')]
_pending_reexports.append((sig_path, package_name))
# 所有 includes 模块符号已加载,现在处理包重导出
for sig_path, package_name in _pending_reexports:
self._register_package_reexports(base_trans, sig_path, package_name)
for sha1_key, sig_path in self.sig_files.items():
rel_path = self.sha1_map.get(sha1_key, sha1_key)
if rel_path.startswith('includes/'):
continue
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
base_trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0)
base_trans.translator._source_module_sig_files[module_name] = sig_path
short_name = module_name.split('.')[-1] if '.' in module_name else module_name
if short_name != module_name:
base_trans.translator._source_module_sig_files[short_name] = sig_path
all_dc = {}
for sha1_key, stub_path in self.stub_files.items():
if os.path.exists(stub_path):
try:
with open(stub_path, 'r', encoding='utf-8') as f:
stub_content = f.read()
for line in stub_content.splitlines():
line = line.strip()
if line.startswith('@') and '= global' in line and 'i32' in line and not line.startswith('@llvm'):
var_name = line.split('=')[0].strip().lstrip('@')
val_part = line.split('i32')[-1].strip().rstrip(']').lstrip('[').lstrip('i32')
try:
val = int(val_part)
all_dc[var_name] = val
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
except Exception as e:
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
for sha1_key, pyi_path in self.sig_files.items():
_ExtractCDefineConstantsFromPyi(pyi_path, all_dc)
export_extern_funcs = {}
for sha1_key, sig_path in self.sig_files.items():
try:
# 复用 parse_python_file 的 AST 缓存sig 文件被多处解析)
_, sig_tree = parse_python_file(sig_path)
if sig_tree is None:
continue
for node in ast.iter_child_nodes(sig_tree):
if isinstance(node, ast.FunctionDef):
if _check_annotation_for_export(node.returns):
export_extern_funcs[node.name] = sha1_key
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"收集共享导出外部函数失败: {_e}", "Exception")
for sym_name, sym_info in self.inline_func_symbols.items():
if sym_name not in base_trans.translator.SymbolTable:
base_trans.translator.SymbolTable[sym_name] = sym_info
else:
existing = base_trans.translator.SymbolTable[sym_name]
if not getattr(existing, 'InlineBody', None) and getattr(sym_info, 'InlineBody', None):
existing.IsInline = sym_info.IsInline
existing.InlineBody = sym_info.InlineBody
existing.InlineParams = sym_info.InlineParams
self._shared_symbol_table = base_trans.translator.SymbolTable
self._shared_source_module_sig_files = dict(base_trans.translator._source_module_sig_files)
self._shared_all_dc = all_dc
self._shared_export_extern_funcs = export_extern_funcs
generic_class_templates = {}
for includes_dir in self.include_dirs:
if os.path.isdir(includes_dir):
abs_includes = os.path.join(self.src_root, includes_dir) if not os.path.isabs(includes_dir) else includes_dir
if not os.path.isdir(abs_includes):
abs_includes = includes_dir
for py_file in os.listdir(abs_includes):
if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'):
py_path = os.path.join(abs_includes, py_file)
try:
# 复用 parse_python_file 的 AST 缓存
py_content, py_tree = parse_python_file(py_path)
if py_tree is None:
continue
# 计算 include 文件的 SHA1供特化时统一命名空间
py_sha1: str = compute_sha1(py_content)
for node in py_tree.body:
if isinstance(node, ast.ClassDef) and hasattr(node, 'type_params') and node.type_params:
type_params = [tp.name for tp in node.type_params]
generic_class_templates[node.name] = {
'node': node,
'type_params': type_params,
'sha1': py_sha1,
}
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"收集泛型类模板失败: {_e}", "Exception")
self._shared_generic_class_templates = generic_class_templates
# 加载所有 {sha1}.doc.json 到 _shared_doc_meta供跨模块 __doc__ 查询
shared_doc_meta: dict[str, dict[str, str]] = {}
if os.path.isdir(self.temp_dir):
for fname in os.listdir(self.temp_dir):
if fname.endswith('.doc.json'):
sha1_key = fname[:-len('.doc.json')]
try:
with open(os.path.join(self.temp_dir, fname), 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, dict):
shared_doc_meta[sha1_key] = {str(k): str(v) for k, v in data.items()}
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"加载 {fname} 失败: {_e}", "Exception")
# 也从全局缓存加载 .doc.jsontemp_dir 被 --clean 清空时补充)
if os.path.isdir(_GLOBAL_CACHE_DIR):
for fname in os.listdir(_GLOBAL_CACHE_DIR):
if fname.endswith('.doc.json'):
sha1_key = fname[:-len('.doc.json')]
if sha1_key in shared_doc_meta:
continue
try:
with open(os.path.join(_GLOBAL_CACHE_DIR, fname), 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, dict):
shared_doc_meta[sha1_key] = {str(k): str(v) for k, v in data.items()}
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"加载全局缓存 {fname} 失败: {_e}", "Exception")
self._shared_doc_meta = shared_doc_meta
t1 = time.time()
_vlog().info(f"[缓存] 共享符号表构建完成 ({t1-t0:.2f}s, {len(self._shared_symbol_table)} 符号, {len(generic_class_templates)} 泛型模板, {len(shared_doc_meta)} doc 元数据)")
def _compile_include_sources(self):
"""扫描项目目录中的汇编存根,编译 includes 目录中发现的 C/C++ 源文件"""
extra_sources = list(self.extra_compile_files)
scan_dirs = []
if self.src_root and os.path.isdir(self.src_root):
scan_dirs.append(self.src_root)
# 不再扫描 output_dir 的父目录:当 output_dir 位于顶层时,
# 父目录是整个仓库根,会误扫到 ViperOS/includes/vqt6 等无关 C/C++ 源文件。
# 项目特定的 C/汇编文件应放在 src_root 下,或通过 extra_compile_files 指定。
for scan_dir in scan_dirs:
for root, dirs, files in os.walk(scan_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith(('.S', '.s', '.c', '.cpp')):
if fname.startswith('test_'):
continue
fpath = os.path.join(root, fname)
if fpath not in extra_sources:
extra_sources.append(fpath)
if not extra_sources:
return
cc_map = {
'.c': 'gcc',
'.cpp': 'g++', '.cc': 'g++', '.cxx': 'g++',
'.m': 'clang', '.mm': 'clang++',
'.s': 'gcc', '.S': 'gcc',
}
_vlog().info(f"\n[includes 编译] 找到 {len(extra_sources)} 个源文件")
for src_path in extra_sources:
_, ext = os.path.splitext(src_path)
ext = ext.lower()
obj_path = os.path.join(self.output_dir, os.path.splitext(os.path.basename(src_path))[0] + '.obj')
if ext in ('.s', '.S') and any('oformat' in f for f in self.linker_flags):
cc = 'clang'
compile_src = src_path
extra_flags = ['-c', '-target', 'x86_64-none-elf', '-o', obj_path, compile_src]
else:
cc = cc_map.get(ext, 'gcc')
extra_flags = ['-c', '-o', obj_path, src_path]
_vlog().info(f" 编译: {os.path.basename(src_path)} -> {os.path.basename(obj_path)}")
try:
cmd = [cc] + extra_flags
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
_vlog().success(" 编译成功")
self.extra_link_files.append(obj_path)
else:
_vlog().error(f" 编译失败: {result.stderr.strip()}")
_vlog().error(f"\nC/C++ 源文件编译失败,立即终止编译。")
sys.exit(1)
except FileNotFoundError:
_vlog().error(f" 找不到编译器: {cc}")
_vlog().error(f"\n找不到编译器,立即终止编译。")
sys.exit(1)
except Exception as e:
_vlog().error(f" {e}")
_vlog().error(f"\n编译异常,立即终止编译。")
sys.exit(1)
def _strip_debug_sections(self, exe_path: str) -> None:
"""剥离 .exe 中的调试段(跳过裸机二进制)"""
if not os.path.exists(exe_path):
return
ext = os.path.splitext(exe_path)[1].lower()
if ext not in ('.exe', '.dll', '.obj', '.o', '.elf'):
_vlog().info(f" [剥离] 跳过({ext} 格式无需剥离)")
return
for tool in ['llvm-strip', 'strip']:
strip_cmd = shutil.which(tool)
if strip_cmd:
try:
result = subprocess.run(
[strip_cmd, '--strip-debug', exe_path],
capture_output=True, text=True
)
if result.returncode == 0:
_vlog().info(f" [剥离] 调试段已移除 ({tool})")
else:
_vlog().warning(f" [剥离] 警告: {result.stderr.strip()}")
except Exception as _e:
if _config_mode == "strict":
raise
_vlog().warning(f"剥离可执行文件调试段失败: {_e}", "Exception")
break
def _link_obj_files(self, active_sha1s: set[str]) -> None:
"""将 active_sha1s 对应的 .obj 文件和 includes 库文件链接为可执行文件"""
obj_files = []
merged_obj = os.path.join(self.output_dir, '_merged.obj')
if os.path.isfile(merged_obj):
obj_files.append(merged_obj)
else:
for root, dirs, files in os.walk(self.output_dir):
for file in files:
if file.endswith('.obj'):
sha1 = file[:-4]
if sha1 in active_sha1s and sha1 not in self._include_sha1s:
obj_files.append(os.path.join(root, file))
if not obj_files and not self.extra_link_files:
_vlog().info("[链接] 无文件可链接")
return
_vlog().info(f"\n[链接] 找到 {len(obj_files)} 个 .obj 文件")
all_link_files = obj_files + self.extra_link_files
seen = set()
all_link_files = [f for f in all_link_files if not (f in seen or seen.add(f))]
if self.extra_link_files:
_vlog().info(f"[链接] 额外库文件: {len(self.extra_link_files)}")
for lib in self.extra_link_files:
_vlog().info(f" - {os.path.basename(lib)}")
if not self.linker_cmd:
_vlog().info("[链接] 未配置链接器")
return
exe_path = os.path.join(self.output_dir, self.linker_output) if self.linker_output else os.path.join(self.output_dir, 'a.exe')
is_binary = self.linker_output and self.linker_output.endswith('.bin')
# 检查是否直接输出二进制(使用 --oformat binary / -oformat binary 标志)
is_direct_binary = is_binary and any('oformat' in f for f in self.linker_flags)
if is_binary and not is_direct_binary:
link_output = exe_path[:-4] + '_pe.exe'
else:
link_output = exe_path
# 自动复制或生成 linker.ld 到输出目录
linker_ld_src = os.path.join(os.path.dirname(self.output_dir), 'linker.ld')
linker_ld_dst = os.path.join(self.output_dir, 'linker.ld')
if os.path.isfile(linker_ld_src) and not os.path.isfile(linker_ld_dst):
shutil.copy2(linker_ld_src, linker_ld_dst)
_vlog().info(f" [链接] 使用链接脚本: linker.ld")
elif not os.path.isfile(linker_ld_dst) and any('-T' in f or '-T' in f.replace(',', ' ') for f in self.linker_flags):
default_ld = """ENTRY(_start)
SECTIONS {
. = 0x100000;
.text : {
*(.text)
*(.text.*)
}
.rodata : {
*(.rodata)
*(.rodata.*)
}
.data : {
*(.data)
*(.data.*)
}
.bss : {
*(.bss)
*(.bss.*)
}
/DISCARD/ : {
*(.comment)
*(.note)
*(.eh_frame)
*(.eh_frame_hdr)
*(.reloc)
*(.rela)
*(.debug*)
}
}
"""
with open(linker_ld_dst, 'w', encoding='utf-8', newline='\n') as f:
f.write(default_ld)
_vlog().info(f" [链接] 自动创建默认链接脚本: linker.ld")
try:
# 将 linker_flags 拆分为非库标志和库标志(-l/-L库标志放在目标文件之后
# 这样 Unix 风格链接器才能正确解析库中的符号
# -Wl, 选项是链接器全局选项,放在目标文件之前
non_lib_flags = []
lib_flags = []
i = 0
while i < len(self.linker_flags):
flag = self.linker_flags[i]
if flag in ('-l', '-L') and i + 1 < len(self.linker_flags):
# -l xxx / -L xxx 分开的形式
lib_flags.extend([flag, self.linker_flags[i + 1]])
i += 2
elif flag.startswith('-l') or flag.startswith('-L'):
# -lxxx / -Lxxx 合并的形式
lib_flags.append(flag)
i += 1
else:
# 其他所有标志(包括 -Wl, 选项)放在目标文件之前
non_lib_flags.append(flag)
i += 1
cmd = [self.linker_cmd] + non_lib_flags + ['-o', link_output] + all_link_files + lib_flags
_vlog().info(f" 执行: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=self.output_dir or '.'
)
if result.returncode == 0:
if is_binary and not is_direct_binary:
objcopy = shutil.which('llvm-objcopy') or shutil.which('objcopy')
if objcopy:
conv = subprocess.run(
[objcopy, '-O', 'binary', link_output, exe_path],
capture_output=True, text=True
)
if conv.returncode == 0:
os.remove(link_output)
_vlog().success(f" 生成裸机二进制: {exe_path}")
file_size = os.path.getsize(exe_path)
_vlog().info(f" [大小] {file_size} 字节")
else:
_vlog().error(f" 二进制转换失败: {conv.stderr.strip()}")
sys.exit(1)
else:
_vlog().error(" 找不到 llvm-objcopy无法生成裸机二进制")
sys.exit(1)
else:
_vlog().success(f" 生成: {exe_path}")
file_size = os.path.getsize(exe_path)
_vlog().info(f" [大小] {file_size} 字节")
if not is_binary:
self._strip_debug_sections(exe_path)
else:
_vlog().error(f" 链接失败: {result.stderr.strip()}")
_vlog().error(f"\n链接失败,立即终止编译。")
sys.exit(1)
except FileNotFoundError:
_vlog().error(f" 找不到链接器: {self.linker_cmd}")
_vlog().error(f"\n找不到链接器,立即终止编译。")
sys.exit(1)
except Exception as e:
_vlog().error(f" {e}")
_vlog().error(f"\n链接异常,立即终止编译。")
sys.exit(1)