Различные статьи по Qt и c++

Упаковка Python/Qt приложений: PyInstaller, fbs, Nuitka для дистрибуции

Введение

Статья 451 DevOps-цикла разобрала упаковку C++ Qt-приложений через windeployqt/linuxdeployqt/macdeployqt — для Python-приложений нужен принципиально иной набор инструментов, поскольку конечному пользователю требуется не просто упаковать Qt-зависимости, но и поставить целый интерпретатор Python вместе со всеми используемыми Python-пакетами, без необходимости отдельной установки Python на машине пользователя.

Концепция

PyInstaller анализирует Python-приложение и его зависимости (включая PySide6 и все используемые сторонние пакеты), упаковывая всё необходимое (интерпретатор Python, библиотеки, ресурсы) в единый исполняемый файл или директорию, готовую для распространения без необходимости установки Python на машине конечного пользователя. fbs (fman build system) — специализированная надстройка над PyInstaller, ориентированная конкретно на PySide-приложения, упрощающая типичные задачи (настройка иконки, версии, подписи кода) по сравнению с использованием «чистого» PyInstaller. Nuitka — альтернативный подход, компилирующий Python-код в настоящий C-код, а затем в нативный машинный код, что может дать как ускорение выполнения (частичное смягчение проблемы производительности из статьи 507), так и более компактный итоговый результат по сравнению с PyInstaller, упаковывающим интерпретатор целиком.

Пример кода: Упаковка Python/Qt приложений

# Базовая упаковка через PyInstaller
pip install pyinstaller
pyinstaller --name MyPySideApp --windowed --onefile main.py
# --windowed — без консольного окна (для GUI-приложения)
# --onefile — единственный исполняемый файл вместо директории с множеством отдельных файлов
# spec-файл PyInstaller для более тонкой настройки (генерируется автоматически, редактируется вручную)
# MyPySideApp.spec
a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[('resources/icons', 'resources/icons')],  # явное включение ресурсов, не обнаруживаемых автоматически
    hiddenimports=['PySide6.QtQml'],  # модули, используемые динамически и не обнаруживаемые статическим анализом
    hookspath=[],
)
# Nuitka — компиляция в нативный код вместо упаковки интерпретатора
pip install nuitka
python -m nuitka --standalone --enable-plugin=pyside6 --windows-disable-console main.py
# --enable-plugin=pyside6 — специальный плагин Nuitka, учитывающий специфику PySide6-приложений

Пояснения к коду

Команда pyinstaller --windowed --onefile main.py показывает простейший, базовый случай упаковки — для многих относительно простых приложений этого достаточно, хотя --onefile имеет собственные практические компромиссы (статья ниже о подводных камнях). spec-файл с явным datas/hiddenimports показывает практически важную деталь — PyInstaller выполняет статический анализ кода для определения нужных зависимостей, но не всегда способен автоматически обнаружить все реально используемые ресурсы или динамически импортируемые модули (особенно специфичные подмодули PySide6, такие как QtQml, используемые не через прямой import верхнего уровня), и эти случаи требуют явного указания в конфигурации. Команда Nuitka с --enable-plugin=pyside6 показывает специализированную поддержку именно для PySide6-проектов — этот плагин помогает Nuitka правильно обработать специфичные для PySide6 паттерны (динамическая загрузка плагинов Qt, ресурсы QML), которые иначе потребовали бы значительной ручной настройки.

Подводные камни

  • Использование --onefile без понимания связанного с ним компромисса по времени запуска приложения — режим единственного файла удобен для распространения (всего один файл вместо целой директории), но требует распаковки всего содержимого во временную директорию при каждом запуске приложения, что заметно увеличивает время старта по сравнению с режимом директории (--onedir), где файлы уже распакованы и готовы к немедленному использованию — для приложений, где быстрый старт важен, стоит явно оценить этот компромисс, а не выбирать --onefile исключительно из-за удобства распространения единственного файла.
  • Забытое явное указание динамически импортируемых модулей или нестандартно расположенных ресурсов, что приводит к ошибке только в УЖЕ упакованном приложении на машине пользователя, хотя само приложение из исходного кода (без упаковки) работало корректно на машине разработчика — статический анализ PyInstaller не может обнаружить модули, импортируемые динамически (через importlib или строковые имена модулей) или используемые лишь косвенно через внутреннюю логику самого PySide6, и тщательное тестирование именно УПАКОВАННОЙ версии приложения (а не только версии, запускаемой непосредственно из исходного кода) — необходимый шаг перед распространением.
  • Игнорирование существенной разницы в итоговом размере упакованного приложения между разными инструментами и настройками, особенно при включении полного PySide6 (со всеми его модулями, многие из которых конкретное приложение может не использовать вовсе) — для уменьшения итогового размера стоит явно исследовать возможности исключения неиспользуемых модулей (excludes в spec-файле PyInstaller) и сравнить размер результата между разными инструментами (PyInstaller против Nuitka), поскольку эта разница может быть существенной для случаев, где размер дистрибутива важен (ограниченная пропускная способность загрузки, ограничения конкретной платформы распространения).
  • Недостаточное тестирование упакованного приложения на чистой системе без установленного Python и без тех системных библиотек, что присутствуют на машине разработчика — аналогично подводному камню статьи 451 DevOps-цикла про упаковку C++ приложений, успешная работа на машине разработчика (где может быть установлено множество дополнительных библиотек и инструментов) не гарантирует корректной работы на действительно «чистой» машине конечного пользователя, и реальная проверка на такой чистой среде (что естественно происходит при интеграции упаковки в CI, статья 454 DevOps-цикла) — необходимый, а не опциональный шаг перед распространением приложения.