Files
TransPyC/lib/Projectrans/Config.py
2026-07-18 19:25:40 +08:00

281 lines
12 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
def save_sha1_map(temp_dir: str, sha1_map: dict[str, str]) -> None:
"""将 SHA1 映射表保存到文件"""
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 映射表"""
map_path: str = os.path.join(temp_dir, '_sha1_map.txt')
result: dict[str, str] = {}
if os.path.exists(map_path):
with open(map_path, 'r', encoding='utf-8') as f:
for line in f:
line: str = line.strip()
if ':' in line:
sha1: str
rel: str
sha1, rel = line.split(':', 1)
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)]
result['temp_dir'] = resolve(config.get('temp_dir'))
result['output_dir'] = resolve(config.get('output_dir'))
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 全局缓存(.pyi/.stub.ll/.doc.json')
args: argparse.Namespace = parser.parse_args()
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', [])
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}")
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 []
for d in Phase2Translator.INCLUDE_DIRS:
if os.path.isdir(d):
d = os.path.abspath(d)
if d not in [os.path.abspath(x) for x in all_include_dirs]:
all_include_dirs.append(d)
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')
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) + list(Phase2Translator.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, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes)
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)