Skillversitas

Packaging with PyInstaller

PyInstaller is the tool that freezes the Python app (and its dependencies, including the Qt libraries and the platform plugins) into a standalone executable: the pyinstaller command analyzes the main script, bundles the site-packages the app imports, the Qt's DLLs/framework, and the platforms plugin (the qwindows.dll plugin, the libcocoa), and produces the distributable. The PyQt app's packaging needs two specifics the plain Python app does not: the Qt plugins (the platforms - the offscreen, the minimal, the platform's) and the resources_rc (the pyrcc chapter's compiled resource, the :/ path's content). This chapter covers the pyinstaller basics (the --onefile, the --windowed, the --add-data), the Qt plugin's (the --collect-all PyQt5 the simple, the --add-binary the specific), the resources's (the resources_rc hiddenimport, the qrc path's), and the per-platform output (the .exe, the .app bundle, the Linux's).

The pyinstaller: The Freeze

# the INSTALL: the pip (the tool)
pip install pyinstaller

# the FREEZE: the main script (the app's entry point)
pyinstaller --onefile --windowed main.py
# the --onefile: the SINGLE file (the dist's exe, the
# self-extracting on the run)
# the --windowed: the NO console (the GUI's, the
# pythonw equivalent - no console window)
# the --add-data: the non-Python asset (the qss, the
# sep is platform: ; the windows, : the unix)
# pyinstaller --onefile --windowed \
#   --add-data "assets/style.qss;style" main.py

The pyinstaller is the freeze: the pip install pyinstaller installs the tool, and the pyinstaller main.py analyzes the main script (the imports it follows), bundles the site-packages the app imports, and produces the dist output. The --onefile is the single-file (the exe the dist's, the app the dist's), the --windowed is the no-console (the GUI's, the pythonw's equivalent - the console window the print's not shown), and the --add-data is the non-Python asset (the qss the add-data, the sep the platform's: the ; the Windows', the : the unix's). The build spec (the spec file) is the pyinstaller's configuration (the Analysis, the PYZ, the EXE), and the --specpath the spec's location. The dist is the output (the exe the dist's), and the build is the intermediate (the spec's work). The pyinstaller is the freeze, and the Qt specifics (the next section) are the add the Qt's needs.

The Qt Plugins: The Platform Plugin

# the Qt's need: the PLATFORM PLUGIN (the qwindows.dll
# the windows, the libcocoa the mac, the libqxcb.so the
# linux) - the app CANNOT start without it (the "could
# not load the Qt platform plugin" is the missing one)
# the PyQt5 hook (the PyInstaller's) bundles most of it,
# and the --collect-all is the belt-and-suspenders:
pyinstaller --onefile --windowed \
  --collect-all PyQt5 main.py
# the --collect-all: the PyQt5's binaries + the data +
# the hiddenimports (the plugins, the resources, the
# translations) - the simple, the complete

# the specific: the --add-binary (the plugin's explicit
# path, the sep is the platform's ;)
# pyinstaller --add-binary \
#   "PyQt5/Qt5/plugins/platforms/*;Qt5/plugins/platforms" \
#   main.py

The Qt specifics are the plugins: the platform plugin (the qwindows.dll the Windows', the libcocoa the Mac's, the libqxcb.so the Linux') is the one the app cannot start without (the "could not load the Qt platform plugin" error is the missing one), and the PyQt5 hook (the PyInstaller's) bundles most of it. The --collect-all PyQt5 is the belt-and-suspenders: the PyQt5's binaries plus the data plus the hiddenimports (the plugins, the resources, the translations) - the simple, the complete. The --add-binary is the specific (the plugin's explicit path, the sep the platform's) - the form for the plugin the hook missed. The Qt plugins are the add the PyInstaller's Qt needs, and the --collect-all is the default the PyQt app uses.

The Resources: The resources_rc

# the RESOURCES: the resources_rc (the pyrcc chapter's
# compiled resource, the :/ path's content) - the
# hiddenimport (the import is the registration, the
# PyInstaller must BUNDLE it)
# the --hidden-import: the resources_rc (the module the
# main does not import directly - the import is in the
# app's module, the PyInstaller's analysis misses it)
pyinstaller --onefile --windowed \
  --collect-all PyQt5 \
  --hidden-import resources_rc main.py
# the --hidden-import: the resources_rc (the import the
# analysis missed, the :/ path's content bundled)
# the result: the :/ path RESOLVES in the frozen app
# (the resources_rc is the binary's, the import
# registers it, the :/ icons/app.png is the same)

The resources are the resources_rc (the pyrcc chapter's compiled resource, the :/ path's content): the hiddenimport (the import is the registration, the PyInstaller must bundle it), and the --hidden-import resources_rc (the module the main does not import directly - the import is in the app's module, the PyInstaller's analysis misses it). The result: the :/ path RESOLVES in the frozen app (the resources_rc is the binary's, the import registers it, the :/icons/app.png is the same) - the resource chapter's :/ path is stable across the dev and the frozen, and the hiddenimport is the packaging's add the resource needs.

The Per-Platform Output

# the WINDOWS: the dist/main.exe (the single file, the
# self-extracting on the run, the Qt5's DLLs inside)
pyinstaller --onefile --windowed --collect-all PyQt5 main.py
# the output: the dist/main.exe (the single file,
# the self-extracting on the run, the Qt5's DLLs inside)

# the MACOS: the dist/main.app (the BUNDLE, the
# Contents/MacOS the binary, the Contents/Resources
# the Qt's framework; the --windowed makes the .app)

# the LINUX: the dist/main (the ELF, the Qt's .so's
# inside, the libqxcb.so the platform plugin)

The output is the platform's: the Windows is the dist/main.exe (the single file, the self-extracting on the run, the Qt5's DLLs inside), the macOS is the dist/main.app (the bundle, the Contents/MacOS the binary, the Contents/Resources the Qt's framework), and the Linux is the dist/main (the ELF, the Qt's sos inside). The --windowed is the platform's (the Windows's no-console, the mac's the .app's no-terminal, the Linux's the no-tty). The platform's plugin is the platform's (the qwindows the Windows', the libcocoa the mac's, the libqxcb the Linux's), and the --collect-all is the platform's (the plugin the platform's). The frozen app is the platform's: the exe the Windows', the .app the mac's, the ELF the Linux's, and the Qt's plugin is the platform's (the add the collect's covers).

The Working Rules

  • The --collect-all PyQt5 is the default for the PyQt app. The PyQt5's binaries plus the data plus the hiddenimports (the plugins, the resources, the translations) - the platform plugin is the one the app cannot start without, and the collect is the add the Qt needs.
  • The resources_rc is the hiddenimport. The pyrcc chapter's compiled resource, the :/ path's content - the import is in the app's module (the PyInstaller's analysis misses it), and the --hidden-import resources_rc bundles it, the :/ path resolves in the frozen app.
  • The --add-data is the non-Python asset, the sep is the platform's. The qss the add-data, the ; the Windows', the : the unix's - the asset travels with the binary, and the sep is the platform's (the add-data's syntax).
  • The --onefile is the single file, the --windowed is the no-console. The exe the self-extracting on the run, the .app the mac's bundle, the ELF the Linux's - the onefile is the distribution's, and the windowed is the GUI's (the no-console, the pythonw equivalent).
  • The spec is the configuration, the dist is the output. The spec file (the Analysis, the PYZ, the EXE) is the PyInstaller's configuration (the --specpath the location), and the dist is the output (the exe, the .app, the ELF) - the build is the intermediate, and the dist is the distribution's.

Summary

PyInstaller freezes the Python app (and its dependencies, including the Qt libraries and the platform plugins) into a standalone executable: the pyinstaller main.py analyzes the main script, bundles the site-packages the app imports, and produces the dist output. The --onefile is the single file (the exe the self-extracting, the .app the mac's bundle, the ELF the Linux's), the --windowed is the no-console (the GUI's, the pythonw equivalent), and the --add-data is the non-Python asset (the sep the platform's: the ; the Windows', the : the unix's). The Qt specifics are the plugins (the platform plugin the app cannot start without) and the --collect-all PyQt5 (the PyQt5's binaries plus the data plus the hiddenimports, the add the Qt needs), and the resources are the resources_rc (the pyrcc chapter's compiled resource, the hiddenimport the PyInstaller must bundle, the :/ path resolves in the frozen app). The spec is the configuration, the dist is the output, and the frozen app is the platform's: the exe the Windows', the .app the mac's, the ELF the Linux's, the Qt's plugin the collect's.