Недавно возникла необходимость предоставить нашему QA-отделу один из модулей на Python в виде автономного бинарика, который не требовал бы установки и настройки окружения. Следуя за необходимостью был сформирован интерес к существующим для этого средства.
Один из вариантов был использовать Docker, но я от него отказался по причине того, что окружение для Docker тоже надо будет готовить. Потом надо будет правильно запуск этот образ и правильно с ним взаимодействовать. Конечно, для упрощения можно использовать docker compose, но это не сильно снижает сложность для конечного пользователя. Кроме того, образ будет достаточно большим.
Поэтому я после некоторых размышлений обратился к таким инструментам как Python Compilers, а именно — Nuitka и PyInstaller и провёл небольшое исследование на предмет их пригодности для моих нужд.
Оба инструмента упаковывают Python-приложение со всеми зависимостями в один пакет таким образом, что конечный пользователь приложения может обойтись без установки Python на свою машину.
Есть два варианта того, что мы получаем от их работы в качестве результата (кроме эмоциональных ощущений):
-
Python-приложение представлено одним каталогом с бинариком для запуска и всеми зависимостями в виде отдельных файлов
-
Python-приложение и все его зависимости упакованы в один бинарь
PyInstaller
Эксперимент проводился на версии 6.10.0
% pyinstaller -version 6.10.0
Приложение в каталоге
% time pyinstaller generator/main.py pyinstaller generator/main.py 18.63s user 2.72s system 95% cpu 22.376 total
На выходе получили два каталога — build и dist
% du -sh build dist 56M build 75M dist
% ls -l build/main dist/main/* -rwxr-xr-x 1 max staff 17177744 Aug 14 12:00 dist/main/main build/main: total 113656 -rw-r--r-- 1 max staff 999486 Aug 14 12:00 Analysis-00.toc -rw-r--r-- 1 max staff 562354 Aug 14 12:00 COLLECT-00.toc -rw-r--r-- 1 max staff 2974 Aug 14 12:00 EXE-00.toc -rw-r--r-- 1 max staff 2780 Aug 14 12:00 PKG-00.toc -rw-r--r-- 1 max staff 16906333 Aug 14 12:00 PYZ-00.pyz -rw-r--r-- 1 max staff 435304 Aug 14 12:00 PYZ-00.toc -rw-r--r-- 1 max staff 1443565 Aug 14 11:59 base_library.zip drwxr-xr-x 6 max staff 192 Aug 14 12:00 localpycs -rwxr-xr-x 1 max staff 17177744 Aug 14 12:00 main -rw-r--r-- 1 max staff 16937430 Aug 14 12:00 main.pkg -rw-r--r-- 1 max staff 17259 Aug 14 12:00 warn-main.txt -rw-r--r-- 1 max staff 3680264 Aug 14 12:00 xref-main.html dist/main/_internal: total 15976 drwxr-xr-x 7 max staff 224 Aug 14 12:00 IPython drwxr-xr-x 9 max staff 288 Aug 14 12:00 PIL lrwxr-xr-x 1 max staff 37 Aug 14 12:00 Python -> Python.framework/Versions/3.11/Python drwxr-xr-x 5 max staff 160 Aug 14 12:00 Python.framework -rwxr-xr-x 1 max staff 234176 Aug 14 12:00 _cffi_backend.cpython-311-darwin.so -rw-r--r-- 1 max staff 1443565 Aug 14 12:00 base_library.zip drwxr-xr-x 3 max staff 96 Aug 14 12:00 cryptography drwxr-xr-x 10 max staff 320 Aug 14 12:00 cryptography-41.0.1.dist-info drwxr-xr-x 10 max staff 320 Aug 14 12:00 email_validator-2.2.0.dist-info drwxr-xr-x 9 max staff 288 Aug 14 12:00 factory_boy-3.3.0.dist-info drwxr-xr-x 3 max staff 96 Aug 14 12:00 faker drwxr-xr-x 3 max staff 96 Aug 14 12:00 jedi drwxr-xr-x 60 max staff 1920 Aug 14 12:00 lib-dynload drwxr-xr-x 7 max staff 224 Aug 14 12:00 lib2to3 lrwxr-xr-x 1 max staff 30 Aug 14 12:00 libXau.6.0.0.dylib -> PIL/.dylibs/libXau.6.0.0.dylib lrwxr-xr-x 1 max staff 39 Aug 14 12:00 libbrotlicommon.1.1.0.dylib -> PIL/.dylibs/libbrotlicommon.1.1.0.dylib lrwxr-xr-x 1 max staff 36 Aug 14 12:00 libbrotlidec.1.1.0.dylib -> PIL/.dylibs/libbrotlidec.1.1.0.dylib -rwxr-xr-x 1 max staff 4222928 Aug 14 12:00 libcrypto.3.dylib lrwxr-xr-x 1 max staff 31 Aug 14 12:00 libfreetype.6.dylib -> PIL/.dylibs/libfreetype.6.dylib lrwxr-xr-x 1 max staff 31 Aug 14 12:00 libharfbuzz.0.dylib -> PIL/.dylibs/libharfbuzz.0.dylib lrwxr-xr-x 1 max staff 32 Aug 14 12:00 libjpeg.62.4.0.dylib -> PIL/.dylibs/libjpeg.62.4.0.dylib lrwxr-xr-x 1 max staff 28 Aug 14 12:00 liblcms2.2.dylib -> PIL/.dylibs/liblcms2.2.dylib lrwxr-xr-x 1 max staff 27 Aug 14 12:00 liblzma.5.dylib -> PIL/.dylibs/liblzma.5.dylib -rwxr-xr-x 1 max staff 189360 Aug 14 12:00 libmpdec.4.dylib lrwxr-xr-x 1 max staff 34 Aug 14 12:00 libopenjp2.2.5.2.dylib -> PIL/.dylibs/libopenjp2.2.5.2.dylib lrwxr-xr-x 1 max staff 29 Aug 14 12:00 libpng16.16.dylib -> PIL/.dylibs/libpng16.16.dylib lrwxr-xr-x 1 max staff 31 Aug 14 12:00 libsharpyuv.0.dylib -> PIL/.dylibs/libsharpyuv.0.dylib -rwxr-xr-x 1 max staff 1240816 Aug 14 12:00 libsqlite3.0.dylib -rwxr-xr-x 1 max staff 838736 Aug 14 12:00 libssl.3.dylib lrwxr-xr-x 1 max staff 27 Aug 14 12:00 libtiff.6.dylib -> PIL/.dylibs/libtiff.6.dylib lrwxr-xr-x 1 max staff 27 Aug 14 12:00 libwebp.7.dylib -> PIL/.dylibs/libwebp.7.dylib lrwxr-xr-x 1 max staff 32 Aug 14 12:00 libwebpdemux.2.dylib -> PIL/.dylibs/libwebpdemux.2.dylib lrwxr-xr-x 1 max staff 30 Aug 14 12:00 libwebpmux.3.dylib -> PIL/.dylibs/libwebpmux.3.dylib lrwxr-xr-x 1 max staff 30 Aug 14 12:00 libxcb.1.1.0.dylib -> PIL/.dylibs/libxcb.1.1.0.dylib lrwxr-xr-x 1 max staff 28 Aug 14 12:00 libz.1.3.1.dylib -> PIL/.dylibs/libz.1.3.1.dylib drwxr-xr-x 3 max staff 96 Aug 14 12:00 markupsafe drwxr-xr-x 3 max staff 96 Aug 14 12:00 ossl-modules drwxr-xr-x 4 max staff 128 Aug 14 12:00 parso drwxr-xr-x 3 max staff 96 Aug 14 12:00 pydantic_core drwxr-xr-x 3 max staff 96 Aug 14 12:00 setuptools drwxr-xr-x 3 max staff 96 Aug 14 12:00 text_unidecode drwxr-xr-x 9 max staff 288 Aug 14 12:00 typeguard-4.3.0.dist-info drwxr-xr-x 9 max staff 288 Aug 14 12:00 wheel-0.43.0.dist-info
Как видно в каталог собраны все зависимости приложения и кроме того сам интерпретатор Python. Все они, включая Python, представлены разделяемыми библиотеками.
% file dist.folder/main/_internal/Python.framework/Versions/3.11/Python dist.folder/main/_internal/Python.framework/Versions/3.11/Python: Mach-O 64-bit dynamically linked shared library arm64
Во время запуска бутлоадер устанавливает переменные окружения, хэндлеры сигналов и т.д. и после этого стартует дочерний процесс. Дочерний процесс, в свою очередь, — это интерпретатор Python, который и начинает выполнение нашего приложения.
Подробности: https://pyinstaller.org/en/stable/advanced-topics.html#the-bootstrap-process-in-detail
Приложение в одном файле
В таком варианте бутлоадер распаковывает содержимое бинарика во временный каталог, из которого затем запускается само приложение. По окончании работы временный каталог удаляется. В остальном процесс выглядит идентично запуска приложения в каталоге.
По этой причине приложение в едином файле занимает больше времени для запуска, чем приложение в виде каталога
% time pyinstaller --onefile generator/main.py pyinstaller --onefile generator/main.py 22.72s user 2.49s system 96% cpu 26.063 total
% du -sh build dist 56M build 33M dist
% ls -l build/main dist/main -rwxr-xr-x 1 max staff 34381088 Aug 15 08:45 dist/main build/main: total 114872 -rw-r--r-- 1 max staff 1000578 Aug 15 08:45 Analysis-00.toc -rw-r--r-- 1 max staff 564818 Aug 15 08:45 EXE-00.toc -rw-r--r-- 1 max staff 564630 Aug 15 08:45 PKG-00.toc -rw-r--r-- 1 max staff 17070667 Aug 15 08:45 PYZ-00.pyz -rw-r--r-- 1 max staff 436396 Aug 15 08:45 PYZ-00.toc -rw-r--r-- 1 max staff 1443565 Aug 15 08:45 base_library.zip drwxr-xr-x 6 max staff 192 Aug 15 08:45 localpycs -rw-r--r-- 1 max staff 34007396 Aug 15 08:45 main.pkg -rw-r--r-- 1 max staff 18491 Aug 15 08:45 warn-main.txt -rw-r--r-- 1 max staff 3693075 Aug 15 08:45 xref-main.html
Здесь видно, что на выходе мы получили единый бинарный файл.
Запуск того, что получилось
% time dist/main/main --no-serve-files FileNotFoundError: [Errno 2] No such file or directory: '/Users/max/work/wectory/qa-automation/dist/main/_internal/generator/data.json' [PYI-4129:ERROR] Failed to execute script 'main' due to unhandled exception! dist/main/main --no-serve-files 0.35s user 0.04s system 97% cpu 0.406 total
Судя по всему дистрибутив нужно доукомплектовать некоторыми файлами
% cp generator/data.json dist/main/_internal/generator/
Пробуем ещё раз и получаем
FileNotFoundError: [Errno 2] No such file or directory: '/Users/max/work/wectory/qa-automation/dist/main/_internal/generator/templates/default' [PYI-4343:ERROR] Failed to execute script 'main' due to unhandled exception!
Ну что ж, переносим и это
% cp -r generator/templates dist/main/_internal/generator
На этот раз всё прошло успешно:
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Landlord ┃ Katherine Francesca Mills (Company) ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ birthdate │ 30.05.1969 │ │ email │ developers+l240815084019@wectory.com │ │ phone │ +447181970103 │ │ address │ 3046 Powell Union Suite 769, North Rita, NH 65169 │ └───────────┴───────────────────────────────────────────────────┘ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Agent ┃ Chelsea Hazel Williams ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ birthdate │ 22.11.1988 │ │ email │ developers+a240815084019@wectory.com │ │ phone │ +447197971413 │ │ address │ 2310 Bolton Lodge Apt. 402, Jonesstad, WA 61453 │ └───────────┴─────────────────────────────────────────────────┘ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Agency ┃ Jones, Bradley and Murphy ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ email │ developers+a240815084019@wectory.com │ │ sort code │ 846683 │ │ account number │ 73977208 │ │ address │ 577 Hull Drives, Curtisberg, WY 87473 │ └────────────────┴───────────────────────────────────────┘ Results 1: /Users/max/work/wectory/qa-automation/dist/main/_internal/generator/generated/2024-08-15-08-40-19
В случае с запуском приложения в виде единого файлы мы видим те же ошибки
% time dist/main --no-serve-files FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/dw/vk1bw2wd18zdpgs2m8lfzw4r0000gn/T/_MEIzYrZXM/generator/data.json' [PYI-5126:ERROR] Failed to execute script 'main' due to unhandled exception! dist/main --no-serve-files 0.93s user 0.87s system 14% cpu 12.197 total
Надо отметить, что время запуска значительно увеличилось и не сильно меняется на повторных запусках, как было в случае с приложением в каталоге.
Но вернёмся к ошибке. Из-за того, что каждый раз создаётся временный каталог, то нет возможности положить куда-то нашу статику просто так. К счастью, разработчики это предусмотрели и позволили задать временный каталог несколькими способами: через переменную окружения либо через командную строку.
Разница в том, что через переменную окружения мы можем задать путь к временному каталогу во время выполнения нашего приложения, а командную строку можем использовать только на этапе сборки.
Попробуем с командной строкой:
% pyinstaller --runtime-tmpdir main.tmp --onefile generator/main.py
% dist/main --no-serve-files FileNotFoundError: [Errno 2] No such file or directory: '/Users/max/work/wectory/qa-automation/main.tmp/_MEIXIoz7Y/generator/data.json' [PYI-7163:ERROR] Failed to execute script 'main' due to unhandled exception!
Как можно заметить в качестве временного каталога действительно был использован заданный ранее main.tmp, но с нюансом — само приложение было распаковано в подкаталог _MEIXIoz7Y, который был автоматически удалён после завершения выполнения
% ls -l main.tmp total 0
Тоже самое, если указать петь через переменную окружения (пришлось пересобрать приложение без ключа --runtime-tmpdir). Путь должен быть создан заранее
% mkdir main.tmp2 % env TMPDIR=main.tmp2 dist/main --no-serve-files FileNotFoundError: [Errno 2] No such file or directory: '/Users/max/work/wectory/qa-automation/main.tmp2/_MEIbatKsa/generator/data.json' [PYI-7744:ERROR] Failed to execute script 'main' due to unhandled exception!
Получается, что простым способом нашу статику не получится куда-то поместить. Дальше мне было лень с этим разбираться.
А вывод отсюда такой: надо на этапе проектирования и разработки принять решение о том, где вы будете хранить статику.
Nuitka
Nuitka отличается от PyInstaller тем, что транспилирует код на Python в код на C и затем компилирует его в нативный запускаемый файл. Но чтобы сделать приложение полностью переносимым нужно использовать опцию --standalone, иначе приложение будет зависеть от библиотек, которые придётся устанавливать на целевой машине.
Версия
% nuitka --version 2.4.5 Commercial: None Python: 3.11.9 (main, Apr 2 2024, 08:25:04) [Clang 15.0.0 (clang-1500.3.9.4)] Flavor: Homebrew Python Executable: /Users/max/work/wectory/qa-automation/env/bin/python3.11 OS: Darwin Arch: arm64 macOSRelease: 14.6 Version C compiler: /usr/bin/clang (clang 15.0.0).
Приложение в виде каталога
% time nuitka --standalone generator nuitka --standalone generator 1616.68s user 210.46s system 473% cpu 6:26.05 total
Сборка выполнялась заметно дольше (даже ногам стало тепло в процессе), чем это делал PyInstaller. Но оно и понятно — в процессе происходить полноценная компиляция не только кода нашего приложения, но и всех зависимостей.
% du -sh generator.build generator.dist 442M generator.build 104M generator.dist
Не буду приводить здесь вывод ls -ltr generator.build generator.dist — он получается очень большой из-за количества модулей.
Приложение в одном файле
% time nuitka --standalone --onefile generator nuitka --standalone --onefile generator 301.93s user 106.49s system 202% cpu 3:21.68 total
Что получаем
% du -sh generator.build generator.dist generator.onefile-build generator.bin 440M generator.build 104M generator.dist 23M generator.onefile-build 23M generator.bin
Запуск того, что получилось
% time generator.dist/generator.bin --no-serve-files FileNotFoundError: [Errno 2] No such file or directory: '/Users/max/work/wectory/qa-automation/generator.dist/generator/data.json' generator.dist/generator.bin 0.23s user 0.03s system 97% cpu 0.274 total
И видим ошибку, которая похожа на ту, что мы видели с PyInstaller. Благо, понятно что делать
% cp -r generator/data.json generator/templates generator.dist/generator/
Попытка №2
% time generator.dist/generator.bin --no-serve-files
И неминуемый успех
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Landlord ┃ Carly Ashley Roberts (Individual) ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ birthdate │ 26.08.1988 │ │ email │ developers+l240815105459@wectory.com │ │ phone │ +447417436058 │ │ address │ 597 Allison Shoal, North James, TN 27518 │ └───────────┴──────────────────────────────────────────┘ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Agent ┃ Jenna Lorraine Humphreys ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ birthdate │ 17.05.1975 │ │ email │ developers+a240815105459@wectory.com │ │ phone │ +447934521633 │ │ address │ 551 Justin Light Apt. 663, Hessberg, HI 69859 │ └───────────┴───────────────────────────────────────────────┘ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Agency ┃ Smith Group ┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ email │ developers+a240815105459@wectory.com │ │ sort code │ 563919 │ │ account number │ 04717704 │ │ address │ 0237 Haley Mountain Suite 776, Meredithmouth, ME 27066 │ └────────────────┴────────────────────────────────────────────────────────┘ Results 1: /Users/max/work/wectory/qa-automation/generator.dist/generator/generated/2024-08-15-10-54-59 generator.dist/generator.bin --no-serve-files 1.18s user 0.33s system 94% cpu 1.590 total
Теперь попробуем однофайловый вариант
% time ./generator.bin FileNotFoundError: [Errno 2] No such file or directory: '/private/var/folders/dw/vk1bw2wd18zdpgs2m8lfzw4r0000gn/T/onefile_20785_1723701790_311895/generator/data.json' ./generator.bin 0.77s user 0.24s system 15% cpu 6.668 total
Проблема аналогичная той, что была с PyInstaller — статика ищется во временном каталоге. На этом пробы варианта с одним файлом я закончил, т.к. разбираться как это обойти мне было уже лень.
Вывод тот же: заранее решить, где будем хранить статику.
Заключение
Выводы по итогам знакомства с PyInstaller и Nuitka у меня получаются такие:
-
оба решения работают без специальных танцов с бубном, что хорошо. Правда, возникли сложности со статикой приложения, но это недоработка со стороны самого приложения, а не PyInstaller или Nuitka
-
на этапе разработки приложений обязательно нужно принять правильное решение, о том где будут храниться файлы с данными и прочая статика, и как будем их запаковывать или передавать пользователям
-
что касается скорости сборки, то PyInstaller в этом деле значительно обходит Nuitka. Оно и понятно — PyInstaller не перекомпилирует исходники всего приложения и его зависимостей, а только перекладывает в целевой каталог. Время сборки важно учитывать, когда мы строим пайплайны
-
по размеру получившихся каталогов/файлов PyInstaller тоже обходит Nuitka, хоть и не значительно. Возможно, на больших проектах эта разница уже не будет такой заметной
Для себя я бы выбрал Nuitka, т.к. есть подозрение (хотя я специально этого не проверял, но I Want to Believe), что скомпилированный код будет работать быстрее.
А какой инструмент выберете сегодня вы?
ссылка на оригинал статьи https://habr.com/ru/articles/838480/
Добавить комментарий