127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
"""用 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())
|