agent / backend /build_pyc.py
Toadied's picture
2312
8b383ad verified
import os
import shutil
import sys
import marshal
from pathlib import Path
# 自动获取当前Python的magic number(推荐,避免手动配置错误)
try:
import importlib._bootstrap_external as bootstrap
MAGIC_NUMBER = bootstrap.MAGIC_NUMBER
except ImportError:
# 手动指定Python 3.13的magic number(备用)
MAGIC_NUMBER = b'\x40\x3e\x00\x00'
# ===================== 配置项(仅需调整这两个)=====================
SOURCE_DIR = Path("./app") # 你的源码根目录:E:\ai\llama3.2-projec\src
OUTPUT_DIR = Path("./dist") # .pyc产物输出目录
# ==================================================================
def clean_old_build():
"""清空旧的dist目录,避免残留文件"""
if OUTPUT_DIR.exists():
shutil.rmtree(OUTPUT_DIR, ignore_errors=True) # 忽略Windows文件占用错误
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print(f"✅ 已清空并创建输出目录:{OUTPUT_DIR.absolute()}")
def compile_single_py(src_file: Path, dst_pyc: Path):
"""
编译单个.py文件为.pyc文件,直接写入目标路径
:param src_file: 源.py文件的路径
:param dst_pyc: 目标.pyc文件的路径
"""
# 1. 读取源码文件(二进制模式,避免编码问题)
try:
with open(src_file, "rb") as f:
source_content = f.read()
except Exception as e:
raise RuntimeError(f"读取源码失败:{src_file}{str(e)}") from e
# 2. 编译源码为Python字节码对象
try:
code_obj = compile(
source_content,
filename=str(src_file), # 报错时显示的文件名
mode="exec", # 编译整个模块(exec模式)
dont_inherit=True, # 不继承当前环境的编译标志
optimize=0 # 优化级别(0/1/2,0为默认)
)
except SyntaxError as e:
raise RuntimeError(f"源码语法错误:{src_file} → 行{e.lineno}{e.msg}") from e
except Exception as e:
raise RuntimeError(f"编译源码失败:{src_file}{str(e)}") from e
# 3. 写入.pyc文件(遵循Python的.pyc文件格式)
try:
with open(dst_pyc, "wb") as f:
f.write(MAGIC_NUMBER) # 写入版本标识(magic number)
f.write(b'\x00\x00\x00\x00') # 写入时间戳(0表示不验证时间)
marshal.dump(code_obj, f) # 写入字节码对象(序列化)
except Exception as e:
raise RuntimeError(f"写入.pyc失败:{dst_pyc}{str(e)}") from e
def compile_all_py_to_dist():
"""遍历所有.py文件,编译为.pyc并写入dist目录(保留原目录结构)"""
print("📝 开始编译所有.py文件为.pyc...")
compiled_count = 0
skipped_count = 0
# 递归遍历源码目录下的所有.py文件
for src_file in SOURCE_DIR.rglob("*.py"):
# 跳过无关文件:脚本自身、__pycache__目录下的文件、隐藏文件
if (src_file.name == "build_pyc.py" or
"__pycache__" in src_file.parts or
src_file.name.startswith(".")):
skipped_count += 1
continue
# 计算目标.pyc文件的路径(保留原目录结构,替换后缀为.pyc)
rel_path = src_file.relative_to(SOURCE_DIR)
dst_pyc = OUTPUT_DIR / rel_path.with_suffix(".pyc")
# 创建目标目录(如果不存在)
dst_pyc.parent.mkdir(parents=True, exist_ok=True)
# 编译并写入
try:
compile_single_py(src_file, dst_pyc)
compiled_count += 1
# 可选:打印编译日志(如需精简,注释掉下面这行)
# print(f" ✅ {rel_path} → {dst_pyc.relative_to(OUTPUT_DIR)}")
except Exception as e:
print(f"⚠️ 跳过文件:{src_file}{str(e)}")
skipped_count += 1
print(f"✅ 编译完成:成功{compiled_count}个,跳过{skipped_count}个")
def verify_compile_result():
"""验证编译结果,显示dist目录下的.pyc文件数量"""
pyc_files = list(OUTPUT_DIR.rglob("*.pyc"))
print(f"\n📌 编译结果验证:")
print(f" - 输出目录:{OUTPUT_DIR.absolute()}")
print(f" - 生成.pyc文件总数:{len(pyc_files)}")
# 显示前5个.pyc文件的路径(便于确认结构)
for i, file in enumerate(pyc_files[:5]):
print(f" - {file.relative_to(OUTPUT_DIR)}")
if len(pyc_files) > 5:
print(f" - ... 还有{len(pyc_files)-5}个文件")
if __name__ == "__main__":
# 打印环境信息
print(f"===== 开始编译Python源码为.pyc =====\n🔍 Python版本:{sys.version.split()[0]}\n🔍 Magic Number:{MAGIC_NUMBER.hex()}{MAGIC_NUMBER})\n🔍 源码目录:{SOURCE_DIR.absolute()}")
# 执行核心流程
clean_old_build()
compile_all_py_to_dist()
verify_compile_result()
print(f"\n===== 全部操作完成!=====")