Наверняка, каждый, кто хоть раз писал что-то на Python, задумывался о том, как распространять свою программу (или, пусть даже, простой скрипт) без лишней головной боли: без необходимости устанавливать сам интерпретатор, различные зависимости, кроссплатформенно, чтобы одним файлом-exe'шником (на крайний случай, архивом) и минимально возможного размера.
Для этой цели существует немало инструментов: PyInstaller, cx_Freeze, py2exe, py2app, Nuitka и многие другие… Но что, если вы используете в своей программе PyQt? Несмотря на то, что многие (если не все) из выше перечисленных инструментов умеют упаковывать программы, использующие PyQt, существует другой инструмент от разработчиков самого PyQt под названием pyqtdeploy. К моему несчастью, я не смог найти ни одного вменяемого гайда по симу чуду, ни на русском, ни на английском.
Данная статья не претендует на всеобъемлющее описание pyqtdeploy и работы с ним, но, в конце концов, всегда приятно иметь все в одном месте, не так ли?
Так что же такое pyqtdeploy? В первом приближении, то же самое, что и выше перечисленные программы. Все ваши модули (стандартная библиотека, PyQt, все прочие модули) упаковываются средствами Qt (используется утилита rcc) в так называемый файл ресурсов, генерируется обертка вокруг питоновского интерпретатора на C++, позволяющая получать доступ ко все вашим модулям, и потом все это пакуется/компилируется/… в исполняемый файл. Для работы самого pyqtdeploy нужны Python 3.5+ и PyQt5. Перечислим несколько особенностей (за подробностями сюда и сюда):
Установка pyqtdeploy
Как уже было сказано выше, у нас должен быть установлен Python 3.5+ и PyQt5:
pip install PyQt5 pyqtdeploy
Сборка нашего exe'шника состоит из нескольких этапов:
Структура программы
Возьмем в качестве примера проект со следующей структурой: main.py — "точка входа" для нашей программы, она вызывает mainwindow.py — допустим, отрисовывает окошечко с виджетами и берет из resources иконку icon.png и mainwindow.ui, сгенерированный нами с помощью Qt Designer. Имеющиеся зависимости, версии библиотек и прочие необходимые вещи будут всплывать по ходу повествования:
main.py src/ |---__init__.py |---gui/ |---mainwindow.py |---__init__.py |---resources/ |---__init__.py |---images/ |---icon.png |---__init__.py |---ui/ |---mainwindow.ui |---__init__.py
Обзор плагинов sysroot (документация)
Как уже было сказано ранее, на этом этапе мы собираем все необходимые части, которые затем будут использоваться при генерации исполняемого файла. Данный процесс осуществляется с использованием конфигурационного файла sysroot.json (в принципе, вы можете назвать его как хотите и указать затем путь к нему). Он состоит из блоков, каждый из которых описывает сборку отдельного компонента (Python, Qt и т.д.). В pyqtdeploy реализован API, позволяющий вам написать свой плагин, управляющий сборкой необходимой вам библиотеки/модуля/whatever, если он еще не реализован разработчиками pyqtdeploy. Давайте пробежимся по стандартным плагинам и их параметрам (примеры из документации):
openssl (не обязательный) — позволяет собирать из исходников или использовать установленную в системе библиотеку (подробности). Компонент, описывающий данный плагин в sysroot.json, выглядит следующим образом:
"android|macos|win#openssl": { "android#source": "openssl-1.0.2r.tar.gz", "macos|win#source": "openssl-1.1.0j.tar.gz", "win#no_asm": true }
Первое, на что следует обратить внимание, это синтаксис:
Параметры:
zlib (не обязательный) — используется при сборке других компонентов (если не указан, по идее, будет использоваться тот, что установлен в системе) (подробности):
"ios|linux|macos|win#zlib": { "source": "zlib-1.2.11.tar.gz", "static_msvc_runtime": true }
Параметры:
qt5 (обязательный) — тут понятно (подробности):
"qt5": { "android-32#qt_dir": "android_armv7", "android-64#qt_dir": "android_arm64_v8a", "ios#qt_dir": "ios", "linux|macos|win#source": "qt-everywhere-src-5.12.2.tar.xz", "edition": "opensource", "android|linux#ssl": "openssl-runtime", "ios#ssl": "securetransport", "macos|win#ssl": "openssl-linked", "configure_options": [ "-opengl", "desktop", "-no-dbus", "-qt-pcre" ], "skip": [ "qtactiveqt", "qtconnectivity", "qtdoc", "qtgamepad", ... ], "static_msvc_runtime": true }
Параметры:
python (обязательный) — тут тоже понятно (подробности):
"python": { "build_host_from_source": false, "build_target_from_source": true, "source": "Python-3.7.2.tar.xz" }
Параметры:
sip (обязательный) — компонент, отвечающий за автоматическое генерирование Python-bindings для C/C++ библиотек (подробности тут и тут):
"sip": { "module_name": "PyQt5.sip", "source": "sip-4.19.15.tar.gz" }
Параметры:
pyqt5 (обязательный) — тут тоже понятно (подробности):
"pyqt5": { "android#disabled_features": [ "PyQt_Desktop_OpenGL", "PyQt_Printer", "PyQt_PrintDialog", "PyQt_PrintPreviewDialog", "PyQt_PrintPreviewWidget" ], "android#modules": [ "QtCore", "QtGui", "QtNetwork", "QtPrintSupport", "QtWidgets", "QtAndroidExtras" ], "ios#disabled_features": [ "PyQt_Desktop_OpenGL", "PyQt_MacOSXOnly", ... ], "ios|macos#modules": [ "QtCore", "QtGui", "QtNetwork", "QtPrintSupport", "QtWidgets", "QtMacExtras" ], "linux#modules": [ "QtCore", "QtGui", "QtNetwork", "QtPrintSupport", "QtWidgets", "QtX11Extras" ], "win#disabled_features": ["PyQt_Desktop_OpenGL"], "win#modules": [ "QtCore", "QtGui", "QtNetwork", "QtPrintSupport", "QtWidgets", "QtWinExtras" ], "source": "PyQt5_*-5.12.1.tar.gz" }
Параметры:
pyqt3D, pyqtchart, pyqtdatavisualization, pyqtpurchasing, qscintilla (не обязательные) — дополнительные модули, не входящие в состав PyQt. Имеют единственный параметр
Стоит заметить, что некоторые значения параметров могут не работать друг с другом. В таких случаях вы получите ошибку при сборке sysroot с информацией, что не так. Я постарался здесь описать такие случаи, по крайней мере, для обязательных компонентов.
Собираем sysroot
Давайте взглянем на итоговый sysroot.json для нашей программы:
{ "linux#zlib": { "source": "zlib-1.2.11.tar.gz" }, "linux#qt5": { "source": "qt-everywhere-src-5.12.2.tar", "edition": "opensource", "configure_options": [ "-no-dbus", "-no-system-proxies", "-no-cups", "-no-sql-db2", "-no-sql-ibase", "-no-sql-mysql", "-no-sql-sqlite", "-no-sql-sqlite2", "-no-sql-oci", "-no-sql-odbc", "-no-sql-psql", "-no-sql-tds", "-no-sqlite", "-ccache", "-optimize-size" ], "skip": [ "qt3d", "qtactiveqt", "qtandroidextras", "qtcanvas3d", "qtcharts", "qtconnectivity", "qtdatavis3d", "qtdeclarative", "qtdoc", "qtgamepad", "qtgraphicaleffects", "qtlocation", "qtmacextras", "qtmultimedia", "qtnetworkauth", "qtpurchasing", "qtquickcontrols", "qtquickcontrols2", "qtremoteobjects", "qtscript", "qtscxml", "qtsensors", "qtserialbus", "qtserialport", "qtspeech", "qtsvg", "qttools", "qttranslations", "qtvirtualkeyboard", "qtwayland", "qtwebchannel", "qtwebengine", "qtwebglplugin", "qtwebsockets", "qtwebview", "qtwinextras", "qtx11extras", "qtxmlpatterns" ], "disabled_features": [ "network", "bearermanagement", "dnslookup", "dtls", "ftp", "http", "localserver", "networkdiskcache", "networkinterface", "networkproxy", "socks5", "udpsocket", "concurrent", "future", "cups", "printer", "printdialog", "printpreviewdialog", "printpreviewwidget", "sql", "sqlmodel", "testlib", "xml" ] }, "linux#python": { "build_host_from_source": false, "build_target_from_source": true, "source": "Python-3.7.2.tgz", "dynamic_loading": true }, "linux#sip": { "module_name": "PyQt5.sip", "source": "sip-4.19.15.tar.gz" }, "linux#pyqt5": { "modules": ["QtCore", "QtGui", "QtWidgets"], "source": "PyQt5_*-5.12.2.tar.gz" } }
Что интересного мы тут видим? Во-первых, не используется ряд компонентов(например, ssl, pyqt3D и прочие). Во-вторых, собирать наш exe'шник мы будет под linux (а точнее, linux-64; в нашем случае, можно не указывать перед каждым компонентом платформу).
Далее, в qt5 по-максимуму выключены модули и функции, которые не будут использоваться (те, о назначении которых у меня было хотя бы минимальное представление). Среди top-level директорий собирается только QtBase. Особо упомяну опции
В pyqt5 собираем только модули QtCore, QtGui, QtWidgets.
В python включен
Прежде чем приступить к сборке sysroot, не забываем скачать все необходимые исходники: zlib, Qt5, Python, sip, PyQt5 и кладем их в папочку с sysroot.json (можно и любую другую, указав потом путь к ней). Запускаем сборку:
pyqtdeploy-sysroot sysroot.json
Данная команда имеет еще несколько опций, которые можно посмотреть здесь.
Крайне рекомендую также использовать опцию
Ну и запаситесь попкорном, ибо, в зависимости от мощности вашего калькулятора компьютера, это может занять немалое время.
Создаем "проектный" файл (документация)
Как только у нас все удачно собралось, приступаем к выбору модулей, которые мы хотим запаковать в exe'шник. Для этого в pyqtdeploy есть удобная утилита с GUI. Запускаем (имя .pdy файла может быть любым):
pyqtdeploy main.pdy
Application Source. В первой вкладке мы видим следующие настройки:
Еще один момент: любой файл с расширением .py будет "заморожен" (будет сгенерирован байт-код) — в ряде случаев это может быть нежелательным.
Кнопки справа:
qmake. Так как в сборке участвует qmake, здесь можно добавить дополнительные параметры для него (я не использовал);
PyQt Modules. На этой вкладке выделяем все PyQt-модули, которые мы явно импортируем в нашей программе. Если они зависят от других модулей, те выделятся автоматически. В нашем случае использовались QtCore, QtGui, QtWidgets, uic; sip подхватился автоматом. Если планируется использовать уже установленный PyQt, а не привязывать статически его к нашему исполняемому файлу, ничего не выделяем (такой сценарий не тестировался).
Standard Library. Здесь тот же подход, что и в предыдущем пункте, только для стандартной библиотеки. Если у вас в программе явно импортируется какой-то модуль, ставим галку. Если выделенным нами модулям (или самому интерпретатору) нужны другие модули, они выделятся автоматом (квадратики).
Правда это не всегда работает. Если поставили какой-то пакет со стороны (через тот же pip), и он импортирует что-то из стандартной библиотеки (еще не выделенное), вы получите при запуске
Python использует ряд модулей/пакетов (например, ssl), которым для работы нужны внешние библиотеки. Если мы хотим их статически привязать, то мы настраиваем это дело справа. В INCLUDEPATH указываем путь к заголовочным файлам (headers), в LIBS — путь к этой либе (мной не использовались, так что подробности смотрим в доках).
Other Packages. На этой вкладке выбираем необходимые нам сторонние пакеты (например, установленные из pypi). Подход тот же, что и в Application source: кликаем дважды на пустой строке, выбираем папку (в нашем случае, site-packages используемого при разработке virtual environment), жмем Scan и выбираем нужные пакеты/модули (у нас это PIL).
Other Extension Modules. Тут мы настраиваем модули расширения на C, которые хотим СТАТИЧЕСКИ привязать к exe'шнику (сторонние; те, что в стандартной библиотеке, привязываются сами).
Мы может настроить как компиляцию с нуля этих самых расширений, так и привязку уже скомпилированных. Второе делается довольно просто. Допустим у нас есть пакет Package со статической либой Lib.a, то в поле Name указываем полное имя расширения, используемое во время импорта — Package.Lib (без расширения .a); затем в поле LIBS указываем путь к этому расширению, например, -L/home/user1/venv/programme1/lib/python3.7/site-packages/Package -lLib (это специальный формат, также можно указать путь "по старинке", /home/user1/venv/programme1/lib/python3.7/site-packages/Package/Lib.a).
С компиляцией я не разбирался, но советую почитать, во-первых, про эту вкладку в доках, во-вторых, про qmake (там гораздо подробнее описаны опции, чем в pyqt'шных доках).
А что, если у нас динамическая либа, например, Lib.so? Еще проще — переименовываем ее в Package.Lib.so (т.е. все то же полное имя расширения, используемое во время импорта + расширение) и кладем его рядом с нашим exe'шником. Все должно подхватится, если это простое расширение без всяких зависимостей. В противном случае, ждите опять кучу
Locations. Тут тоже подробно не останавливаемся, за описанием отдельных путей сюда. Если вы действовали в соответствии с этой статьей (собранный sysroot лежит тут же, рядом с main.pdy), тут менять ничего не надо.
Собираем exe'шник (документация)
Наконец-таки собираем наш исполняемый файл:
pyqtdeploy-build main.pdy cd build-linux-64 ../sysroot-linux-64/host/bin/qmake make #nmake для win
Гипотетически, все должно собраться, на деле — доки и гугл вам в помощь.
Лирическое отступление #1 — меняем поведение программы в зависимости от того, "заморожено" оно или нет
Если вам нужно определить, запущена ваша программа как есть или из собранного exe'шника, используется тот же подход, что и в PyInstaller:
if getattr(sys, 'frozen', False): # запустили из exe'шника else: # запустили не из exe'шника
Лирическое отступление #2 — использование ресурсов (изображения, иконки и пр.)
У Qt имеется специальная "система ресурсов", которая позволяет с помощью утилиты rcc упаковать любые бинарные файлы в exe'шник. Далее с помощью пути специального формата вы можете получить доступ к необходимому ресурсу. В нашем проекте файл с иконкой icon.png расположен в src/resources/images, тогда путь в "системе ресурсов" будет выглядеть так — :/src/resources/images/icon.png. Как видите, ничего хитрого. Однако с таким путем есть одна засада — его понимают только Qt'шные функции. Т.е. если вы напишите у себя в программе что-нибудь в духе:
icon = QIcon(':/src/resources/images/icon.png')
Все будет в порядке. Но если, например, так:
icon_file = open(':/src/resources/images/icon.png', 'rb') icon = icon_file.read()
Ничего не выйдет, ибо
Если вам нужно читать запакованные ресурсы не только средствами Qt (например, вы, как и я, создавали GUI с помощью Qt Designer и получили файл .ui, который потом надо прочитать с помощью
ui_file = QtCore.QFile(':/src/resources/images/icon.png') ui_file.open(QtCore.QIODevice.ReadOnly) data = ui_file.readAll() ui_file.close() ui_file = BytesIO(bytes(data))
Итоги
Стоит ли так сильно заморачиваться, если вам нужен exe'шник, и старые добрые дедовские способы распространения программы вам по каким-то причинам не подходят? Если вы не используете PyQt, то, на мой взгляд, точно не стоит. Используйте что-нибудь более дружелюбное (тот же PyInstaller). Если хотите выжать максимум соков из вашего файла — дерзайте. В конечном счете мне таки удалось уменьшить размер файла до ~40 МБ (c
Когда у нас собрана минимально необходимая Qt и PyQt, было бы неплохо попробовать сделать на их основе exe'шник с помощью PyInstaller или cx_Freeze и посмотреть на размер, но это, как говорится, уже другая история...
| |
| |
Просмотров: 213 | |