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

218 lines
12 KiB
Markdown
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.
# 打包分发
把 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 模式把工作目录里的散落文件一起拷进去。
## 构建后自检(为什么必须有)
冻结失败往往是**静默**的 —— 界面能开,但图标全空白、复选框点不动,或者签发时才抛异常。
所以构建脚本会自动调用 ``<exe> --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 侧的,打包目标平台与授权对象设备无关。