Desktop Packaging | Skills Pool
Desktop Packaging Python desktop application packaging and distribution patterns. Use when building executables, installers, or distributing PySide6/PyQt applications.
Desktop Application Packaging
Overview
This skill covers packaging Python desktop applications into standalone executables and installers. It focuses on PySide6/PyQt applications and covers PyInstaller, Nuitka, cx_Freeze, and creating professional installers.
When to Use
Building standalone executables from Python code
Creating Windows installers (.msi, .exe)
Bundling PySide6/PyQt applications
Optimizing package size
Setting up CI/CD for desktop builds
Code signing and distribution
Tool Speed Size Compatibility Obfuscation PyInstaller Fast Large Excellent
npx skills add CloverKG369/Media-Tools
星标 0
更新时间 2026年4月4日
职业 Nuitka Slow Medium Good Native
cx_Freeze Fast Large Good Basic
Briefcase Medium Medium Good Basic
PyInstaller Patterns
1. Basic Spec File for PySide6 # app.spec
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
import sys
from pathlib import Path
block_cipher = None
# Project paths
PROJECT_ROOT = Path(SPECPATH)
SRC_PATH = PROJECT_ROOT / 'src'
# Collect PySide6 data files
pyside6_datas = collect_data_files('PySide6', include_py_files=False)
# Custom data files
datas = pyside6_datas + [
(str(PROJECT_ROOT / 'resources'), 'resources'),
(str(PROJECT_ROOT / 'locales'), 'locales'),
(str(PROJECT_ROOT / 'config'), 'config'),
]
# Hidden imports (not detected automatically)
hiddenimports = [
'PySide6.QtSvg',
'PySide6.QtSvgWidgets',
'PySide6.QtMultimedia',
'PySide6.QtNetwork',
] + collect_submodules('encodings')
# Exclude unnecessary modules
excludes = [
'tkinter',
'matplotlib',
'numpy.testing',
'pytest',
'IPython',
]
a = Analysis(
[str(PROJECT_ROOT / 'main.py')],
pathex=[str(SRC_PATH)],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
# Remove unused Qt plugins
def filter_binaries(binaries):
"""Remove unnecessary Qt binaries."""
exclude_patterns = [
'Qt6WebEngine',
'Qt6Quick',
'Qt6Qml',
'Qt6Designer',
'd3dcompiler',
]
filtered = []
for name, path, type_ in binaries:
if not any(pattern in name for pattern in exclude_patterns):
filtered.append((name, path, type_))
return filtered
a.binaries = filter_binaries(a.binaries)
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=True,
console=False, # Set True for CLI apps
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=str(PROJECT_ROOT / 'resources' / 'icon.ico'),
version=str(PROJECT_ROOT / 'version_info.txt'),
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='MyApp',
)
2. Resource Path Handler # utils/resource_path.py
import sys
from pathlib import Path
def get_resource_path(relative_path: str) -> Path:
"""Get absolute path to resource, works for dev and PyInstaller."""
if hasattr(sys, '_MEIPASS'):
# Running as compiled executable
base_path = Path(sys._MEIPASS)
else:
# Running as script
base_path = Path(__file__).parent.parent
return base_path / relative_path
def get_data_path() -> Path:
"""Get writable data directory."""
if sys.platform == 'win32':
from PySide6.QtCore import QStandardPaths
app_data = Path(QStandardPaths.writableLocation(
QStandardPaths.AppDataLocation
))
elif sys.platform == 'darwin':
app_data = Path.home() / 'Library' / 'Application Support' / 'MyApp'
else:
app_data = Path.home() / '.myapp'
app_data.mkdir(parents=True, exist_ok=True)
return app_data
# Usage in application
ICON_PATH = get_resource_path('resources/icons/app.png')
CONFIG_PATH = get_data_path() / 'config.json'
3. Version Info File (Windows) # version_info.txt
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(1, 0, 0, 0),
prodvers=(1, 0, 0, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'040904B0',
[
StringStruct('CompanyName', 'Your Company'),
StringStruct('FileDescription', 'My Application'),
StringStruct('FileVersion', '1.0.0'),
StringStruct('InternalName', 'myapp'),
StringStruct('LegalCopyright', '© 2024 Your Company'),
StringStruct('OriginalFilename', 'MyApp.exe'),
StringStruct('ProductName', 'My Application'),
StringStruct('ProductVersion', '1.0.0'),
]
)
]
),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]
)
4. PyInstaller Build Script # scripts/build.py
"""Build script for creating distributable package."""
import subprocess
import shutil
import sys
from pathlib import Path
from datetime import datetime
PROJECT_ROOT = Path(__file__).parent.parent
BUILD_DIR = PROJECT_ROOT / 'build'
DIST_DIR = PROJECT_ROOT / 'dist'
SPEC_FILE = PROJECT_ROOT / 'app.spec'
def clean_build() -> None:
"""Remove previous build artifacts."""
for path in [BUILD_DIR, DIST_DIR]:
if path.exists():
print(f"Removing {path}")
shutil.rmtree(path)
def run_pyinstaller() -> bool:
"""Run PyInstaller with spec file."""
cmd = [
sys.executable,
'-m', 'PyInstaller',
'--clean',
'--noconfirm',
str(SPEC_FILE)
]
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=PROJECT_ROOT)
return result.returncode == 0
def copy_additional_files() -> None:
"""Copy additional files to dist."""
dist_app = DIST_DIR / 'MyApp'
# Copy license
shutil.copy(PROJECT_ROOT / 'LICENSE', dist_app)
# Copy readme
shutil.copy(PROJECT_ROOT / 'README.md', dist_app)
def create_archive() -> Path:
"""Create zip archive of distribution."""
timestamp = datetime.now().strftime('%Y%m%d')
archive_name = f'MyApp-1.0.0-{timestamp}'
archive_path = shutil.make_archive(
str(DIST_DIR / archive_name),
'zip',
DIST_DIR,
'MyApp'
)
return Path(archive_path)
def main() -> int:
print("=== Building Application ===")
print("\n1. Cleaning previous build...")
clean_build()
print("\n2. Running PyInstaller...")
if not run_pyinstaller():
print("ERROR: PyInstaller failed")
return 1
print("\n3. Copying additional files...")
copy_additional_files()
print("\n4. Creating archive...")
archive = create_archive()
print(f"Created: {archive}")
print("\n=== Build Complete ===")
return 0
if __name__ == '__main__':
sys.exit(main())
Nuitka Patterns
1. Nuitka Build Configuration # scripts/build_nuitka.py
"""Build with Nuitka for better performance and obfuscation."""
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
def build_with_nuitka() -> bool:
"""Build using Nuitka."""
cmd = [
sys.executable,
'-m', 'nuitka',
'--standalone',
'--onefile', # Single executable
'--enable-plugin=pyside6',
'--include-data-dir=resources=resources',
'--include-data-dir=locales=locales',
# Windows specific
'--windows-icon-from-ico=resources/icon.ico',
'--windows-company-name=Your Company',
'--windows-product-name=My Application',
'--windows-file-version=1.0.0.0',
'--windows-product-version=1.0.0.0',
'--windows-file-description=My Application',
'--windows-disable-console',
# Optimization
'--lto=yes',
'--jobs=4',
# Output
'--output-dir=dist',
'--output-filename=MyApp',
# Main script
'main.py'
]
result = subprocess.run(cmd, cwd=PROJECT_ROOT)
return result.returncode == 0
if __name__ == '__main__':
success = build_with_nuitka()
sys.exit(0 if success else 1)
Installer Creation
1. Inno Setup Script (Windows) ; installer.iss
#define MyAppName "My Application"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "Your Company"
#define MyAppURL "https://yourwebsite.com"
#define MyAppExeName "MyApp.exe"
[Setup]
AppId={{YOUR-GUID-HERE}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
LicenseFile=dist\MyApp\LICENSE
OutputDir=installer
OutputBaseFilename=MyApp-{#MyAppVersion}-Setup
SetupIconFile=resources\icon.ico
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "vietnamese"; MessagesFile: "compiler:Languages\Vietnamese.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "dist\MyApp\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "dist\MyApp\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Code]
function InitializeSetup(): Boolean;
begin
Result := True;
end;
2. Build Installer Script # scripts/build_installer.py
"""Create Windows installer using Inno Setup."""
import subprocess
import sys
from pathlib import Path
ISCC_PATH = Path(r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe")
PROJECT_ROOT = Path(__file__).parent.parent
def build_installer() -> bool:
"""Compile Inno Setup script."""
iss_file = PROJECT_ROOT / 'installer.iss'
if not ISCC_PATH.exists():
print("ERROR: Inno Setup not found")
return False
cmd = [str(ISCC_PATH), str(iss_file)]
result = subprocess.run(cmd, cwd=PROJECT_ROOT)
return result.returncode == 0
if __name__ == '__main__':
success = build_installer()
sys.exit(0 if success else 1)
CI/CD Configuration
GitHub Actions Workflow # .github/workflows/build.yml
02
Overview