pyqtdeploy, или упаковываем Python-программу в exe’шник… the hard way

в 9:17, , рубрики: cx_freeze, freeze, py2app, py2exe, pyinstaller, pyqt, pyqtdeploy, python, qt, исполняемый файл

КДПВ

Наверняка, каждый, кто хоть раз писал что-то на Python, задумывался о том, как распространять свою программу (или, пусть даже, простой скрипт) без лишней головной боли: без необходимости устанавливать сам интерпретатор, различные зависимости, кроссплатформенно, чтобы одним файлом-exe'шником (на крайний случай, архивом) и минимально возможного размера.

Для этой цели существует немало инструментов: PyInstaller, cx_Freeze, py2exe, py2app, Nuitka и многие другие… Но что, если вы используете в своей программе PyQt? Несмотря на то, что многие (если не все) из выше перечисленных инструментов умеют упаковывать программы, использующие PyQt, существует другой инструмент от разработчиков самого PyQt под названием pyqtdeploy. К моему несчастью, я не смог найти ни одного вменяемого гайда по симу чуду, ни на русском, ни на английском. На хабре и вовсе, если верить поиску, есть всего одно упоминание, и то — в комментариях (из него я и узнал про эту утилиту). К сожалению, официальная документация написана довольно поверхностно: не указан ряд опций, которые можно использовать во время сборки, для выяснения которых мне пришлось лезть в исходники, не описан ряд тонкостей, с которыми мне пришлось столкнуться.

Данная статья не претендует на всеобъемлющее описание pyqtdeploy и работы с ним, но, в конце концов, всегда приятно иметь все в одном месте, не так ли?

Все началось с того, что мне захотелось один мой проект запихнуть в исполняемый файл со всеми зависимостями (вы и сами уже догадались). Сначала я решил попробовать провернуть эту операцию с помощью PyInstaller — шикарный инструмент, простой, хорошо документированный. Но на выходе я получил папку размером 170 МБ (для сравнения, весь PyQt5 весил около 180 МБ). Поковырявшись в собранных либах, я понял, что используемые мной модули — QtCore, QtGui, QtWidgets — тащат с собой почти весь пакет. Попытки поиграться с опцией --exclude-module не увенчались успехом. Справедливости ради, если использовать опцию --onefile и включить сжатие, то получится файл размером 60 МБ, что все равно много. К тому же, во время запуска происходит разархивирование программы во временную папку, что увеличивает время старта и все равно (пусть и где-то там) отжирает все те же 170 МБ.

Тут мне подвернулся pyqtdeploy. "Утилита от самих разработчиков PyQt… Ну уж они-то должны знать, как по-максимуму отвязаться от лишних зависимостей внутри PyQt и Qt?" — подумал я и взялся плотненько за сей агрегат.

Так что же такое pyqtdeploy? В первом приближении, то же самое, что и выше перечисленные программы. Все ваши модули (стандартная библиотека, PyQt, все прочие модули) упаковываются средствами Qt (используется утилита rcc) в так называемый файл ресурсов, генерируется обертка вокруг питоновского интерпретатора на C++, позволяющая получать доступ ко все вашим модулям, и потом все это пакуется/компилируется/… в исполняемый файл. Для работы самого pyqtdeploy нужны Python 3.5+ и PyQt5. Перечислим несколько особенностей (за подробностями сюда и сюда):

  • может собирать exe'шники на основе PyQt4 и PyQt5, Python 2.7 и Python 3.3+ (максимальная поддерживаемая версия на данный момент Python 3.7.2);
  • позволяет статически (все пихаем в exe'шник) и динамически привязывать зависимости (использовать уже установленные в системе библиотеки, пакеты — с рядом ограничений);
  • поддерживаемые платформы:
    • android-32;
    • android-64;
    • ios-64;
    • linux-64;
    • macos-64;
    • win-32;
    • win-64;
  • также позволяет собирать несвязанные с PyQt и Qt программы, но из-за тесной интеграции с QtCore, будет тянуть оттуда кое-что в качестве зависимостей.

Установка pyqtdeploy

Как уже было сказано выше, у нас должен быть установлен Python 3.5+ и PyQt5:

pip install PyQt5 pyqtdeploy

Сборка нашего exe'шника состоит из нескольких этапов:

  • Разработка нашей Python-программы, как обычно (сюрприз!);
  • Сборка так называемого sysroot для нашей платформы, где будут лежать собранные из исходников нужные зависимости;
  • Создание "проектного" файла с расширением .pdy, где будет вся необходимая информация для сборки нашего exe'шника (пути к собранным Qt, PyQt, Python, прочим библиотекам и модулям и другие опции);
  • Собственно сборка exe'шника с помощью qmake.

Структура программы

Возьмем в качестве примера проект со следующей структурой: 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
}

Первое, на что следует обратить внимание, это синтаксис: arch1|arch2|...#plugin-name. То есть мы можем выбрать, на какой платформе использовать этот плагин (ios, android, macos, win, linux), а на какой — нет. Более того, этот синтаксис применим и к параметрам внутри блока.

Параметры:

  • source (обязательный) — имя архива с исходниками;
  • no_asm (не обязательный) — выключаем ассемблерные оптимизации. Если включен, в PATH должен быть установлен nasm;
  • python_source (не обязательный) — имя архива, содержащего патчи, необходимые для сборки OpenSSL под macOS для Python v3.6.4 и более ранних версий;

zlib (не обязательный) — используется при сборке других компонентов (если не указан, по идее, будет использоваться тот, что установлен в системе) (подробности):

"ios|linux|macos|win#zlib": {
    "source": "zlib-1.2.11.tar.gz",

    "static_msvc_runtime": true
}

Параметры:

  • source (обязательный) — очевидно, имя архива с исходниками;
  • static_msvc_runtime (не обязательный) — статически привязать MSVC библиотеки (Windows);

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
}

Параметры:

  • qt_dir (не обязательный, если указан source) — путь к папке с установленным Qt;
  • source (не обязательный, если указан qt_dir) — имя архива с исходниками Qt;
  • edition (обязательный, если указан source) — один из 2 вариантов:
    • commercial;
    • opensource;
  • ssl — 3 возможных варианта:
    • openssl-linked — будет собран из исходников (подробности должны быть указаны в описании компонента openssl);
    • securetransport — используется SSL, реализованный в Qt (который, в свою очередь, будет использовать Apple’s Secure Transport);
    • openssl-runtime — используется версия OpenSSL, установленная в системе;
  • configure_options — дополнительные опции, используемые при сборке Qt. Существует их целая прорва, смотрим тут;
  • skip — позволяет исключить из сборки ненужные модули (точнее говоря, top-level директории, содержащие модули). Открываем архив с исходниками Qt и видим папки, начинающиеся с qt — это и есть top-level директории. Имейте в виду, что эти папки могут содержать и те модули, что вам нужны. К сожалению, можно скипнуть только top-level директорию целиком (подробности);
  • disabled_features — позволяет исключить выбранный функционал. Для просмотра всех возможных фич можно воспользоваться командой configure -list-features (подробности)
  • static_msvc_runtime (не обязательный) — статически привязать MSVC библиотеки (Windows);

python (обязательный) — тут тоже понятно (подробности):

"python": {
    "build_host_from_source": false,
    "build_target_from_source": true,
    "source": "Python-3.7.2.tar.xz"
}

Параметры:

  • build_host_from_source (обязательный) — true — собираем Python для хоста из исходников, false — используем установленный Python (не поддерживается для win32);
  • build_target_from_source (обязательный) — true — собираем Python для целевой платформы из исходников, false — используем установленный Python (использование установленного Python поддерживается только на win32);
  • source (обязательный, если Python собирается из исходников) — имя архива с исходниками Python;
  • version (обязательный, если используется установленный Python) — версия установленного Python;
  • dynamic_loading (не обязательный) — true — включить поддержку динамической загрузки модулей расширения (тех, что на C);
  • host_installation_bin_dir (не обязательный) — путь к установленному Python, если не собирается из исходников (если не указан, на win ищется в реестре автоматически, на других платформах — в PATH);

sip (обязательный) — компонент, отвечающий за автоматическое генерирование Python-bindings для C/C++ библиотек (подробности тут и тут):

"sip": {
    "module_name": "PyQt5.sip",
    "source": "sip-4.19.15.tar.gz"
}

Параметры:

  • module_name (обязательный) — имя sip-модуля;
  • source (обязательный) — имя архива с исходниками sip;

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"
}

Параметры:

  • disabled_features (не обязательный) — позволяет выключить конкретный функционал. Если не указан, выключаемые фичи определяются автоматически на основе фич, выключенных в собранном нами Qt (подробности);
  • modules (обязательный) — перечисляем модули, которые мы хотим собрать (подробности);
  • source (обязательный) — имя архива с исходниками PyQt;

pyqt3D, pyqtchart, pyqtdatavisualization, pyqtpurchasing, qscintilla (не обязательные) — дополнительные модули, не входящие в состав PyQt. Имеют единственный параметр source — имя архива с исходниками.

Стоит заметить, что некоторые значения параметров могут не работать друг с другом. В таких случаях вы получите ошибку при сборке 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. Особо упомяну опции -optimize-size и -ccache. Первая позволяет уменьшить размер собранного Qt и, соответственно, итогового файла (у меня получилось минус 5 МБ), но увеличится время компиляции, вторая — использовать ccache (по крайней мере, на linux), что при повторных компиляциях СУЩЕСТВЕННО уменьшает время (у меня уменьшилось раз в 5). Никакой настройки не требует, просто ставим командой apt install ccache.

В pyqt5 собираем только модули QtCore, QtGui, QtWidgets.

В python включен dynamic_loading, так как мы хотим позднее динамически прилинковать C-extension.

Прежде чем приступить к сборке sysroot, не забываем скачать все необходимые исходники: zlib, Qt5, Python, sip, PyQt5 и кладем их в папочку с sysroot.json (можно и любую другую, указав потом путь к ней). Запускаем сборку:

pyqtdeploy-sysroot sysroot.json

Данная команда имеет еще несколько опций, которые можно посмотреть здесь.

Крайне рекомендую также использовать опцию --verbose. Будьте готовы к тому, что вы получите целую кучу ошибок, прежде чем все удачно соберется. Многие из них будут связаны с тем, что у вас не установлены dev-пакеты. Я их здесь не перечисляю, ибо они зависят от вашей конфигурации и платформы. Наверняка, вам нужен будет python3-dev, также смотрим тут (особенно, разделы Requirements). Правда, вам никто не запрещает использовать для тех же Qt и Python уже установленные версии (я не пробовал, возможны свои подводные камни).

Ну и запаситесь попкорном, ибо, в зависимости от мощности вашего калькулятора компьютера, это может занять немалое время.

Создаем "проектный" файл (документация)

Как только у нас все удачно собралось, приступаем к выбору модулей, которые мы хотим запаковать в exe'шник. Для этого в pyqtdeploy есть удобная утилита с GUI. Запускаем (имя .pdy файла может быть любым):

pyqtdeploy main.pdy

Application Source

Application Source. В первой вкладке мы видим следующие настройки:

  • Name — имя вашего будущего exe'шника;
  • Main script file (не указывается, если используется Entry Point) — скрипт, используемый для запуска программы (в нашем случае, main.py);
  • Entry Point (не указывается, если используется Main script file) — точка входа для программы, основанной на setuptools;
  • sys.path — используется для указания дополнительных директорий, zip-файлов и яиц (тех, что Python egg), которые будут добавлены в sys.path (я не использовал, смотрим доки, там подробно описана эта опция);
  • Target Python version — версия Python;
  • Target PyQt version — PyQt4 или PyQt5 (игнорируется, если вы мазохист и решили собрать программу, не использующую PyQt, этим монстром);
  • Use console — выбрать, если приложение должно использовать консоль (только Windows). Может быть полезно для дебага;
  • Application bundle — выбрать, если приложение должно быть собрано как bundle (только MacOS);
  • Application Package Directory — содержит все файлы, составляющие вашу программу. Для добавления жмем кнопку Scan… У нас папка со всеми "кишками" (src) отделена от main.py, так что выбираем эту папку и галочками выделяем все файлы, которые мы хотим включить в итоговый файл. Если же у вас нет такого разделения (т.е. main.py находится внутри src), то напротив main.py галочку нужно снять (или напротив вашего аналога, указанного в Main script file).

Еще один момент: любой файл с расширением .py будет "заморожен" (будет сгенерирован байт-код) — в ряде случаев это может быть нежелательным.

Кнопки справа:

  • Scan… — добавляем файлы в Application Package Directory;
  • Remove all — очищаем Application Package Directory;
  • Include all — выделяем все файлы в Application Package Directory;
  • Exclude all — снимаем выделение со всех файлов в Application Package Directory;
  • Exclusions — паттерны, позволяющие исключить файлы из Application Package Directory. Дважды кликаем на пустой строке для добавления;

qmake. Так как в сборке участвует qmake, здесь можно добавить дополнительные параметры для него (я не использовал);

PyQt Modules

PyQt Modules. На этой вкладке выделяем все PyQt-модули, которые мы явно импортируем в нашей программе. Если они зависят от других модулей, те выделятся автоматически. В нашем случае использовались QtCore, QtGui, QtWidgets, uic; sip подхватился автоматом.
Если планируется использовать уже установленный PyQt, а не привязывать статически его к нашему исполняемому файлу, ничего не выделяем (такой сценарий не тестировался).

Standard Library

Standard Library. Здесь тот же подход, что и в предыдущем пункте, только для стандартной библиотеки. Если у вас в программе явно импортируется какой-то модуль, ставим галку. Если выделенным нами модулям (или самому интерпретатору) нужны другие модули, они выделятся автоматом (квадратики).

Правда это не всегда работает. Если поставили какой-то пакет со стороны (через тот же pip), и он импортирует что-то из стандартной библиотеки (еще не выделенное), вы получите при запуске ImportError. Так что вам придется вернуться сюда и поставить галочку. Например, я использую библиотеку PIL, и одному из модулей нужны была библиотека fractions.

Python использует ряд модулей/пакетов (например, ssl), которым для работы нужны внешние библиотеки. Если мы хотим их статически привязать, то мы настраиваем это дело справа. В INCLUDEPATH указываем путь к заголовочным файлам (headers), в LIBS — путь к этой либе (мной не использовались, так что подробности смотрим в доках).

Other Packages

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'шником. Все должно подхватится, если это простое расширение без всяких зависимостей. В противном случае, ждите опять кучу ImportError. Мне, например, так и не удалось прикрутить _imaging.so, используемый PIL'ом.

Locations. Тут тоже подробно не останавливаемся, за описанием отдельных путей сюда. Если вы действовали в соответствии с этой статьей (собранный sysroot лежит тут же, рядом с main.pdy), тут менять ничего не надо.

Собираем exe'шник (документация)

Наконец-таки собираем наш исполняемый файл:

pyqtdeploy-build main.pdy
cd build-linux-64
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()

Ничего не выйдет, ибо open будет пытаться найти такой путь в вашей файловой системе и, естественно, ничего не найдет.

Если вам нужно читать запакованные ресурсы не только средствами Qt (например, вы, как и я, создавали GUI с помощью Qt Designer и получили файл .ui, который потом надо прочитать с помощью loadUi), нужно будет сделать как-то так:

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 -optimize-size ~35 МБ), что все-равно больше, чем хотелось бы.

Когда у нас собрана минимально необходимая Qt и PyQt, было бы неплохо попробовать сделать на их основе exe'шник с помощью PyInstaller или cx_Freeze и посмотреть на размер, но это, как говорится, уже другая история...

Автор: Zalmancheg

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js