Files
2026-09-22 16:08:12 +08:00

127 lines
4.8 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.
"""用 PyInstaller 打包成单文件 exe。
用法::
uv run python packaging/build_pyinstaller.py # 单文件(默认)
uv run python packaging/build_pyinstaller.py --onedir # 目录模式,启动更快
uv run python packaging/build_pyinstaller.py --console # 带控制台,排查启动问题
uv run python packaging/build_pyinstaller.py --no-selftest # 跳过构建后自检
产物:``dist/pyinstaller/``
构建前会先做两件事(任一失败即中止):
1. **私钥守卫**:源码树里若存在私钥 / ``*.lic`` / ``licenses.db`` 就停下 ——
这些东西绝不能进分发包;
2. **生成 exe 图标**:由主题里的盾牌 SVG 渲染多尺寸 ``.ico``,无需美术素材。
构建后自动跑 ``<exe> --selftest`` 两轮(offscreen + 系统原生平台插件),
验证冻结产物里的 Qt 插件、图标渲染、Ed25519 签发与验签链路都还在。
"""
from __future__ import annotations
import argparse
import os
import shutil
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _common as common # noqa: E402
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="用 PyInstaller 打包 chongming 授权工具")
mode = parser.add_mutually_exclusive_group()
mode.add_argument("--onefile", action="store_true", default=True,
help="打成单个 exe(默认;分发最省事)")
mode.add_argument("--onedir", action="store_true",
help="打成目录(启动更快,适合固定办公机)")
parser.add_argument("--console", action="store_true",
help="保留控制台窗口(默认无控制台;排查启动失败时有用)")
parser.add_argument("--clean", action="store_true", help="构建前清掉旧的 dist/build")
parser.add_argument("--no-selftest", action="store_true", help="跳过构建后的产物自检")
parser.add_argument("--allow-sensitive-files", action="store_true",
help="源码树里有私钥/.lic/台账库时仍继续构建(默认中止)")
return parser.parse_args()
def main() -> int:
args = parse_args()
common.require("PyInstaller", "pyinstaller")
onefile = not args.onedir
dist_dir = common.DIST / "pyinstaller"
build_dir = common.PACKAGING / "build" / "pyinstaller"
print("=" * 60)
print(f"PyInstaller 打包({'单文件' if onefile else '目录模式'})")
print("=" * 60)
# 1) 安全闸门:私钥/授权文件/台账库一个都不能出现在产物链路里
common.guard_no_secrets(allow=args.allow_sensitive_files)
# 2) 图标
icon = common.build_app_icon()
if args.clean:
for path in (dist_dir, build_dir):
if path.exists():
shutil.rmtree(path)
print(f"已清理 {path.relative_to(common.ROOT)}")
# 3) 调用 PyInstaller(选项通过环境变量传给 spec)
env = dict(os.environ)
env.update({
"CLG_NAME": common.APP_NAME,
"CLG_ONEFILE": "1" if onefile else "0",
"CLG_CONSOLE": "1" if args.console else "0",
"CLG_ICON": str(icon) if icon else "",
})
# PATH 里若有"与 Python 同名的 DLL",PyInstaller 会误收集它们(见 _common 的说明)
env.update(common.build_env_overrides(env))
command = [
sys.executable, "-m", "PyInstaller",
"--noconfirm",
"--distpath", str(dist_dir),
"--workpath", str(build_dir),
str(common.PACKAGING / "pyinstaller.spec"),
]
log_path = common.PACKAGING / "build" / "pyinstaller-build.log"
print("\n$ " + " ".join(command))
print(f"构建日志:{log_path.relative_to(common.ROOT)}\n")
started = time.monotonic()
code, log_text = common.run_build(command, log_path, env)
elapsed = time.monotonic() - started
if code != 0:
print(f"❌ PyInstaller 构建失败(退出码 {code})\n")
print(common.log_tail(log_text, 30))
print(f"\n完整日志:{log_path.relative_to(common.ROOT)}")
return code
# 4) 找到产物并验证
artifact = common.find_artifact(dist_dir)
if artifact is None:
print(f"\n❌ 构建结束但没在 {dist_dir} 里找到 exe")
return 1
stray = common.guard_artifact_no_secrets(artifact.parent)
if stray:
print(f"\n❌ 产物目录里出现敏感文件,请检查后再分发:{stray}")
return 1
if not args.no_selftest:
if not common.verify_artifact(artifact, "PyInstaller 产物"):
print("\n❌ 产物自检未通过 —— 先别分发,见上方输出")
return 1
common.report_success("PyInstaller", artifact, elapsed)
return 0
if __name__ == "__main__":
sys.exit(main())