解决Nuitka打包Pywebview项目的坑

Nuitka + pywebview 打包踩坑记:从 OOM 到 ImportError 的完整修复

背景

最近用 pywebview 写了个桌面端表情包管理工具 OhMyMeme,功能很简单:系统托盘 + 全局热键 + HTML 弹窗搜索/复制。打包时选用了 Nuitka,毕竟它编译出来的 exe 是真正的原生二进制,不像 PyInstaller 那样只是解压一个归档。

结果一路踩了几个坑,其实每个坑都值得单独写一篇。

环境

  • Windows 11 x64
  • Python 3.12.4
  • Nuitka 4.1.3 (Free)
  • pywebview 6.2.1

坑一:--onefile 模式下 zstd Out of Memory

现象

第一次打包,兴冲冲地上了 --onefile

bash
1
python -m nuitka --standalone --onefile --enable-plugin=tk-inter --msvc=latest src/

编译 1244 个模块全部通过,最后压缩阶段炸了:

code
1
ZstdError: zstd compress error: Allocation error : not enough memory

生成的 .dll 大约 61MB,payload blob 约 13MB。zstd 试图压缩时内存不足。

原因

--onefile 模式下,Nuitka 会把所有编译产物 + 数据文件打包成一个单文件。最终阶段用 zstd 压缩 payload,对于 60MB+ 的二进制,zstd 需要大量内存完成压缩。在 32GB 内存的机器上依然触发,说明这不是物理内存不足,而是 zstd 的 32 位进程地址空间限制或 Nuitka 内部 buffer 设置问题。

解决

放弃 --onefile,改用 --onedir(Nuitka 默认模式)。目录模式下不需要压缩步骤,直接生成文件夹 + exe 的部署结构:

bash
1
python -m nuitka --standalone --onedir ...

运行 python scripts/build.py --onedir 即可。如果你非要单文件且内存足够,可以试试商业版 Nuitka,它的压缩实现不同。

坑二:pywebview 插件硬编码 Allowlist 遗漏 webview.platforms.win32

现象

--onedir 构建成功,但 exe 运行时报错:

code
12
ImportError: Module 'webview.platforms.win32' was actively excluded
from Nuitka compilation.

更诡异的是,加上 --enable-plugin=pywebview 也没用。

根因

打开 Nuitka 的 pywebview 插件源码一探究竟:

C:\Python312\Lib\site-packages\nuitka\plugins\standard\PywebViewPlugin.py:41-47

python
12345678910
def decide_module(self, module_name, ...):
    if module_name.startswith("webview.platforms"):
        result = module_name in (
            "webview.platforms.winforms",
            "webview.platforms.edgechromium",
            "webview.platforms.edgehtml",
            "webview.platforms.mshtml",
            "webview.platforms.cef",
        )
        return result

这是一个硬编码的白名单,插件根据模块名是否在列表中决定是否编译。问题在于:这个列表是在 pywebview v5 时代写的,当时还没有 webview.platforms.win32 模块

pywebview 6.x 对 Windows 平台做了重构,把 Win32 API 相关功能抽到了独立的 webview/platforms/win32.py 中。winforms.py 第 22 行:

python
1
from webview.platforms import win32

Nuitka 插件检查 webview.platforms.win32 → 不在白名单 → return False → 模块被排除 → winforms 导入失败 → pywebview 认为 pythonnet 未安装 → 炸。

尝试过的无效方案

方案 结果
--enable-plugin=pywebview 插件逻辑没变,依然排除 win32
--include-module=webview.platforms.win32 用户要求 include vs 插件要求 exclude → Fatal Conflict
--no-deployment-flag=excluded-module-usage 不再报 excluded,但模块根本没被编译,变成 cannot import name 'win32'

最终解决

正确的做法是:禁用这个有 bug 的插件,手动指定所有需要的 platform 模块

bash
123456
--disable-plugin=pywebview
--include-module=webview.platforms.win32
--include-module=webview.platforms.winforms
--include-module=webview.platforms.edgechromium
--include-module=webview.platforms.mshtml
--include-module=webview.platforms.cef

--disable-plugin=pywebview 完全禁用 pywebview 插件,避免它的硬编码白名单干扰。然后 --include-module 逐个告诉 Nuitka 需要编译哪些 platform 模块。

后续

我向Nuitka项目提交了PR,已被合并。(顺手还修了个bug:项目将“==”写成了“=”,导致无论如何Qt都会被打包进去)

最终的完整构建命令

bash
12345678910111213141516
python -m nuitka --standalone ^
  --output-filename=OhMyMeme ^
  --python-flag=nosite --python-flag=-m ^
  --enable-plugin=tk-inter ^
  --disable-plugin=pywebview ^
  --include-module=webview.platforms.win32 ^
  --include-module=webview.platforms.winforms ^
  --include-module=webview.platforms.edgechromium ^
  --include-module=webview.platforms.mshtml ^
  --include-module=webview.platforms.cef ^
  --include-data-dir=src/webui=src/webui ^
  --include-package=PIL --include-package=pystray ^
  --include-package=pyperclip --include-package=cryptography ^
  --include-package=keyboard --include-package=bottle ^
  --windows-disable-console --msvc=latest ^
  src/

经验总结

  1. Nuitka 插件不一定可靠。插件的白名单可能滞后于依赖库的版本更新。遇到模块被错误排除时,检查对应插件的 decide_module 实现。

  2. --include-module vs 插件冲突:用户显式要求 include 的模块如果被插件 exclude,Nuitka 会报 Fatal Conflict。解决方案是 --disable-plugin=xxx 禁用有问题的插件。

  3. --no-deployment-flag=excluded-module-usage 的局限性:这个 flag 只是让 Nuitka 不把排除当错误,但不会让模块被编译。模块文件依然不会出现在 dist 目录中。

  4. --onedir 优先于 --onefile:除非你的程序很小(< 20MB),否则 --onefile 的 zstd 压缩阶段容易 OOM。--onedir 没有压缩步骤,更可靠,更新时也只需要替换 exe 本身。

  5. 动态导入是编译器的天敌importlib.import_module() 这种动态导入对任何静态编译器(Nuitka、Cython、PyInstaller)都是挑战。打包前用 grep -r "import_module\|__import__" 扫描一下代码,提前处理。

附:OhMyMeme 项目

项目地址:https://github.com/TNTXZ/OhMyMeme

一个轻量级表情包管理工具:系统托盘常驻、全局热键呼出、HTML 搜索/标签/收藏、一键复制到剪贴板、FTP 同步。

构建脚本见 scripts/build.py,已包含上述所有修复。