技能|PyInstaller打包与取证SOP
> 来源:<知识库系统>多次重建 exe,以及本次(2026-10-03)突破「无 NSIS 安装包」阻塞的实战。
> 起因:改了 app.py 却仍在跑旧 exe ⇒ "改了没生效";本机无 NSIS/Inno、精简 Python 的 msilib 被移除 ⇒ 安装包长期阻塞。
适用场景
单文件/少量文件的 Python 桌面应用(PyInstaller + pywebview 壳)需要重打包、验证产物含新代码、并产出可双击安装的安装包。
不适用:脚本类小工具、容器化部署、需系统级安装(写服务/驱动)的正式分发(敏感环境不联网,NSIS/Inno 不可用)。
触发词
打包、重打包、PyInstaller、COLLECT 被拦、打的 exe 没跟上源码、为什么改了没生效、dist 快照清理、--distpath、.spec 入口、哪些文件要打包、隔离端口取证、_verify_frozen、exe 起来几秒就退出、安装包、自解压安装器、无 NSIS 怎么打安装包、makensis 没有、msilib
操作步骤
-
先判断"要不要重打包":看
.spec的Analysis([...])入口。被import链追踪到的(如app.py由壳kb_desktop.pywimport)⇒ 改了必须重打包;独立进程由计划任务跑源码的(如看门狗kb_watchdog.pyw)⇒ 改了不用打包。 -
移走旧 dist(不直接删):
mv dist/<产品> dist/_prev_<日期>。沙箱会拦 COLLECT 整目录删除且先删一半 ⇒ dist 半残。 -
全新 distpath 打包:
python -m PyInstaller <产品>.spec --distpath dist/_build_<日期>,打完确认齐全再mv回dist/<产品>。大 payload 加--noupx(避免 UPX 内存风险)。 -
隔离端口取证:
KB_PORT=8799起 exe,启动后 0.4s 轮询抢窗口(exe 约 18s 自退)。拉渲染后 HTML 逐条断言新特征串在、旧文案不在、版本号对。取证报告落文件。 -
回收旧快照:确认新包可用后移
_prev_*到.trash(不可逆删除走.trash;本机回收站失效,勿用SHFileOperationW假删)。 -
交付一致性铁律(最关键):凡是改了被追踪的源码 ⇒ 必须
重打包 dist → 同步 PC客户端 → 递增版本重建安装包三步全做,否则用户装到的仍是旧客户端。验收标准:部署自测时,安装包释放出的 exe 大小与 dist 内 exe 大小完全一致。 -
无编译器依赖的自解压安装器(替代 NSIS/Inno):本机无 makensis/ISCC,且精简 Python 的
msilib被移除、tkinter/7z不可用 ⇒ 用 PyInstaller 把「安装逻辑 + 全部资源」打成一个 onefile exe,双击即自解压部署。
- 安装逻辑零 GUI 框架:用ctypes直接调user32.MessageBoxW/shell32.SHBrowseForFolderW做向导(不需要 tkinter/webview),避免额外依赖。
- 无人值守:支持--silent(隐含无弹窗纯部署)/--dir X(指定目录)/--nogui;交互模式才弹窗。无显示器/无桌面会话时绝不可弹 MessageBoxW(会挂起数分钟)——用--silent或 headless 自测。
- 防自我嵌套(必做):构建前先把旧安装包 exe 从 payload 源目录移出,打完再放回;否则每版都把上一版打进新包,体积指数膨胀(73→153→230MB 实测)。同理清理运行时产物(webview_profile缓存 39MB、启动日志)不进包。
- 版本递增不覆盖:产物名带版本号<知识库系统>安装包_vX.Y.exe,新版本另起文件名,旧版移.trash;构建脚本里的INSTALLER_NAME与输出dst路径必须同步改,否则会"版本号升了却覆盖旧文件"。
- 部署自测:安装包.exe --silent --dir <临时目录>,验证文件数/exe 大小/壳脚本/卸载 bat 齐全,安装日志复制完整。
注意边界
- 打包前必须
py_compile;改过内联 JS 还要node --check。 - 敏感项目:exe 只绑 <本机地址>,打包不引入任何联网依赖。
- 安装器 exe 用
--noupx;onefile 首次运行会解压到临时目录(慢几秒),属正常。 ctypes调 Win32 API 仅在 Windows 生效;跨平台需另写分支(本环境只 Windows)。
反例避雷
| 反例 | 为什么错 | 正确做法 |
|---|---|---|
| 改完源码不重打包就验证 | 日常跑旧二进制,验证全假 | 看 .spec Analysis 入口判断是否被追踪 |
| 让 PyInstaller 直接 COLLECT 到旧 dist | 批量删被拦且先删一半,dist 半残 | 旧目录改名 _prev_<日期> + --distpath 全新目录 |
| 取证起完 exe 先睡 5 秒 | exe 约 18s 自退、窗口短 | 启动后立刻 0.4s 轮询 |
| 取证用日常端口 8765 | 抢端口/被看门狗干扰 | 隔离端口 KB_PORT=8799 |
| 装了新功能只重打包不重打安装包 | 用户装的仍是缺功能的旧客户端 | 重打包→同步PC客户端→递增版本重打安装包,部署自测 exe 大小一致 |
| 安装包构建不排旧包 | 旧包被当 payload 递归,体积膨胀/内容错 | 构建前移出旧安装包,打完放回 |
| 无人值守场景弹 MessageBoxW | 无桌面会话会挂起数分钟 | --silent 无弹窗;headless 用 --nogui 自测 |
| 安装器含 webview_profile/日志 | 缓存 39MB 进包,包臃肿且泄漏运行时状态 | 打包前清理运行时产物 |