# 打包分发 把 GUI 打成**免安装 exe**,发给厂商内部同事(商务/售后)。两种方式各一个脚本, 都自带"私钥守卫 + 产物自检"。 ```bash uv sync --group dev # 装打包工具(pyinstaller / nuitka) uv run python packaging/build_pyinstaller.py # 方式一:PyInstaller(快,默认单文件) uv run python packaging/build_nuitka.py # 方式二:Nuitka(真编译,慢但更小更快) # 产物 dist/pyinstaller/chongming-license-gui.exe dist/nuitka/chongming-license-gui.exe ``` ## 该选哪个 下表为**本机实测值**(Windows 11 / Python 3.14 / Qt 6.11.2 / MSVC 14.5,单文件模式): | | PyInstaller | Nuitka | | --- | --- | --- | | 构建耗时 | **约 2.5 分钟**(实测 149s) | **约 10 分钟**(首次实测 1140s,含一次编译器重试) | | 构建前提 | 无 | **需要 C 编译器**(MSVC 或 Nuitka 支持的 mingw64) | | 单文件体积 | 54.8 MB | **38.6 MB** | | 启动 + 跑完自检 | 2.7s | **1.8s** | | 反编译难度 | 打包的 .pyc 可被还原 | **编译成 C,难得多** | | 稳定性 | 生态成熟、踩坑资料多 | 需要针对 Python/Qt 版本调参(见下) | **建议**:日常发给同事用 **Nuitka 单文件**(更小、启动更快、更难反编译); 需要快速出包/临时验证用 **PyInstaller**(两分半就好,且不需要编译器)。 > ⚠️ **要 PostgreSQL 支持就用 PyInstaller。** Nuitka 在 Python 3.13+ 上只能用 MSVC > (`--mingw64` 被 Nuitka 明确拒绝),而 MSVC 编译 psycopg 的生成代码会 > `fatal error C1002`(编译器堆空间不足)—— 这是编译器上限,不是代码问题。 > Nuitka 脚本检测到这一点会**自动降级**为"不带 PG"的构建并告诉你; > 也可以显式 `--no-postgres`。不带 PG 的包里,界面会如实提示 > 「未安装 PostgreSQL 驱动(可选功能)」,本地 SQLite 台账与签发校验全部照常。 ## 常用参数 两个脚本参数一致: | 参数 | 作用 | | --- | --- | | `--onedir` / `--standalone` | 打成目录而不是单文件。**启动更快**,适合放在固定办公机上;分发时整个目录一起拷 | | `--console` | 保留控制台窗口。默认是**无控制台**(纯 GUI);排查"双击没反应"时用得上 | | `--clean` | 构建前清理旧的 `dist/`(Nuitka 还会顺手删中间产物) | | `--lto=no`(仅 Nuitka) | 关掉链接期优化 —— **显著加快构建**(默认开着,链接阶段实测要 6 分钟以上) | | `--no-low-memory`(仅 Nuitka) | 关掉低内存模式。默认**开启**,见下方 C1002 说明 | | `--no-selftest` | 跳过构建后的产物自检(不建议) | | `--allow-sensitive-files` | 源码树里有私钥/`.lic`/台账库时仍继续构建(默认**中止**,见下) | | `--msvc` / `--mingw64`(仅 Nuitka) | 指定编译器;默认自动挑选 | ## 两道安全闸门(重要) **第一道:构建前扫源码树。** 命中 `license_private*.pem`、`*_private.pem`、 `*_private.key`、`*.lic`、`licenses.db` 一律**中止构建**并列出文件: ``` 构建中止:源码树里存在私钥/授权文件/台账库,它们绝不能进入分发包。 - license_L20260921-A36014.lic ``` 原因很直接:这个工具**含签发能力**。私钥一旦随包发出去,拿到的人就能自己签发有效授权; `.lic` 与台账库则含客户名与机器指纹。确实只是临时放置时,可加 `--allow-sensitive-files` 跳过(会打印警告并列出文件)。 **第二道:构建后再查产物目录**,避免 onedir 模式把工作目录里的散落文件一起拷进去。 ## 构建后自检(为什么必须有) 冻结失败往往是**静默**的 —— 界面能开,但图标全空白、复选框点不动,或者签发时才抛异常。 所以构建脚本会自动调用 `` --selftest`` 跑一遍真依赖,而且跑**两轮**: | 轮次 | `QT_QPA_PLATFORM` | 验的是什么 | | --- | --- | --- | | 1 | `offscreen` | 无头环境也能验;CI 里可跑 | | 2 | 系统原生(Windows 上是 `windows`) | **Qt 的 platforms 插件真的打进产物了** —— 少了它,开发机上一切正常,同事双击闪退 | 自检覆盖的正是打包最容易漏的东西: - **QtSvg 模块**:少了它所有图标变空白(`QIcon` 是空的,按钮上什么都不画); - **svg 图片格式插件**:少了它样式表里的复选框/单选框指示器加载失败 → 勾选框变空白; - **cryptography 的 Ed25519 后端**:签发与验签全链路(含"异机指纹被拒绝"); - **orjson**(编译扩展):申请码编解码往返; - **sqlite3 + 台账**:入库/检索/统计; - **PostgreSQL 驱动(可选功能)**:构建输出里会打印一行 `PostgreSQL 驱动:可用/不可用`; 另有一个显式的 `CHONGMING_LICENSE_PG` 开关,给了才会**真连一次**库并跑一遍台账 适配层(在事务里,跑完回滚,不留痕迹); - **tomllib + 路径处理**:插件自动发现; - **主窗口装配**:四个页签、表格 9 列、结论横幅、整窗渲染出非空位图。 自检**不碰真实数据**:QSettings 被重定向到临时 INI、台账库指向临时文件 (否则 `RegistryTab` 初始化会去动你真实的 `~/.chongming/licenses.db`)。 它也不会弹窗 —— 全程不调用 `show()`,只在内存里渲染。 手动跑: ```bash dist/pyinstaller/chongming-license-gui.exe --selftest --selftest-report 报告.txt CHONGMING_LICENSE_PG="postgresql://user:pw@host:5432/db" \ dist/pyinstaller/chongming-license-gui.exe --selftest # 额外验一次真库 ``` ## 踩过的坑:PATH 上的同名 DLL 会污染产物 **症状**:exe 能启动、界面正常,但 PostgreSQL 功能报 `ImportError: no pq wrapper available`,往深看是 `DLL load failed while importing _ssl: 找不到指定的程序`。 **原因**:Windows 上 `libssl-3-x64.dll` / `libcrypto-3-x64.dll` 这类**通用名 DLL** 到处都是 —— Git for Windows、msys64、各种 SDK、厂商工具目录。它们都在 PATH 上。 PyInstaller 解析二进制依赖时会搜 PATH,**搜到哪份就用哪份**,于是把 `_ssl.pyd` 需要的 OpenSSL 换成了别的构建: | 来源 | 大小 | | --- | --- | | Python 自带(正确) | 1,579,008 字节 | | **Git for Windows**(被误收集) | **930,460 字节** | | msys64 | 1,055,094 字节 | 版本对不上,`_ssl.pyd` 绑定符号失败 → 依赖 `_ssl` 的 psycopg 二进制实现整块不可用。 **修法**:`_common.build_env_overrides()` 在构建前清一遍 PATH —— 凡是**含有与 Python 自带 DLL 同名文件**的目录一律临时摘掉(系统目录永远保留), 并打印摘掉了哪些,两个构建脚本都会用。本机实测一次摘掉了 Git、msys64、 `E:\polyos_qemu`、ShadowBot,以及 PySide6 自己的目录。 > 这类问题的教训:**报错点离原因很远**(psycopg 说"没有 pq 实现", > 实际是 OpenSSL 版本不对)。所以构建后自检必须验到"驱动能真的连上库", > 光验"模块能 import"是不够的。 ## 实现要点 ``` packaging/ ├── launcher.py # 冻结入口(用绝对导入,不用 __main__.py 的相对导入) ├── pyinstaller.spec # PyInstaller 配置(选项经环境变量传入) ├── build_pyinstaller.py # 驱动 + 守卫 + 自检 ├── build_nuitka.py # 驱动 + 守卫 + 自检 └── _common.py # 图标生成 / 私钥扫描 / PATH 清理 / 自检驱动(两个脚本共用) ``` - **入口用 `launcher.py` 而不是 `__main__.py`**:后者是 `from .app import main` 这样的 包内相对导入,被冻结工具当普通脚本编译时会因失去包上下文而失败。 - **用源码树而不是 editable 安装**:spec 里 `pathex=src/`、Nuitka 里设 `PYTHONPATH=src/`。 冻结工具对 `.pth` 式 editable 安装的解析并不可靠。 - **图标是构建时生成的**:`_common.build_app_icon()` 把主题里的盾牌 SVG 渲染成 7 个尺寸的 `.ico`(自己拼 ICO 容器 —— Qt 的 ICO 写入只支持单张图,小尺寸会糊), 写完立刻回读校验。所以仓库里不需要存任何美术素材。 - **关掉 UPX**:UPX 压缩容易触发杀软误报,也可能损坏 Qt DLL。 - **裁掉用不到的 Qt 模块**:PySide6 装了整套 Addons(Qt3D/WebEngine/Charts…), spec 里显式排除了一批;Nuitka 侧由 `pyside6` 插件负责,另有 `--nofollow-import-to`。 - **构建前清理 PATH**(见上一节):挡住同名 DLL 污染产物。 - **Optional 依赖**:PostgreSQL 支持是 `[project.optional-dependencies] postgres`, 但 dev 依赖组里也列了 psycopg —— 这样 `uv sync --group dev` 出来的环境打出来的包 **自带 PG 支持**;不做 PG 的用户 `uv sync` 也不会被强迫装驱动。 ## 分发提醒 - 本工具是**厂商侧工具**,含签发能力,只发厂商内部同事;**不要给客户**,也不要放进部署产物。 - 打包产物里的界面截图、示例数据都来自 `tools/screenshot.py` 的演示数据,不含真实客户信息。 - exe **未做代码签名**,同事首次运行可能遇到 SmartScreen 提示("更多信息 → 仍要运行")。 要么让 IT 走内部白名单,要么买代码签名证书后给 exe 签名。 ## 常见问题 **双击没反应 / 闪退?** 用 `--console` 重新打一个,命令行里运行看报错。多数是缺平台插件(自检能提前发现)。 **杀软报毒?** PyInstaller 单文件的自解压行为常被启发式判成可疑。改用 `--onedir`,或换 Nuitka。 **Nuitka 报找不到 C 编译器?** 装 Visual Studio 的「使用 C++ 的桌面开发」工作负载;或用 `--mingw64` (需 Nuitka 支持的 mingw64 版本 —— msys2 自带的那个通常不被接受)。 **Nuitka 报 `fatal error C1002` / `LNK1257`(编译器堆空间不足)?** Nuitka 为每个模块生成一个上万行的 C 文件,MSVC 编译最大的那个时会耗尽编译器内部 堆空间。本项目的踩坑记录: - 不带 `--low-memory` 时崩在 `chongming_license_gui.registry_tab`; - 加上 `--low-memory` 后能过,但**引入 psycopg 后又会崩在 `psycopg.errors`** (该模块定义了几百个异常类,生成 C 文件 2.8 万行,MSVC 单文件上限)。 这是 MSVC 的编译上限,不是代码问题。对策: 1. **优先用 PyInstaller 出带 PostgreSQL 支持的包** —— 它不编译 C,没有这个限制, 本项目已验证可用; 2. 或者给 Nuitka 换编译器:`--mingw64`(Nuitka 会自己下载它支持的 WinLibs mingw64,msys2 自带的那份不被接受); 3. 只在不需要 PG 时才用默认的 MSVC + `--low-memory`。 脚本默认已带 `--low-memory`(不带必崩),检测到该错误码还会自动重试一次。 **Nuitka 提示 "Please install the English language pack for Visual Studio"?** 中文版 VS 的报错文本是 GBK,Nuitka 解析不了(只影响日志可读性,不影响构建)。 装个英文语言包,或忽略它、直接看 `packaging/build/nuitka-build.log`。 **Nuitka 提示 "Please install the English language pack for Visual Studio"?** 中文版 VS 的报错文本是 GBK,Nuitka 解析不了(只影响日志可读性,不影响构建)。 装个英文语言包,或忽略它、直接看 `packaging/build/nuitka-build.log`。 **体积能不能再小?** `--onedir` 后再自行压缩分发;或继续往 spec 的 `EXCLUDES` 里加确定用不到的模块。 单文件模式会把所有东西解压到临时目录,体积换的是分发便利。 **Nuitka 构建太慢?** 时间主要花在两处:一是 C 编译(Nuitka 的缓存目录会保留中间产物,第二次快很多), 二是 **LTO 链接**(实测单这一步就 6 分钟以上)。加 `--lto=no` 可显著缩短, 代价是产物略大、优化略少。另外 `.py` 文件改动越多,重新编译的文件越多。 **能不能打包 Linux / macOS 版?** 可以,但要在目标系统上构建(冻结产物不跨平台)。`packaging/_common.py` 里的 原生平台名已覆盖 `cocoa` / `xcb`,自检会自动按平台跑。注意本工具的硬件指纹 来源全是 Linux 侧的,打包目标平台与授权对象设备无关。