# 打包 svfs-studio / svfs-client 为独立可执行文件 把两个 PySide6 应用打成"发给谁都能双击运行"的形态。支持两个后端: **PyInstaller**(推荐:构建快、Qt/QtWebEngine 钩子成熟)和 **Nuitka**(真编译:启动更快、更难反编译,代价是构建慢 + 需要 C 编译器)。 ## 环境准备(一次性) ```bash uv sync --extra gui # PySide6(完整版,含 WebEngine) uv pip install pyinstaller # PyInstaller 路线 # 或 uv pip install nuitka # Nuitka 路线(还需 C 编译器,见下文) ``` ## PyInstaller(推荐) ```bash # 目录版(推荐分发方式:启动快;整个目录一起拷贝分发) uv run python packaging/build.py --tool pyinstaller --app both # 只打一个 uv run python packaging/build.py --tool pyinstaller --app client uv run python packaging/build.py --tool pyinstaller --app studio # 单文件版(SVFS_ONEFILE=1 传给 spec;QtWebEngine 启动前要先解包,明显更慢) uv run python packaging/build.py --tool pyinstaller --app both --onefile ``` 产物: ```text dist/SVFS-Studio/SVFS-Studio.exe dist/SVFS-Client/SVFS-Client.exe # --onefile 时是 dist/SVFS-Client.exe ``` 要点: * 配置在 `svfs_studio.spec` / `svfs_client.spec`(数据文件、隐藏导入都在那里声明); * QtWebEngine 的运行时(`QtWebEngineProcess.exe`、icudtl.dat、资源包、locale) 由 pyinstaller-hooks-contrib 的 PySide6 钩子自动收集,**请保持 PyInstaller ≥ 6**; * 入口不是 `src/*/__main__.py`(有相对导入),而是本目录的 `run_studio.py` / `run_client.py`(绝对导入); * `svfs_studio` 的图标资源随包打进 `resources/`, `sample.repo_root()` 已适配冻结环境(PyInstaller 找 `sys._MEIPASS`, Nuitka 找可执行文件同级目录)。 ## Nuitka ```bash # 先看看会执行什么命令 uv run python packaging/build.py --tool nuitka --app client --dry-run # 正式构建(首次缺 C 编译器时会自动下载 MinGW64;也可自备 MSVC) uv run python packaging/build.py --tool nuitka --app both ``` 产物在 `<输出目录>/run_.dist/` 下(默认输出目录 `build/nuitka`; **路径含非 ASCII 字符时自动退到系统临时目录**——Windows 的 MSVC 链接器 在中文路径下会 LNK1104)。 要点: * `--enable-plugin=pyside6` 负责 Qt 全家桶(含 WebEngine 子进程与资源); * standalone 模式 + `--windows-console-mode=disable`(GUI 程序不弹控制台); * 构建慢(整包编译,几分钟到十几分钟),但产物启动比 PyInstaller 快、 没有解包临时目录的问题。 **⚠ 已知问题(2026-09 实测)**:Nuitka 4.2.1 + PySide6 6.11.2 的产物运行时报 `Unable to import Shiboken`(编译后的 `shiboken6.Shiboken` 无法导入)。 用最小 PySide6 程序即可复现,属工具链兼容问题,与本仓库代码无关; `--include-package=shiboken6`、`--nofollow-import-to` + 数据目录两种绕法均无效。 解决途径:等 Nuitka 发版修复,或临时降级 PySide6(`uv pip install "PySide6<6.10"`) 后再构建。**PyInstaller 路线不受影响,当前请优先使用。** ## 体积裁剪 两个 spec 在 Analysis 之后按**白名单**过滤 `a.binaries` / `a.datas` (PySide6 的钩子会把 wheel 里所有 Qt 模块的 DLL/pyd 整包搬进来,黑名单删不干净)。 实测效果(PyInstaller 6.22 + PySide6 6.11,Windows x64): | 应用 | 原始 | 目录版(白名单) | 单文件版(LZMA) | |---|---|---|---| | SVFS-Studio | 116 MB | **66 MB** | **29 MB** | | SVFS-Client | 559 MB | **300 MB** | **126 MB** | Client 的白名单来自 objdump 实测的导入表(不是猜的): * `Qt6WebEngineCore` 链 Core/Gui/Network/Positioning/Qml/Quick/WebChannel, `WebEngineWidgets` 另需 PrintSupport/QuickWidgets/Widgets,`Qt6Quick` 另需 Qt6OpenGL 与 QmlMeta/QmlModels —— 这些 DLL 必须留; * pyd 只留 Python 层实测 import 链用到的 8 个绑定(QtCore/QtGui/QtWidgets/ QtNetwork/QtPrintSupport/QtWebChannel/QtWebEngineCore/QtWebEngineWidgets), 注意 pyd 的导入链比 DLL 的依赖多两个(QtNetwork/QtPrintSupport); * 删掉的:Quick3D 全家、QuickControls2 样式、Pdf、ShaderTools、Svg、 opengl32sw.dll(20M,Chromium 自带 SwiftShader 兜底)、DevTools 资源包(85M)、 QML 组件目录(29M)、Qt 译文、虚拟键盘、qmltooling。 裁不动的:`Qt6WebEngineCore.dll` ~195M(Chromium 本体)+ `icudtl.dat` 10M。 已知取舍: * DevTools 资源包删了 → 插件页面里 F12 检查元素无资源,调试请用开发环境; * opengl32sw 删了 → 极端无 GPU 环境若渲染异常,把它加回 spec 的保留例外重打; * Qt 译文删了 → Qt 内建对话框退回英文,界面自有文案不受影响。 **UPX 实测无效**:对 Qt DLL 用 `--best` 只把 Client 目录版从 300M 压到 290M (pyd 全部压缩失败),收益 3%,还要承担杀软误报——不推荐。 想要更小就用单文件版(上表第三列),代价是每次启动先解包到临时目录 (Client 单文件启动明显慢,QtWebEngine 尤甚)。 ## 冒烟验证 ```bash # client:--help 在进入 GUI 之前返回(退出码 0 即装配成功) ./dist/SVFS-Client/SVFS-Client.exe --help # client:真实加载一个插件页面(--open 启动后直开指定插件; # 页面加载后 web/ 下会出现 Chromium 的 Local Storage/GPUCache 等痕迹) ./dist/SVFS-Client/SVFS-Client.exe --data <已装插件的数据目录> --open <插件id> # studio:offscreen 截图模式,跑完自动退出并在目录里留下每页一张 PNG ./dist/SVFS-Studio/SVFS-Studio.exe --shots ./smoke-shots ``` ## 常见问题 * **杀软误报**:PyInstaller 单文件版常见,目录版较少;Nuitka 编译产物基本不误报。 * **体积**:QtWebEngine 应用的目录版约 150~250 MB,属正常(Chromium 运行时)。 * **数据目录**:两个应用的用户数据都写在 `%LOCALAPPDATA%/<应用名>`(client)与 QSettings 里(studio 的窗口几何), 与安装位置无关,覆盖更新不需要迁移。 * **离线构建**:`--assume-yes-for-downloads` 会在缺编译器时下载 MinGW64; 全离线环境请预装 MSVC Build Tools 或自行提供 MinGW64。