diff --git a/lib/Projectrans/Phase2Translator.py b/lib/Projectrans/Phase2Translator.py index 778bdfe..5d45618 100644 --- a/lib/Projectrans/Phase2Translator.py +++ b/lib/Projectrans/Phase2Translator.py @@ -196,7 +196,7 @@ def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_ 防污染:SymbolTable 是编译期唯一被写的共享对象(import insert/类注册), 批内每个文件翻译前从 symbol_table_bytes 反序列化出干净副本,前文件的 符号写入不会残留到后文件(否则跨模块 struct 类型前缀错位)。 - 返回 (错误列表[(src_path, err)], batch)。 + 返回 (错误列表[(src_path, err)], batch, per_file_times[(src_path, elapsed, rel)])。 """ try: trans = Phase2Translator(src_root, temp_dir, output_dir, @@ -235,13 +235,16 @@ def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_ trans._build_shared_symbol_data() batch_errors: list = [] + per_file_times: list[tuple[str, float, str]] = [] for _i, src_path, out_path, _sha1, msm in batch: + _f0: float = time.time() try: # 每文件前重置 SymbolTable 为干净副本(防跨文件污染) if symbol_table_bytes is not None: trans._shared_symbol_table = pickle.loads(symbol_table_bytes) trans._shared_symbol_table.translator = None trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm) + per_file_times.append((src_path, time.time() - _f0, os.path.relpath(src_path, src_root))) except Exception as e: # fail-fast(2026-08-18):任意文件编译出错立即终止整个编译进程, # 不再收集错误继续翻译批内剩余文件(避免浪费大量时间)。主进程 @@ -251,9 +254,9 @@ def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_ f"{traceback.format_exc()}\n") sys.stderr.flush() os._exit(88) - return (batch_errors, batch) + return (batch_errors, batch, per_file_times) except Exception as e: - return ([(item[1], f'{e}\n{traceback.format_exc()}') for item in batch], batch) + return ([(item[1], f'{e}\n{traceback.format_exc()}') for item in batch], batch, []) # ================================================================== @@ -895,13 +898,15 @@ class Phase2Translator: for future in as_completed(futures): bidx, rel_first, bcount = futures[future] try: - batch_errors, batch = future.result() - done_files += len(batch) + batch_errors, batch, per_file_times = future.result() + for src, elapsed, rel in per_file_times: + done_files += 1 + _vlog().info(f" [{done_files}/{total_files}] {rel} ({elapsed:.1f}s)") for src, err in batch_errors: rel = os.path.relpath(src, self.src_root) _vlog().error(f" {rel}: {err}") errors.append((rel, err)) - _vlog().info(f" [{done_files}/{total_files}] 批次完成({bcount} 文件, 首个 {rel_first})") + _vlog().info(f" [批次完成] {bcount} 文件, 首个 {rel_first}") except Exception as e: # fail-fast:worker 已 os._exit(文件编译出错)或进程池 # 已 broken → 立即取消全部剩余批次并终止编译,不空等。 @@ -1376,8 +1381,17 @@ class Phase2Translator: with open(src_path, 'r', encoding='utf-8') as f: code = f.read() - # SHA1 必须在自动导入注入之前计算,以保持与 Phase1 生成的文件名/函数名前缀一致 - sha1 = compute_sha1(code) + # SHA1 以主流程 out_path 文件名为准(= 主流程 compute_sha1(parse_python_file 内容)), + # 必须在自动导入注入之前确定。不可在此重读文件重算:主流程收集(need_translate) + # 与本次实际翻译之间若文件内容变化(如编辑器保存),重算会得到不同 sha1, + # 导致函数定义前缀 ≠ 文件名/调用方引用前缀 → 链接 undefined symbol + # (实例:696d41ca86e0a29a.BuildTopoSortedIndices 定义被生成成 5536… 前缀)。 + sha1 = os.path.splitext(os.path.basename(out_path))[0] + cur_sha1 = compute_sha1(code) + if cur_sha1 != sha1: + _vlog().warning( + f"模块内容在收集后已变化(文件名sha1={sha1}, 当前sha1={cur_sha1}): {src_path}," + f"仍按主流程 sha1 生成前缀以保证链接一致;下次构建将采用新 sha1", "SHA1") # 自动导入: 检测 list 容器使用,自动注入 import _list code = self._inject_auto_imports(code, src_path) @@ -1594,7 +1608,7 @@ class Phase2Translator: f.write(text_content) self._save_deps(sha1, ModuleSha1Map) - _vlog().info(f" -> {os.path.basename(out_path)} {os.path.relpath(src_path, self.src_root) if src_path else ''} ({time.time() - _t0:.1f}s)") + # 进度日志由调用方统一打印(主进程按文件粒度 [x/total]),此处不再重复输出。 else: stub_path = self.stub_files.get(sha1) if stub_path and os.path.exists(stub_path): @@ -1605,7 +1619,7 @@ class Phase2Translator: 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)} {os.path.relpath(src_path, self.src_root) if src_path else ''} (仅声明, {time.time() - _t0:.1f}s)") + # 进度日志由调用方统一打印。 def _register_package_reexports(self, trans, init_pyi_path: str, package_name: str) -> None: """解析 __init__.pyi 中的 from .xxx import yyy 或 from __xxx import yyy,在包级别注册导出符号"""