上位机开发日记 · 第 11 篇 · 打包与发布:PyInstaller 与 Nuitka
阅读时长:约 5 分钟 ·难度:进阶 ·前置知识:第 10 篇(日志与配置)
代码写完了,本篇让它能装到别人电脑上跑。
1. 交付前一天的经典场景
代码全部完成,功能测试通过。你信心满满地把main.py发给现场同事,附了一句"先装个 Python,再pip install -r requirements.txt"。
第二天收到回复:
“装 Python 说要装 Visual C++ 运行库,装不上。”
“pip 装 pyserial 报错了。”
“装好了,但双击没反应,黑框闪一下就没了。”
“我们这边电脑不能联网装东西。”
【判断】只要交付对象不是你自己,就必须打包。"装个 Python 就行"在实验室说得通,在现场永远说不通——这是上位机交付中最常见也最不必要的一次返工。
2. 两种主流方案
| 维度 | PyInstaller | Nuitka |
|---|---|---|
| 原理 | 打包字节码 + 附带 Python 解释器 | 把 Python 编译成 C,再编译为机器码 |
| 产物大小 | 较大 | 较小 |
| 启动速度 | 稍慢(需解包/加载) | 较快 |
| 逆向难度 | 低(字节码可反编译) | 高 |
| 构建时间 | 快 | 慢(首次尤其明显) |
| 生态成熟度 | 很成熟,遇到问题好搜 | 较成熟,边界情况稍多 |
| 适用场景 | 常规交付、内部工具 | 性能敏感、需保护代码 |
命令示意:
# PyInstallerpyinstaller--noconfirm--windowed--nameTool main.py# Nuitkapython-mnuitka--standalone--enable-plugin=pyside6 main.py【判断】默认用PyInstaller(快、稳、资料多);当有明确的启动速度或代码保护需求时,再考虑 Nuitka。不要为了"看起来更高级"而选 Nuitka—— 它的构建时间和排错成本都是真实代价。
(顺带一提:Nuitka 编译同样能提升纯 Python 计算热点的执行效率,如果你的项目里正好有性能瓶颈,这一项可以合并考虑。)
3. 四个必踩的坑
坑一:资源文件找不到
症状:源码里跑得好好的,打包后启动就报FileNotFoundError。
原因:打包后,程序运行时的目录结构完全变了。源码里那种open("assets/icon.png")的相对路径,指向的位置已经不是你以为的地方。
解决:写一个统一的资源定位函数。
importsysfrompathlibimportPathdefresource_path(relative:str)->Path:"""兼容源码运行、PyInstaller 打包、Nuitka 打包三种情况。"""ifhasattr(sys,"_MEIPASS"):# PyInstaller:临时解包目录base=Path(sys._MEIPASS)elif"__compiled__"inglobals():# Nuitka:编译后的模块目录base=Path(__file__).resolve().parentelse:# 源码运行base=Path(__file__).resolve().parentreturnbase/relative# 用法icon=resource_path("assets/icon.png")【事实】sys._MEIPASS是 PyInstaller 在运行时注入的属性,指向解包目录;Nuitka 则会定义__compiled__全局变量。两个标记都是各自工具的官方机制。
坑二:动态导入的模块没被打进去
症状:报ModuleNotFoundError,但那个模块你明明装了、也import了。
原因:打包工具靠静态分析找依赖。通过字符串导入的模块(importlib.import_module("xxx")、插件式加载、部分库的后端选择逻辑)它看不到。
解决:显式声明。
pyinstaller --hidden-import=pyserial.tools.list_ports main.pyNuitka 用--include-module=模块名。排查思路:报错里说缺哪个模块,就把它加进去;如果它还是一个库内部动态加载的,去该库的文档里搜"packaging"或"frozen"关键字,通常有官方说明。
坑三:驱动与运行库依赖
| 类型 | 问题 |
|---|---|
| 串口 | Windows 上可能需要附带 USB 转串口驱动(CH340 / FTDI / CP210x) |
| VISA | pyvisa 需要后端(NI-VISA / Keysight IO Libraries),这部分无法打包,要单独安装并写进交付说明 |
| VC 运行库 | 部分扩展模块依赖 MSVC 运行库 |
【判断】把"需要用户预先安装的东西"写成一份交付前置清单,比事后解释便宜得多。清单里至少包括:驱动、运行库、系统版本、权限要求。
坑四:首次运行被安全软件拦下
| 平台 | 处理方式 |
|---|---|
| macOS | 未签名应用会被 Gatekeeper 拦住。需要codesign签名,分发到外部还需notarytool公证 |
| Windows | 未签名 exe 常被 SmartScreen 或杀软报毒,需代码签名证书 |
【判断】如果交付对象是公司内部,通常可以走"手动允许"来绕过;但如果对外交付,签名与公证必须提前规划——它需要证书申请流程,往往要等好几天,绝不是交付前一天能搞定的事。
4. 打包时机:中期就要开始
这是本篇最重要的一条建议:
不要等交付前才第一次打包。
理由:
| 早期打包能提前暴露的问题 | 为什么拖到后期代价大 |
|---|---|
| 某个依赖打不进去 | 可能牵动架构(要换成可打包的替代库) |
| 配置文件路径假设错误 | 影响所有文件读写代码 |
| 需要附带驱动或运行库 | 影响交付文档与用户培训 |
| 性能和启动速度不达标 | 可能需要换方案重写 |
【判断】建议从项目进行到一半时,就固定每两周打一次包,并在一台"干净的"目标机器上跑一遍全流程。把它当成一次小型的集成测试,而不是收尾工作。
对调试打包版本也有一个小技巧:打包时先保留控制台(PyInstaller 去掉--windowed),这样启动失败时的报错能看到;确认稳定后再改成无控制台版本发布。
5. 本篇小结
| 记住这一条 | 说明 |
|---|---|
| 只要交付对象不是你,就必须打包 | 别指望现场装 Python |
| 默认选 PyInstaller | 快、稳、资料多;有明确需求再上 Nuitka |
资源路径统一走resource_path() | 三种运行方式一套代码 |
| 动态导入要显式声明 | hidden-import/include-module |
| 驱动和运行库写进交付清单 | VISA 这类后端无法打包 |
| 签名公证要提前规划 | 流程耗时,不能赶 |
| 中期就开始打包 | 打包是集成测试,不是收尾工作 |
| 打包版先用带控制台的版本调试 | 报错才看得见 |
6. 动手练习
今天就打一次包:哪怕项目还没完成,现在打个包,拿到另一台电脑上跑一遍。你大概率会立刻遇到至少一个坑——这比读十篇教程都值。
写交付前置清单:列出"用户需要预先准备什么"(驱动、运行库、系统版本、目录权限)。这份清单会成为你的交付文档的第一页。
检查硬编码路径:回顾第 10 篇的练习,把
open()里的字面路径全部改成resource_path()或从配置读取。
上一篇:[第 10 篇 · 工程化:日志与配置]
下一篇:[第 12 篇 · 测试:设备模拟器是核心资产]
——能装上了,接下来要有办法证明它是对的。