Как стать автором
Обновить
26.99
СВД ВС
Надежность. Безопасность. Реальное время

Методика портирования пакетов Python в операционную систему «Нейтрино»

Уровень сложностиСредний
Время на прочтение11 мин
Количество просмотров1.8K

Многие расширения (модули) Python поставляются в виде платформонезависимого байт-кода и могут быть использованы в системах с любой архитектурой. Однако, в некоторых случаях расширения поставляются в виде Py-исходников лишь частично. Например, часть внутренних функций может быть реализована на Си и для обеспечения работоспособности всего расширения потребуется их предкомпиляция для каждой требуемой архитектуры. В контексте ОС «Нейтрино» перечень последних достаточно широк.

В статье рассмотрим общий подход к портированию Python-расширений в нашу ОС. Для примера возьмем NumPy, чей жизненный путь проходит следующие стадии: нативный Pyhton код →трансляция в Си (Cython) → компиляция → запаковка результатов с wrapper-ами для Python.


Подготовка окружения для cross-сборки

В состав Комплекта Разработчика (КР) наряду с runtime-компонентами самого Python входит сборочное окружения для cross-компиляции расширений. С его помощью можно выполнять как автоматизированную подготовку пакетов, так и ручную с необходимым уровнем кастомизации.

Сразу стоит отметить, что поддержка cross-сборки расширений Python осуществляется только для инструментальных систем на базе дистрибутивов Linux. Для отдельных процедур может потребоваться подключение к сети.

Во избежание проблем с совместимостью инструментальные средства (КР) включают версию Python, которая будет доступна и в runtime-окружении. Также на инструментальной системе должен быть установлен Python-модуль crossenv, однако, это не является обязательным условием, так как впоследствии он может быть загружен автоматически (см. далее).

Перво-наперво следует воспользоваться скриптом python-setup-crossenv.sh, который произведёт настройку сборочного окружения. Ключевыми параметрами здесь являются опция -t, задающая целевую архитектуру, и каталог, в котором будут развёрнуты платформо-зависимые компоненты. Так, после выполнения команды

$ python-setup-crossenv.sh

в текущей директории будет создан каталог ./venv/ и в нем будут созданы независимые окружения для всех поддерживаемых архитектур. Лог его выполнения позволяет получить представление о составе окружения:

Лог исполнения скрипта python-setup-crossenv.sh
$ python-setup-crossenv.sh
Creating virtual Python environment in /home/a-n-d/venv...
Building cross environment for python3.9 (x86)... done!
Patching /home/a-n-d/venv/x86/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/x86/bin/activate" command to enable cross environment for x86 platform.
Building cross environment for python3.9 (armle-v7)... done!
Patching /home/a-n-d/venv/armle-v7/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/armle-v7/bin/activate" command to enable cross environment for armle-v7 platform.
Building cross environment for python3.9 (ppcbe)... done!
Patching /home/a-n-d/venv/ppcbe/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/ppcbe/bin/activate" command to enable cross environment for ppcbe platform.
Building cross environment for python3.9 (ppcbe-spe)... done!
Patching /home/a-n-d/venv/ppcbe-spe/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/ppcbe-spe/bin/activate" command to enable cross environment for ppcbe-spe platform.
Building cross environment for python3.9 (mipsbe)... WARNING: CC is a compound command (['mips-unknown-nto-qnx6.5.0-gcc', '-EB'])
WARNING: This can cause issues for modules that don't expect it.
WARNING: Consider setting CC='mips-unknown-nto-qnx6.5.0-gcc' and CFLAGS='-EB'
WARNING: CXX is a compound command (['mips-unknown-nto-qnx6.5.0-g++', '-EB'])
WARNING: This can cause issues for modules that don't expect it.
WARNING: Consider setting CXX='mips-unknown-nto-qnx6.5.0-g++' and CXXFLAGS='-EB'
done!
Patching /home/a-n-d/venv/mipsbe/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/mipsbe/bin/activate" command to enable cross environment for mipsbe platform.
Building cross environment for python3.9 (mipsle)... WARNING: CC is a compound command (['mips-unknown-nto-qnx6.5.0-gcc', '-EL'])
WARNING: This can cause issues for modules that don't expect it.
WARNING: Consider setting CC='mips-unknown-nto-qnx6.5.0-gcc' and CFLAGS='-EL'
WARNING: CXX is a compound command (['mips-unknown-nto-qnx6.5.0-g++', '-EL'])
WARNING: This can cause issues for modules that don't expect it.
WARNING: Consider setting CXX='mips-unknown-nto-qnx6.5.0-g++' and CXXFLAGS='-EL'
done!
Patching /home/a-n-d/venv/mipsle/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/mipsle/bin/activate" command to enable cross environment for mipsle platform.
Building cross environment for python3.9 (e2kle)... done!
Patching /home/a-n-d/venv/e2kle/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/e2kle/bin/activate" command to enable cross environment for e2kle platform.

Стоит отметить, что при запуске cross-окружения, становится доступна специализированная версия интерпретатора Python, именно в которой должна производиться работа с портируемыми пакетами:

$ . ./venv/x86/bin/activate
(cross) $ cross-python3 ...

Для удаления артефактов сборки в последующем будет достаточно удалить сами каталоги ./venv/, ./cache/ и репозитории проектов. При успешном завершении этого шага можно переходить к сборке расширений и их пакетов (к вопросу о яйцах и колёсах).

Автоматизированный режим установки расширений

Попробуем спортировать достаточно простое расширение, не требующее патчей в исходники или предкомпиляции. Их есть в достатке, например, human_math. Запускаем cross-окружение для соответствующей архитектуры и пробуем установить пакет "из коробки":

$ . ./venv/x86/bin/activate 
(cross) $ cross-python3 -m pip install human_math
Collecting human_math
  Downloading human_math-0.1-py3-none-any.whl (13 kB)
Installing collected packages: human-math
Successfully installed human-math-0.1

При успешном завершении в каталоге ./venv/x86/cross/lib/python3.9/site-packages/ появятся каталоги human_math/ и human_math-0.1.dist-info/. Один из вариантов дальнейших действий — руками затащить эти каталоги на таргет в эквивалентную директорию /usr/lib/python3.9/site-packages. Альтернативно, и, наверное, более правильно, их можно запаковать в архив, эквивалентный оригиналу:

$ cd venv/x86/cross/lib/python3.9/site-packages
$ zip -r human_math-0.1-py3-none-any.whl human_math*

И установить его через пакетный менеджер, предварительно накатив pip по его штатной инструкции. В конечном счете это позволит проверить пакет по ванильным рекомендациям авторов:

$ uname -a
KPDA EAea7e68 20.09 2020/09/03-14:45:00-MSK x86pc x86
$
$ python -m ensurepip --upgrade
...
Installing collected packages: setuptools, pip
Successfully installed pip-20.2.3 setuptools-49.2.1
$
$ python3 -m pip install ./human_math-0.1-py3-none-any.whl
Processing ./human_math-0.1-py3-none-any.whl
Installing collected packages: human-math
Successfully installed human-math-0.1
$
$ python3
Python 3.9.0 (default, Sep  3 2020, 14:45:00) 
[GCC 4.8.3]
Type "help", "copyright", "credits" or "license" for more information.
>>> import human_math as hm
>>> tree = hm.parse("2 - (-sin(3pi/2)) - 3.0")
>>> tree
((2 - (-1 * sin(3 * (pi / 2)))) - 3)
>>> tree.evaluate()
-2

При анализе приведённых логов можно заметить, что непосредственно cross-сборки в данном случае не наблюдается. Для рандомного пакета это не всегда заведомо известно и стоит сначала проверить эмпирически (на самом деле информация о расширении может быть найдена на сайте разработчика и при анализе заявленных языков в каталоге пакетов, но это не всегда точно). Фактически, при установленном pip, в данном случае всю рассмотренную процедуру можно производить прямо на таргете. Очевидно, что для этого пакета также нет зависимости от архитектуры и проводить повторное портирование для всех вариантов не нужно ‒ о чем нам намекает постфикс -none-any в имени пакета.

Рассмотрим второй пример, в котором компиляция гарантировано требуется. Возьмем, к примеру, расширение для расчета расстояния Левенштейна. Заявлено, что оно написано на Си и Python. Порядок действий точно такой же, разве что добавим вызову pip install вербозности, чтобы увидеть свидетельства вызова компилятора:

Фрагмент лога сборки расширения polyleven
$ python-setup-crossenv.sh -tx86
Creating virtual Python environment in /home/a-n-d/venv...
Building cross environment for python3.9 (x86)... done!
Patching /home/a-n-d/venv/x86/cross/bin/python3.9... done!
Use ". /home/a-n-d/venv/x86/bin/activate" command to enable cross environment for x86 platform.
$
$ . ./venv/x86/bin/activate
(cross) $
(cross) $ cross-python3 -m pip install -v polyleven
Using pip 20.2.3 from /home/a-n-d/venv/x86/build/lib/python3.9/site-packages/pip (python 3.9)
Non-user install because user site-packages disabled
Created temporary directory: /tmp/pip-ephem-wheel-cache-fg_fuga_
Created temporary directory: /tmp/pip-req-tracker-f3sa3exq
Initialized build tracking at /tmp/pip-req-tracker-f3sa3exq
Created build tracker: /tmp/pip-req-tracker-f3sa3exq
Entered build tracker: /tmp/pip-req-tracker-f3sa3exq
Created temporary directory: /tmp/pip-install-kfgre1ru
1 location(s) to search for versions of polyleven:
* https://pypi.org/simple/polyleven/
Fetching project page and analyzing links:
...
Given no hashes to check 6 links for project 'polyleven': discarding no candidates
Using version 0.8 (newest of versions: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8)
Collecting polyleven
  Created temporary directory: /tmp/pip-unpack-mg3leorj
  Starting new HTTPS connection (1): files.pythonhosted.org:443
  https://files.pythonhosted.org:443 "GET ... polyleven-0.8.tar.gz HTTP/1.1" 200 6373
  Downloading polyleven-0.8.tar.gz (6.4 kB)
  Added polyleven from https://...
    Running setup.py (path:/tmp/pip-install-kfgre1ru/polyleven/setup.py) egg_info for package polyleven
    Created temporary directory: /tmp/pip-pip-egg-info-kpfd_tqt
    Running command python setup.py egg_info
    running egg_info
    creating /tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info
    writing /tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info/PKG-INFO
    writing dependency_links to /tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info/dependency_links.txt
    writing top-level names to /tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info/top_level.txt
    writing manifest file '/tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info/SOURCES.txt'
    reading manifest file '/tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info/SOURCES.txt'
    reading manifest template 'MANIFEST.in'
    warning: no files found matching 'README.md'
    writing manifest file '/tmp/pip-pip-egg-info-kpfd_tqt/polyleven.egg-info/SOURCES.txt'
  Source in /tmp/pip-install-kfgre1ru/polyleven has version 0.8, which satisfies requirement polyleven from https://...
  Removed polyleven from https://...
Using legacy 'setup.py install' for polyleven, since package 'wheel' is not installed.
Installing collected packages: polyleven
  Created temporary directory: /tmp/pip-record-m5pksfmh
    Running command /home/a-n-d/venv/x86/cross/bin/python3 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-kfgre1ru/polyleven/setup.py'"'"'; __file__='"'"'/tmp/pip-install-kfgre1ru/polyleven/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /tmp/pip-record-m5pksfmh/install-record.txt --single-version-externally-managed --compile --install-headers /home/a-n-d/venv/x86/cross/include/site/python3.9/polyleven
    running install
    running build
    running build_ext
    building 'polyleven' extension
    creating build
    creating build/temp.qnx--i486-3.9
    i486-pc-nto-qnx6.5.0-gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -O3 -Wall -fno-aggressive-loop-optimizations -fPIC -D_QNX_SOURCE -std=c99 -DNDEBUG -g3 -I. -I. -I. -I. -I. -I. -I. -I. -I. -DBUILDENV_qss -D__KPDA__ -D__KPDANTO__ -I/home/a-n-d/venv/x86/cross/include -I/opt/kpda2020/target/neutrino/x86/usr/include/python3.9 -c polyleven.c -o build/temp.qnx--i486-3.9/polyleven.o
    creating build/lib.qnx--i486-3.9
    i486-pc-nto-qnx6.5.0-gcc -shared -L. -L. -L. -L. -L. build/temp.qnx--i486-3.9/polyleven.o -o build/lib.qnx--i486-3.9/polyleven.so
    running install_lib
    copying build/lib.qnx--i486-3.9/polyleven.so -> /home/a-n-d/venv/x86/cross/lib/python3.9/site-packages
    running install_egg_info
    running egg_info
    writing polyleven.egg-info/PKG-INFO
    writing dependency_links to polyleven.egg-info/dependency_links.txt
    writing top-level names to polyleven.egg-info/top_level.txt
    reading manifest file 'polyleven.egg-info/SOURCES.txt'
    reading manifest template 'MANIFEST.in'
    warning: no files found matching 'README.md'
    writing manifest file 'polyleven.egg-info/SOURCES.txt'
    Copying polyleven.egg-info to /home/a-n-d/venv/x86/cross/lib/python3.9/site-packages/polyleven-0.8-py3.9.egg-info
    running install_scripts
    writing list of installed files to '/tmp/pip-record-m5pksfmh/install-record.txt'
    Running setup.py install for polyleven ... done
Successfully installed polyleven-0.8

Сборка также прошла в автоматизированном режиме, но, в отличие от первого примера, потребовала полноценной предкомпиляции (см. в логе -gcc), однако, можно заметить предупреждение об отсутствии пакета wheel. Если установить его, то также будут созданы материалы для формирования колеса:

$ cd venv/x86/cross/lib/python3.9/site-packages
$ zip -r polyleven-0.8-py3-none-any.whl polyleven*

В конечном счете следует проверить работоспособность расширения на таргете по соответствующей методике:

$ python3 -m pip install ./polyleven-0.8-py3-none-any.whl
Processing ./polyleven-0.8-py3-none-any.whl
Installing collected packages: polyleven
Successfully installed polyleven-0.8
$
$ python3
Python 3.9.0 (default, Sep  3 2020, 14:45:00) 
[GCC 4.8.3]
Type "help", "copyright", "credits" or "license" for more information.
>>> from polyleven import levenshtein
>>> levenshtein('aaa', 'ccc')
3
>>> levenshtein('deadbeef', 'coffee')
6

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

Но не все расширения настолько автономны.

Ручной режим сборки и портирование исходников

В некоторых случаях сборка может потребовать более тщательного подхода. Так, всем известный NumPy, при сборке проходит более интересные стадии: нативный Pyhton код →трансляция части исходников в Си (Cython) → компиляция (GCC) → запаковка результатов с wrapper-ами для Python.

В первую очередь скачиваем официальный репозиторий проекта и переключаемся на стабильную версию (в нашем случае на ту, которая проверялась на совместимость с дистрибутивом ОС соответствующей редакции):

$ git clone https://github.com/numpy/numpy.git
$ cd numpy/
$ git checkout v1.20.2

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

(cross) $ cross-python3 setup.py build -j16 install --prefix /home/a-n-d/install/
Running from numpy source directory.
...
ModuleNotFoundError: No module named 'Cython'

Бешеной собаке, как говорится... Забегая немного вперёд, в этом случае понадобится не только Cython, но и ещё один пакет (если он не был установлен при воспроизведении предыдущего подхода).

(cross) $ cross-pip3 install wheel
(cross) $ cross-pip3 install Cython
(cross) $ cross-python3 setup.py build -j16 install --prefix /home/a-n-d/install/
...

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

numpy/distutils/ccompiler_opt.py:
@ -894,3 +894,3 @@ class _CCompiler(object):
            ("cc_on_x64",      ".*(x|x86_|amd)64.*"),
---            ("cc_on_x86",      ".*(win32|x86|i386|i686).*"),
+++            ("cc_on_x86",      ".*(win32|x86|i386|i486|i686).*"),
            ("cc_on_ppc64le",  ".*(powerpc|ppc)64(el|le).*"),

Вся сборка в 16 потоков на Ryzen 7 5700G / HDD занимает около полутора минут. По факту создаётся яйцо. Поскольку данный формат пакетов считается устаревшим, при желании, с помощью следующей команды, можно собрать и колесо:

(cross) $ cross-python3 setup.py sdist bdist_wheel

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

$ python3 -m pip install ./numpy-1.20.2-py3-none-any.whl
Processing ./numpy-1.20.2-py3-none-any.whl
Installing collected packages: numpy
Successfully installed numpy-1.20.2
$
$ python3
Python 3.9.0 (default, Sep  3 2020, 14:45:00) 
[GCC 4.8.3]
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> x = np.arange(15, dtype=np.int64).reshape(3, 5)
>>> x[1:, ::2] = -99
>>> x
array([[  0,   1,   2,   3,   4],
       [-99,   6, -99,   8, -99],
       [-99,  11, -99,  13, -99]], dtype=int64)
>>> x.max(axis=1)
array([ 4,  8, 13], dtype=int64)
>>> rng = np.random.default_rng()
>>> samples = rng.normal(size=2500)
>>> samples
array([ 1.32379234,  0.29264934, -1.03925619, ..., -0.68672435,
       -1.55879728,  1.21000793])

В некоторых пакетах авторы документируют что нужно предпринять для успешного портирования в новое окружение. Примером такого расширения является, например, Redlibssh2 (см разделы "System library build" и "Custom Compiler Configuration"). Но это делают лишь добросовестные разработчики. В достаточно крупных проектах, к которым относится и NumPy, обычно с этим не заморачиваются. А фикс исходников и/или правил сборки как раз и составляют основную трудоемкость при портировании.

P.S. При установке через pip на таргете может потребоваться изменить последний компонент в имени колеса на any, поскольку штатный скрипт setup.py проекта может неправильно назвать формируемый пакет.

P.P.S. В процессе подготовки статьи нейросетью сгенерирован ряд иллюстраций, а выкидывать жалко:

Серпентарий

Теги:
Хабы:
Всего голосов 9: ↑9 и ↓0+9
Комментарии4

Публикации

Информация

Сайт
www.kpda.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
Игорь Дерябин