qt-packaging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Packaging Qt Python Applications

打包Qt Python应用

PyInstaller (most common)

PyInstaller(最常用)

Critical: Virtual Environment Isolation
The official Qt for Python docs document a known PyInstaller gotcha: if a system-level PySide6 is installed, PyInstaller silently picks it instead of your venv version. Before building:
bash
undefined
重点:虚拟环境隔离
Qt for Python官方文档提到一个PyInstaller的常见陷阱:如果系统级PySide6已安装,PyInstaller会静默选择它而非你的venv版本。构建前请执行:
bash
undefined

Remove ALL system-level PySide6 installs from the build machine

从构建机器中移除所有系统级PySide6安装包

pip uninstall pyside6 pyside6_essentials pyside6_addons shiboken6 -y
pip uninstall pyside6 pyside6_essentials pyside6_addons shiboken6 -y

Verify only venv version remains

验证仅保留venv版本

python -c "import PySide6; print(PySide6.file)"
python -c "import PySide6; print(PySide6.file)"

Must show a path inside .venv/, not /usr/lib or system site-packages

路径必须显示在.venv/内部,而非/usr/lib或系统site-packages


**`--onefile` limitation:** For Qt6, `--onefile` bundles cannot deploy Qt plugins automatically. The one-directory (`dist/MyApp/`) approach is reliable. Use `--onefile` only if you understand its limitations and handle Qt plugins manually.

**Installation:**
```bash
uv add --dev pyinstaller
Basic one-directory build:
bash
pyinstaller --name MyApp \
  --windowed \
  --icon resources/icons/app.ico \
  src/myapp/__main__.py
Spec file (reproducible builds):
python
undefined

**`--onefile`限制**:对于Qt6,`--onefile`捆绑包无法自动部署Qt插件。单目录(`dist/MyApp/`)方式更可靠。仅当你了解其限制并能手动处理Qt插件时,才使用`--onefile`。

**安装:**
```bash
uv add --dev pyinstaller
基础单目录构建:
bash
pyinstaller --name MyApp \
  --windowed \
  --icon resources/icons/app.ico \
  src/myapp/__main__.py
Spec文件(可重现构建):
python
undefined

MyApp.spec

MyApp.spec

block_cipher = None
a = Analysis( ["src/myapp/main.py"], pathex=[], binaries=[], datas=[ ("src/myapp/resources", "resources"), # (src, dest inside bundle) ], hiddenimports=[ "PySide6.QtSvg", # SVG support "PySide6.QtSvgWidgets", # SVG widgets "PySide6.QtXml", # required by some Qt modules ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=["tkinter", "matplotlib"], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, )
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name="MyApp", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=False, # True for CLI apps disable_windowed_traceback=False, argv_emulation=False, # macOS: use True for drag-and-drop files target_arch=None, codesign_identity=None, entitlements_file=None, icon="resources/icons/app.ico", )
coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=False, upx_exclude=[], name="MyApp", )

Run: `pyinstaller MyApp.spec`

**Qt plugin detection issues:** PySide6 often needs explicit plugin imports. Add to `hiddenimports`:
```python
hiddenimports = [
    "PySide6.QtSvg", "PySide6.QtSvgWidgets",
    "PySide6.QtPrintSupport",   # required by QTextEdit on some platforms
    "PySide6.QtDBus",           # Linux
]
QRC compiled resources: Include compiled
.py
resource files in
datas
or ensure they're importable. The cleanest approach is importing
rc_resources
in
__init__.py
so PyInstaller detects it automatically.
block_cipher = None
a = Analysis( ["src/myapp/main.py"], pathex=[], binaries=[], datas=[ ("src/myapp/resources", "resources"), # (源路径,捆绑包内目标路径) ], hiddenimports=[ "PySide6.QtSvg", # SVG支持 "PySide6.QtSvgWidgets", # SVG组件 "PySide6.QtXml", # 部分Qt模块所需 ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=["tkinter", "matplotlib"], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, )
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name="MyApp", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=False, # CLI应用设为True disable_windowed_traceback=False, argv_emulation=False, # macOS:如需处理拖放文件设为True target_arch=None, codesign_identity=None, entitlements_file=None, icon="resources/icons/app.ico", )
coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=False, upx_exclude=[], name="MyApp", )

运行:`pyinstaller MyApp.spec`

**Qt插件检测问题**:PySide6通常需要显式导入插件。添加至`hiddenimports`:
```python
hiddenimports = [
    "PySide6.QtSvg", "PySide6.QtSvgWidgets",
    "PySide6.QtPrintSupport",   # 部分平台上QTextEdit所需
    "PySide6.QtDBus",           # Linux平台
]
QRC编译资源:将编译后的
.py
资源文件加入
datas
,或确保其可被导入。最简洁的方式是在
__init__.py
中导入
rc_resources
,让PyInstaller自动检测到它。

Briefcase (cross-platform, preferred for distribution)

Briefcase(跨平台,分发首选)

Briefcase produces native platform installers (
.msi
,
.dmg
,
.AppImage
):
bash
pip install briefcase
briefcase create     # create platform package
briefcase build      # compile
briefcase run        # run from package
briefcase package    # create installer
pyproject.toml for Briefcase:
toml
[tool.briefcase]
project_name = "MyApp"
bundle = "com.myorg.myapp"
version = "1.0.0"
url = "https://myorg.com"
license = "MIT"
author = "My Name"
author_email = "me@myorg.com"

[tool.briefcase.app.myapp]
formal_name = "My Application"
description = "Description here"
icon = "resources/icons/app"   # no extension — briefcase uses platform-appropriate format
sources = ["src/myapp"]
requires = ["PySide6>=6.6"]
Briefcase handles Qt plugin bundling more reliably than PyInstaller for PySide6.
Briefcase可生成原生平台安装程序(
.msi
.dmg
.AppImage
):
bash
pip install briefcase
briefcase create     # 创建平台包
briefcase build      # 编译
briefcase run        # 从包中运行应用
briefcase package    # 创建安装程序
Briefcase对应的pyproject.toml:
toml
[tool.briefcase]
project_name = "MyApp"
bundle = "com.myorg.myapp"
version = "1.0.0"
url = "https://myorg.com"
license = "MIT"
author = "My Name"
author_email = "me@myorg.com"

[tool.briefcase.app.myapp]
formal_name = "My Application"
description = "此处填写描述"
icon = "resources/icons/app"   # 无需扩展名——Briefcase会使用平台适配的格式
sources = ["src/myapp"]
requires = ["PySide6>=6.6"]
对于PySide6,Briefcase处理Qt插件捆绑的可靠性优于PyInstaller。

Windows: windeployqt + Code Signing

Windows:windeployqt + 代码签名

After PyInstaller builds the one-directory package, run
windeployqt
from the Qt SDK to copy any missing Qt plugins and translations:
bash
undefined
PyInstaller构建单目录包后,运行Qt SDK中的
windeployqt
以复制缺失的Qt插件和翻译文件:
bash
undefined

Run from the Qt SDK tools directory (or add to PATH)

从Qt SDK工具目录运行(或添加至PATH)

windeployqt dist/MyApp/MyApp.exe

This ensures platform plugins (`qwindows.dll`) and other Qt plugin DLLs are present. PyInstaller hooks should collect most of them automatically, but `windeployqt` catches stragglers.

```bash
windeployqt dist/MyApp/MyApp.exe

这能确保平台插件(`qwindows.dll`)和其他Qt插件DLL存在。PyInstaller钩子通常会自动处理大部分情况,但`windeployqt`能补全遗漏的部分。

```bash

Sign the executable (requires a code signing certificate)

签名可执行文件(需要代码签名证书)

signtool sign /fd SHA256 /a /tr http://timestamp.digicert.com dist/MyApp.exe

Unsigned Windows executables trigger SmartScreen warnings. For internal distribution, instruct users to right-click → Properties → Unblock.
signtool sign /fd SHA256 /a /tr http://timestamp.digicert.com dist/MyApp.exe

未签名的Windows可执行文件会触发SmartScreen警告。内部分发时,可告知用户右键→属性→解除阻止。

macOS: App Bundle

macOS:App捆绑包

PyInstaller produces a
.app
bundle. For distribution outside the App Store:
bash
undefined
PyInstaller会生成
.app
捆绑包。如需在App Store外分发:
bash
undefined

Ad-hoc signing (no developer ID)

临时签名(无需开发者ID)

codesign --force --deep --sign - dist/MyApp.app
codesign --force --deep --sign - dist/MyApp.app

With developer ID

使用开发者ID签名

codesign --force --deep --sign "Developer ID Application: Name (TEAM_ID)" dist/MyApp.app
codesign --force --deep --sign "Developer ID Application: Name (TEAM_ID)" dist/MyApp.app

Notarization (required for Gatekeeper)

公证(Gatekeeper要求)

xcrun notarytool submit dist/MyApp.zip --apple-id me@example.com --team-id TEAM_ID
undefined
xcrun notarytool submit dist/MyApp.zip --apple-id me@example.com --team-id TEAM_ID
undefined

Linux: AppImage via PyInstaller

Linux:通过PyInstaller制作AppImage

bash
undefined
bash
undefined

Build one-directory first, then package as AppImage

先构建单目录包,再打包为AppImage

appimagetool dist/MyApp/ MyApp-x86_64.AppImage
undefined
appimagetool dist/MyApp/ MyApp-x86_64.AppImage
undefined

Build Automation (CI)

构建自动化(CI)

yaml
undefined
yaml
undefined

.github/workflows/build.yml

.github/workflows/build.yml

jobs: build-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - run: pip install pyinstaller PySide6 - run: pyinstaller MyApp.spec - uses: actions/upload-artifact@v4 with: name: windows-build path: dist/MyApp/
undefined
jobs: build-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - run: pip install pyinstaller PySide6 - run: pyinstaller MyApp.spec - uses: actions/upload-artifact@v4 with: name: windows-build path: dist/MyApp/
undefined

Common Packaging Pitfalls

常见打包陷阱

  • Missing Qt platform plugins:
    qt.qpa.plugin: Could not find the Qt platform plugin
    — ensure
    PySide6/Qt/plugins/platforms/
    is included. PyInstaller hooks usually handle this; rebuild if not.
  • Missing SVG support: Import
    PySide6.QtSvg
    in
    hiddenimports
    or the app will crash loading SVGs silently.
  • Relative path assumptions: Use
    Path(__file__).parent
    for locating resource files in development; use
    sys._MEIPASS
    for PyInstaller runtime paths (or bundle via QRC to avoid the problem entirely).
  • App freezes on macOS: Set
    argv_emulation=True
    in the spec if the app needs to handle file associations.
  • 缺失Qt平台插件
    qt.qpa.plugin: Could not find the Qt platform plugin
    ——确保包含
    PySide6/Qt/plugins/platforms/
    。PyInstaller钩子通常会处理此问题,若未解决请重新构建。
  • 缺失SVG支持:在
    hiddenimports
    中导入
    PySide6.QtSvg
    ,否则应用加载SVG时会静默崩溃。
  • 相对路径假设:开发环境中使用
    Path(__file__).parent
    定位资源文件;PyInstaller运行时使用
    sys._MEIPASS
    (或通过QRC捆绑资源彻底避免此问题)。
  • macOS上应用冻结:如果应用需要处理文件关联,在spec中设置
    argv_emulation=True