Files
TransPyC/lib/Projectrans/Config.py
T
TermiNexus cc2f5d34eb feat(tpc): includes 只认项目配置;ASan 改为显式链接 clang64 运行库
includes 目录不再回退仓库硬编码 TransPyCRoot/includes:Phase2Translator 新增 PROJECT_INCLUDE_DIRS(INCLUDE_DIRS 置空以兼容旧引用),Config.py 从 project.vpj 的 includes 同步;HandlesImports 的模块搜索、TypeAnnotationResolver._builtins_include_dirs 一律只读项目目录。

ASan:mingw64 clang 没有 x86_64-w64-windows-gnu 目标的 ASan runtime,新增 _FindAsanRuntimeDir 定位 clang64 运行库并按 -l: 显式链接(asan_dynamic-x86_64.dll.a + runtime_thunk),定位失败即报错退出。HandlesExprFormat:i32 不再写 'l' 长度修饰符(LP64 下 printf 会按 64 位读实参)。
2026-09-11 22:47:21 +08:00

332 lines
16 KiB
Python
Raw 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 os
import sys
import json
import glob as glob_module
import argparse
import shutil
import subprocess
from typing import Any
from lib.core.VLogger import get_logger as _vlog
from lib.Projectrans.Phase1Generator import Phase1Generator
from lib.Projectrans.Phase2Translator import Phase2Translator
# ============================================================
# 全局 SHA1 映射内存存储器
#
# Phase1 生成 sha1_map 后写入此处(save_sha1_map),
# Phase2 从此处读取(Load_sha1_map / Phase2Translator._LoadSha1Map)。
# _sha1_map.txt 文件仅作为人类可读输出,不再作为机器分析数据源。
# ============================================================
_g_sha1_map_store: dict[str, str] = {}
def get_sha1_map_store() -> dict[str, str]:
"""返回全局 SHA1 映射内存存储器(供 Phase2Translator 直接访问)"""
return _g_sha1_map_store
def clear_sha1_map_store() -> None:
"""清空全局 SHA1 映射内存存储器"""
_g_sha1_map_store.clear()
def save_sha1_map(temp_dir: str, sha1_map: dict[str, str]) -> None:
"""将 SHA1 映射表保存到内存存储器,并输出人类可读的 _sha1_map.txt
机器分析使用内存存储器(_g_sha1_map_store),
_sha1_map.txt 仅作为人类可读输出,不再被机器读取。
"""
# 写入内存存储器(机器分析数据源)
_g_sha1_map_store.clear()
for sha1, rel in sha1_map.items():
_g_sha1_map_store[sha1] = rel
# 写入文件(仅人类可读输出)
map_path: str = os.path.join(temp_dir, '_sha1_map.txt')
with open(map_path, 'w', encoding='utf-8') as f:
for sha1, rel in sorted(sha1_map.items()):
f.write(f"{sha1}:{rel}\n")
def Load_sha1_map(temp_dir: str) -> dict[str, str]:
"""从内存存储器加载 SHA1 映射表
temp_dir 参数保留用于签名兼容性,但不再读取文件。
机器分析使用内存存储器(_g_sha1_map_store)。
"""
result: dict[str, str] = {}
for sha1, rel in _g_sha1_map_store.items():
result[sha1] = rel
return result
def Load_project_config(project_file: str) -> dict[str, Any]:
"""从 project.json 加载配置"""
with open(project_file, 'r', encoding='utf-8') as f:
return json.load(f)
def resolve_paths(config: dict[str, Any], project_file: str) -> dict[str, Any]:
"""将配置中的相对路径转换为绝对路径"""
project_dir: str = os.path.dirname(os.path.abspath(project_file))
result: dict[str, Any] = dict(config)
def resolve(p: str | None) -> str | None:
if p is None:
return None
if os.path.isabs(p):
return p
return os.path.normpath(os.path.join(project_dir, p))
if 'source_dir' in result:
result['source_dir'] = resolve(result.get('source_dir'))
elif 'sources' in result:
resolved_sources: list[str] = []
for src in result['sources']:
if '*' in src or '?' in src:
for matched in glob_module.glob(os.path.join(project_dir, src), recursive=True):
if os.path.isfile(matched):
resolved_sources.append(matched)
else:
resolved_sources.append(resolve(src))
result['sources'] = [s for s in resolved_sources if os.path.isfile(s)]
# 直接使用 output_dir / temp_dir(与新版本 TPV 一致,不再支持 build_dir)
result['temp_dir'] = resolve(config.get('temp_dir', './temp'))
result['output_dir'] = resolve(config.get('output_dir', './output'))
if 'includes' in result:
result['includes'] = [resolve(inc) for inc in result['includes']]
result['_project_dir'] = project_dir
return result
def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description='TransPyC 工程翻译器 - 基于 project.json 的两阶段翻译',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 从 project.json 读取配置并执行(默认查找当前目录)
python ProjectTrans.py
# 指定 project.json 文件
python ProjectTrans.py --project ./TestProject/project.json
# 使用命令行参数覆盖 project.json 配置
python ProjectTrans.py --src ./src --temp ./decl --output ./out --phase all
# 仅运行阶段一
python ProjectTrans.py --phase 1
# 仅运行阶段二
python ProjectTrans.py --phase 2
# 清理临时目录
python ProjectTrans.py --clean
# 编译并立即执行
python Projectrans.py --project ./TestProject/project.json --clean --run
# 删除 includes.binary 预编译缓存并重新编译所有 includes
python Projectrans.py --project ./TestProject/project.json --rebuild-includes
'''
)
parser.add_argument('--project', help='project.json 路径(默认查找当前目录)')
parser.add_argument('--src', help='源文件目录(覆盖 project.json)')
parser.add_argument('--temp', help='声明接口临时目录(覆盖 project.json)')
parser.add_argument('--output', help='输出目录(覆盖 project.json)')
parser.add_argument('--phase', choices=['1', '2', 'all'], help='阶段: 1=生成声明, 2=翻译+编译, all=全部')
parser.add_argument('--cc', help='LLVM 编译器命令(覆盖 project.json)')
parser.add_argument('--clean', action='store_true', help='清理 output 和 temp 目录')
parser.add_argument('--run', action='store_true', help='编译成功后立即执行生成的可执行文件')
parser.add_argument('--rebuild-includes', action='store_true', help='删除 includes.binary 预编译缓存并重新编译所有 includes')
parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.vpi/.stub.ll/.doc.json)')
parser.add_argument('--asan', action='store_true', help='ASan 模式:对全部 .ll 做 opt -passes=asan 插桩,并显式链接 clang64 的 ASan 运行库(mingw64 的 clang 没有 x86_64-w64-windows-gnu 目标的 ASan runtime,不能用 -fsanitize=address)')
parser.add_argument('--nowarn-register', action='store_true', help='抑制"外部函数返回类型未登记"类 WARN 输出(透传给自举编译的 TransPyV.exe)')
args: argparse.Namespace = parser.parse_args()
# ASan 模式(--asan):直接经 args 传给 Phase2Translator(compile_ll_to_obj
# 先 opt -passes=asan 插桩,链接显式 -l: clang64 的 libclang_rt.asan_dynamic*)。
# 不再用 TPC_ASAN 环境变量透传——环境变量在 worker 子进程/外部调用链下不可靠。
if args.asan:
_vlog().info('ASan 模式已启用(opt -passes=asan + clang64 libclang_rt.asan_dynamic*)', 'asan')
project_file: str | None = args.project
if not project_file:
candidates: list[str] = ['project.json', './project.json']
for c in candidates:
if os.path.exists(c):
project_file = c
break
if project_file and os.path.exists(project_file):
_vlog().info(f"读取配置文件: {project_file}")
config: dict[str, Any] = Load_project_config(project_file)
project_dir: str = os.path.dirname(os.path.abspath(project_file))
_vlog().info(f"项目目录: {project_dir}")
else:
_vlog().error("未找到 project.json,请使用 --project 指定")
sys.exit(1)
resolved: dict[str, Any] = resolve_paths(config, project_file)
src: str | list[str] | None = args.src or resolved.get('source_dir') or resolved.get('sources')
temp_dir: str | None = args.temp or resolved.get('temp_dir')
output_dir: str | None = args.output or resolved.get('output_dir')
phase: str = args.phase or 'all'
cc_cmd: str = args.cc or resolved.get('compiler', {}).get('cmd', 'llc')
cc_flags: list[str] = resolved.get('compiler', {}).get('flags', ['-filetype=obj'])
slice_level: int = resolved.get('options', {}).get('slice_level', 3)
project_include_dirs: list[str] = resolved.get('includes', [])
# includes 只认项目配置:同步到 Phase2Translator.PROJECT_INCLUDE_DIRS,
# 供 builtins.vp 扫描等静态路径在 phase1/2 全程使用(禁止仓库硬编码回退)
try:
from lib.Projectrans.Phase2Translator import Phase2Translator
Phase2Translator.PROJECT_INCLUDE_DIRS = [os.path.abspath(d) for d in project_include_dirs if os.path.isdir(os.path.join(project_dir, d)) or os.path.isdir(d)]
except Exception:
pass
target_triple: str | None = resolved.get('target', {}).get('triple')
target_datalayout: str | None = resolved.get('target', {}).get('datalayout')
if isinstance(src, list):
if len(src) == 1 and os.path.isdir(src[0]):
src = src[0]
else:
src = src[0] if len(src) == 1 else os.path.dirname(os.path.commonpath(src)) if all(os.path.exists(p) for p in src) else src[0]
_vlog().info(f"源目录: {src}")
_vlog().info(f"临时目录: {temp_dir}")
_vlog().info(f"输出目录: {output_dir}")
_vlog().info(f"阶段: {phase}")
_vlog().info(f"编译器: {cc_cmd} {' '.join(cc_flags)}")
if target_triple:
_vlog().info(f"目标三元组: {target_triple}")
if target_datalayout:
_vlog().info(f"数据布局: {target_datalayout}")
# 每次编译自动清理子进程性能剖析输出(PROF 目录 *.prof,硬编码路径)
from lib.Projectrans.Phase2Translator import _CleanProfFiles, PROF_DIR
_CleanProfFiles()
_vlog().info(f"PROF 目录已清理: {PROF_DIR}")
if args.clean:
if output_dir and os.path.exists(output_dir):
for f in os.listdir(output_dir):
fpath: str = os.path.join(output_dir, f)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
elif os.path.isdir(fpath):
shutil.rmtree(fpath, ignore_errors=True)
except:
pass
_vlog().info(f"output: 已清空 {output_dir}")
if temp_dir and os.path.exists(temp_dir):
for f in os.listdir(temp_dir):
fpath: str = os.path.join(temp_dir, f)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
elif os.path.isdir(fpath):
shutil.rmtree(fpath, ignore_errors=True)
except:
pass
_vlog().info(f"temp: 已清空 {temp_dir}")
_vlog().info("清理完成")
if (not phase or phase == 'all') and not args.project:
return
if args.clear_cache:
# 清除 .transpyc_cache 全局缓存
cache_dir: str = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), '.transpyc_cache')
if os.path.isdir(cache_dir):
shutil.rmtree(cache_dir, ignore_errors=True)
os.makedirs(cache_dir, exist_ok=True)
_vlog().info(f"已清除全局缓存: {cache_dir}")
else:
_vlog().info(f"全局缓存目录不存在: {cache_dir}")
if phase in ('1', 'all'):
if isinstance(src, list):
_vlog().error("阶段一需要指定源目录,不支持文件列表")
sys.exit(1)
all_include_dirs: list[str] = list(project_include_dirs) if project_include_dirs else []
gen: Phase1Generator = Phase1Generator(src, temp_dir, include_dirs=all_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout)
gen.run()
save_sha1_map(temp_dir, gen.sha1_map)
if phase in ('2', 'all'):
if not output_dir:
_vlog().error("阶段二需要指定 --output 目录")
sys.exit(1)
if not os.path.exists(temp_dir):
_vlog().error(f"声明目录不存在,请先运行阶段一: {temp_dir}")
sys.exit(1)
if isinstance(src, list):
_vlog().error("阶段二需要指定源目录,不支持文件列表")
sys.exit(1)
linker_cmd: str | None = resolved.get('linker', {}).get('cmd')
linker_flags: list[str] = resolved.get('linker', {}).get('flags', [])
linker_output: str | None = resolved.get('linker', {}).get('output')
# 显式静态链接文件列表(.a/.obj 等),相对 project.vpj 目录解析为绝对路径
static_libs: list[str] = []
for lib in resolved.get('linker', {}).get('static_libs', []) or []:
static_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
# 显式动态链接文件列表(.dll/.so 等),相对 project.vpj 目录解析为绝对路径;
# 链接时作为输入文件(GNU ld 从 DLL/SO 提取导入符号),运行时需 DLL/SO 在搜索路径
dynamic_libs: list[str] = []
for lib in resolved.get('linker', {}).get('dynamic_libs', []) or []:
dynamic_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
target_triple = resolved.get('target', {}).get('triple')
target_datalayout = resolved.get('target', {}).get('datalayout')
force_recompile_includes: bool = bool(resolved.get('options', {}).get('force_recompile_includes', False))
if args.rebuild_includes:
force_recompile_includes = True
all_inc_dirs: list[str] = list(project_include_dirs)
seen_dirs: set[str] = set()
for inc_dir in all_inc_dirs:
abs_inc: str = os.path.abspath(inc_dir)
if abs_inc in seen_dirs:
continue
seen_dirs.add(abs_inc)
if not os.path.isdir(abs_inc):
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')
if os.path.isdir(precompiled_dir):
cleared: int = 0
for fname in os.listdir(precompiled_dir):
fpath: str = os.path.join(precompiled_dir, fname)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
cleared += 1
except:
pass
_vlog().info(f"includes.binary: 已清空 {precompiled_dir} ({cleared} 个文件)")
trans: Phase2Translator = Phase2Translator(src, temp_dir, output_dir, compile_cmd=cc_cmd, compile_flags=cc_flags, linker_cmd=linker_cmd, linker_flags=linker_flags, linker_output=linker_output, static_libs=static_libs, dynamic_libs=dynamic_libs, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes, asan_mode=args.asan)
trans.sha1_map = Load_sha1_map(temp_dir)
trans.slice_level = slice_level
trans.run()
if args.run and linker_output and not linker_output.endswith('.bin'):
exe_path: str = os.path.join(output_dir, linker_output)
if os.path.isfile(exe_path):
_vlog().info(f"\n[运行] 执行: {exe_path}")
try:
result: subprocess.CompletedProcess = subprocess.run([exe_path], cwd=output_dir)
_vlog().info(f"[运行] 退出码: {result.returncode}")
except Exception as e:
_vlog().error(f"[运行] 执行失败: {e}")
sys.exit(1)
else:
_vlog().error(f"[运行] 找不到可执行文件: {exe_path}")
sys.exit(1)