Как стать автором
Обновить

Сборка Python-приложения в бинарник через Nuitka: особенности, проблемы и решения

Уровень сложностиСредний

Введение

Python удобен для разработки, но его интерпретируемая природа создаёт проблемы при распространении. При использовании PyInstaller или аналогичных инструментов требуется, чтобы на целевом компьютере был установлен Python и необходимые зависимости. Nuitka позволяет компилировать Python-код в бинарный исполняемый файл, исключая необходимость в виртуальном окружении. Это повышает производительность и упрощает развёртывание.

Кроме того, Nuitka обеспечивает лучшую защиту от реверс-инжиниринга по сравнению с PyInstaller и cx_Freeze, так как код транслируется в C, а не просто упаковывается в архив с интерпретатором. Это делает его более устойчивым к декомпиляции и анализу. Альтернативным способом защиты может быть использование Cython, который компилирует Python-код в C-расширения, обеспечивая дополнительную защиту, но требует наличия виртуального окружения на целевой системе.

В статье разберём процесс сборки Python-приложения в бинарник через Nuitka, возникшие сложности и их решения.

Nuitka: как это работает

Nuitka транслирует Python-код в C и компилирует через компиляторы (GCC, Clang, MSVC), создавая исполняемый файл, не требующий интерпретатора Python. В отличие от PyInstaller и cx_Freeze, Nuitka выполняет настоящую компиляцию, а не просто упаковку кода. Итоговый бинарник содержит скомпилированные модули и может работать автономно, обеспечивая лучшую производительность, защиту от реверс-инжиниринга и уменьшение зависимости от среды выполнения.

Установка и сборка

Моё приложение разработано с использованием PySide6, так как Nuitka не очень хорошо работает с PyQt. В программе активно используются многопоточность (threading) и SVG-файлы, что накладывает дополнительные требования к сборке.

Использование виртуального окружения и версия python

Для работы использую venv, так как это стандартный инструмент для управления зависимостями в Python. Сам Python 3.12.

Установка Nuitka и зависимостей:

pip install nuitka
brew install clang

или

brew install gcc

Базовая сборка на Windows:

Флаги сборки:

  • --standalone — создаёт полностью независимый бинарник, включающий все зависимости.

  • --enable-plugin=pyside6 — включает поддержку PySide6.

  • --windows-console-mode=disable — отключает консольное окно при запуске GUI-приложения.

  • --include-package=имя_пакета — вручную включает указанный пакет в сборку.

  • --include-data-dir=абсолютный_путь=относительный_путь — включает файлы и папки в сборку, где слева указывается полный путь, а справа — путь внутри бинарника.

  • --onefile — упаковывает весь бинарник и зависимости в один исполняемый файл.

  • --onedir — создаёт папку с бинарником и всеми зависимостями отдельно.

Пример:

python -m nuitka 
    --standalone \
    --onedir \
    --enable-plugin=pyside6 \
    --windows-console-mode=disable \
    --include-package=Мой класс \
    --include-data-dir="папка/папка внутри/данные=папка внутри/папка" \
    --include-data-dir="папка/данные=данные" \
    --include-data-dir="данные=данные" \
    --windows-icon-from-ico="папка/иконка.ico" \
    --output-dir="папка/куда будем выводить файл" \
    --output-file="Название приложения.exe" \
    "путь/Исполняемый код.py"

Сборка на macOS

Флаги сборки для macOS:

  • --standalone — создаёт автономный бинарник с включёнными зависимостями.

  • --enable-plugin=pyside6 — поддержка PySide6.

  • --onefile — создаёт единый исполняемый файл.

  • --onedir — создаёт папку с бинарником и всеми зависимостями отдельно.

  • --macos-app-mode — формирует .app-пакет для macOS.

  • --macos-create-app-bundle — создаёт полноценный macOS-совместимый .app-бандл.

  • --force-stderr-spec=файл.log — перенаправляет стандартный поток ошибок в указанный файл.

  • --force-stdout-spec=файл.log — перенаправляет стандартный поток вывода в указанный файл.

Пример команды сборки на macOS:

python -m nuitka \
    --standalone \
    --onedir \
    --enable-plugin=pyside6 \
    --macos-app-mode \
    --macos-create-app-bundle \
    --include-package=Мой_класс \
    --include-data-dir="папка/данные=данные" \
    --output-dir="папка/куда будем выводить файл" \
    --macos-app-icon="иконка.icns" \
    --output-file="Название_приложения.app" \
    --force-stderr-spec="error.log" \
    --force-stdout-spec="output.log" \
    "путь/Исполняемый_код.py"

Итоговый результат

После успешной сборки с Nuitka мы получаем полностью автономный бинарник, который можно запускать без установленного Python и дополнительных зависимостей. В зависимости от флагов (--onefile или --onedir), это либо один исполняемый файл, либо папка с бинарником и всеми необходимыми библиотеками.

На Windows получаем две папки: dist и build. В dist находится итоговый исполняемый файл (если используется --onedir).

На macOS я рекомендую использовать только --onedir, далее объясню почему. В результате сборки получаем аналогичные папки dist и build, а также готовое .app-приложение.

После успешной сборки с Nuitka мы получаем полностью автономный бинарник, который можно запускать без установленного Python и дополнительных зависимостей. В зависимости от флагов (--onefile или --onedir), это либо один исполняемый файл, либо папка с бинарником и всеми необходимыми библиотеками.

На Windows это .exe-файл, а на macOS — .app-пакет.

Проблемы и их решения

1. Nuitka не поддерживает PyQt, рекомендуется использовать PySide

PyQt не всегда корректно компилируется в бинарник через Nuitka, поэтому рекомендуется использовать PySide6. Nuitka имеет встроенный плагин для PySide6, который упрощает сборку и уменьшает вероятность ошибок.

2. Проблема с путями к данным в собранном приложении

После сборки бинарника пути к данным могут отличаться от исходных. Для правильной работы с ресурсами можно использовать следующую функцию, которая корректно определяет путь к файлам как в исходном коде, так и в собранном приложении:

import os
import sys

def resource_path(relative_path):
    """Возвращает абсолютный путь к ресурсу, учитывая режим упаковки и macOS .app."""
    if sys.platform == "darwin":  # macOS
        base_path = os.path.abspath(
            os.path.join(os.path.dirname(sys.executable), "..", "..", "..") 
            # здесь можно добавить или убрать ".." в зависимости от глубины вложенности
        )
    else:  # Windows или Linux
        if getattr(sys, 'frozen', False):
            base_path = os.path.dirname(sys.executable)
        else:
            base_path = os.path.dirname(os.path.abspath(__file__))
    
    return os.path.join(base_path, relative_path)

Пример использования:

logo_label = QSvgWidget(resource_path("Logo.svg"))

3. Проблема вывода print в macOS .app

Если в приложении есть print-выводы в консоль, то при свёртке в .app на macOS программа не понимает, куда отправлять вывод, и крашится. Самое интересное, что если запускать .app через терминал командой open, программа работает корректно.

Решение: Использование флага --force-stdout-spec=файл.log перенаправляет вывод print в лог-файл, предотвращая падение программы:

--force-stdout-spec=output.log

Заключение

Для завершённого вида на Windows создаём ярлык с путём до исполняемого файла.

На macOS используем команду для создания .dmg-образа:

hdiutil create -fs HFS+ -srcfolder "папка вывода/исполняемый файл" -volname "Название" "Название.dmg"

После этого получаем законченное приложение (особенно на MacOS). Поэтому я и рекомендовал на MacOS делать onedir, ведь при сборе в .dmg все файлы укомплектуются в .app.

В этой статье разобраны основные проблемы, возникающие при сборке Python-приложений в бинарники с помощью Nuitka. Это не полный список возможных сложностей, и если я столкнусь с новыми проблемами, обязательно расскажу о них в следующей статье.

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.