Как создать и распространять пакет Python/Cython, который зависит от стороннего libFoo.so

Я написал модуль Python, который зависит от некоторых расширений C. Эти расширения C, в свою очередь, зависят от нескольких скомпилированных библиотек C. Я хотел бы иметь возможность распространять этот модуль в комплекте со всеми зависимостями.

Я собрал минимальный пример (он может быть найден на GitHub полностью).

Структура каталогов:

$ tree .
.
├── README.md
├── poc
│   ├── __init__.py
│   ├── cython_extensions
│   │   ├── __init__.py
│   │   ├── cvRoberts_dns.c
│   │   ├── cvRoberts_dns.h
│   │   ├── helloworld.c
│   │   ├── helloworld.pxd
│   │   ├── helloworld.pyx
│   │   ├── test.c
│   │   └── test.h
│   ├── do_stuff.c
│   └── do_stuff.pyx
└── setup.py

setup.py строит расширения и связывает с необходимыми библиотеками (libsundials_cvode, libsundials_nvectorserial в этом случае):

from setuptools import setup, find_packages
from setuptools.extension import Extension
from Cython.Build import cythonize


ext_module_dostuff = Extension(
    'poc.do_stuff',
    ['poc/do_stuff.pyx'],
)

ext_module_helloworld = Extension(
    'poc.cython_extensions.helloworld',
    ['poc/cython_extensions/helloworld.pyx', 'poc/cython_extensions/test.c', 'poc/cython_extensions/cvRoberts_dns.c'],
    include_dirs = ['/usr/local/include'],
    libraries = ['m', 'sundials_cvodes', 'sundials_nvecserial'],
    library_dirs = ['/usr/local/lib'],
)

cython_ext_modules = [
   ext_module_dostuff,
   ext_module_helloworld
]


setup (
  name = "poc",
  ext_modules = cythonize(cython_ext_modules),
  packages=['poc', 'poc.cython_extensions'],
)

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

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

Учитывая различные учебники, примеры и сообщения SO, которые я нашел до сих пор. Мне повезло, что я на правильном пути. Однако есть какой-то заключительный шаг, который я просто не собираюсь.

Любая помощь приветствуется: -).

Ответ 1

Как вы, наверное, знаете, рекомендуемым способом распространения модуля Python с компилируемыми компонентами является использование формата wheel. Кажется, не существует какого-либо стандартного кросс-платформенного способа объединения сторонних библиотек в колесо. Однако для этой цели существуют инструменты для платформы.

В Linux используйте auditwheel.

auditwheel изменяет существующий файл колес Linux, чтобы добавить любые сторонние библиотеки, которые не включены в базовый " manylinux". Здесь вы найдете обзор того, как использовать его с вашим проектом на чистой установке Ubuntu 17.10:

Сначала установите базовые средства разработки Python и стороннюю библиотеку со своими заголовками:

[email protected]:~# apt-get install cython python-pip unzip
[email protected]:~# apt-get install libsundials-serial-dev

Затем создайте проект в файл колес:

[email protected]:~# cd cython-example/
[email protected]:~/cython-example# python setup.py bdist_wheel
[...]
[email protected]:~/cython-example# cd dist/
[email protected]:~/cython-example/dist# ll
total 80
drwxr-xr-x 2 root root  4096 Nov  8 11:28 ./
drwxr-xr-x 7 root root  4096 Nov  8 11:28 ../
-rw-r--r-- 1 root root 70135 Nov  8 11:28 poc-0.0.0-cp27-cp27mu-linux_x86_64.whl
[email protected]:~/cython-example/dist# unzip -l poc-0.0.0-cp27-cp27mu-linux_x86_64.whl
Archive:  poc-0.0.0-cp27-cp27mu-linux_x86_64.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
    62440  2017-11-08 11:28   poc/do_stuff.so
        2  2017-11-08 11:28   poc/__init__.py
   116648  2017-11-08 11:28   poc/cython_extensions/helloworld.so
        2  2017-11-08 11:28   poc/cython_extensions/__init__.py
       10  2017-11-08 11:28   poc-0.0.0.dist-info/DESCRIPTION.rst
      211  2017-11-08 11:28   poc-0.0.0.dist-info/metadata.json
        4  2017-11-08 11:28   poc-0.0.0.dist-info/top_level.txt
      105  2017-11-08 11:28   poc-0.0.0.dist-info/WHEEL
      167  2017-11-08 11:28   poc-0.0.0.dist-info/METADATA
      793  2017-11-08 11:28   poc-0.0.0.dist-info/RECORD
---------                     -------
   180382                     10 files

Теперь файл колеса можно установить локально и проверить:

[email protected]:~/cython-example/dist# pip install poc-0.0.0-cp27-cp27mu-linux_x86_64.whl
[...]
[email protected]:~/cython-example/dist# python -c "from poc.do_stuff import hello; hello()"
hello cython
0.841470984808
trying to load the sundials program

3-species kinetics problem

At t = 2.6391e-01      y =  9.899653e-01    3.470564e-05    1.000000e-02
    rootsfound[] =   0   1
At t = 4.0000e-01      y =  9.851641e-01    3.386242e-05    1.480205e-02
[...]

Теперь мы устанавливаем инструмент auditwheel. Он требует Python 3, но он способен обрабатывать колеса для Python 2 или 3.

[email protected]:~/cython-example/dist# apt-get install python3-pip
[email protected]:~/cython-example/dist# pip3 install auditwheel

auditwheel использует другой инструмент под названием patchelf для выполнения своей работы. К сожалению, в версии patchelf, включенной в Ubuntu 17.10, отсутствует исправление, без которого auditwheel не будет работать. Таким образом, нам придется построить его из источника (script, взятого из изображение манилайна Docker):

[email protected]:~# apt-get install autoconf
[email protected]:~# PATCHELF_VERSION=6bfcafbba8d89e44f9ac9582493b4f27d9d8c369
[email protected]:~# curl -sL -o patchelf.tar.gz https://github.com/NixOS/patchelf/archive/$PATCHELF_VERSION.tar.gz
[email protected]:~# tar -xzf patchelf.tar.gz
[email protected]:~# (cd patchelf-$PATCHELF_VERSION && ./bootstrap.sh && ./configure && make && make install)

Теперь мы можем проверить, какие библиотеки сторонних производителей требуют:

[email protected]:~/cython-example/dist# auditwheel show poc-0.0.0-cp27-cp27mu-linux_x86_64.whl

poc-0.0.0-cp27-cp27mu-linux_x86_64.whl is consistent with the
following platform tag: "linux_x86_64".

The wheel references external versioned symbols in these system-
provided shared libraries: libc.so.6 with versions {'GLIBC_2.4',
'GLIBC_2.2.5', 'GLIBC_2.3.4'}

The following external shared libraries are required by the wheel:
{
    "libblas.so.3": "/usr/lib/x86_64-linux-gnu/blas/libblas.so.3.7.1",
    "libc.so.6": "/lib/x86_64-linux-gnu/libc-2.26.so",
    "libgcc_s.so.1": "/lib/x86_64-linux-gnu/libgcc_s.so.1",
    "libgfortran.so.4": "/usr/lib/x86_64-linux-gnu/libgfortran.so.4.0.0",
    "liblapack.so.3": "/usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.7.1",
    "libm.so.6": "/lib/x86_64-linux-gnu/libm-2.26.so",
    "libpthread.so.0": "/lib/x86_64-linux-gnu/libpthread-2.26.so",
    "libquadmath.so.0": "/usr/lib/x86_64-linux-gnu/libquadmath.so.0.0.0",
    "libsundials_cvodes.so.2": "/usr/lib/libsundials_cvodes.so.2.0.0",
    "libsundials_nvecserial.so.0": "/usr/lib/libsundials_nvecserial.so.0.0.2"
}

In order to achieve the tag platform tag "manylinux1_x86_64" the
following shared library dependencies will need to be eliminated:

libblas.so.3, libgfortran.so.4, liblapack.so.3, libquadmath.so.0,
libsundials_cvodes.so.2, libsundials_nvecserial.so.0

И создайте новое колесо, которое связывает их:

[email protected]:~/cython-example/dist# auditwheel repair poc-0.0.0-cp27-cp27mu-linux_x86_64.whl
Repairing poc-0.0.0-cp27-cp27mu-linux_x86_64.whl
Grafting: /usr/lib/libsundials_nvecserial.so.0.0.2 -> poc/.libs/libsundials_nvecserial-42b4120e.so.0.0.2
Grafting: /usr/lib/libsundials_cvodes.so.2.0.0 -> poc/.libs/libsundials_cvodes-50fde5ee.so.2.0.0
Grafting: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.7.1 -> poc/.libs/liblapack-549933c4.so.3.7.1
Grafting: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.7.1 -> poc/.libs/libblas-52fa99c8.so.3.7.1
Grafting: /usr/lib/x86_64-linux-gnu/libgfortran.so.4.0.0 -> poc/.libs/libgfortran-2df4b07d.so.4.0.0
Grafting: /usr/lib/x86_64-linux-gnu/libquadmath.so.0.0.0 -> poc/.libs/libquadmath-0d7c3070.so.0.0.0
Setting RPATH: poc/cython_extensions/helloworld.so to "$ORIGIN/../.libs"
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp27-cp27mu-linux_x86_64
New WHEEL info tags: cp27-cp27mu-manylinux1_x86_64

Fixed-up wheel written to /root/cython-example/dist/wheelhouse/poc-0.0.0-cp27-cp27mu-manylinux1_x86_64.whl
[email protected]:~/cython-example/dist# unzip -l wheelhouse/poc-0.0.0-cp27-cp27mu-manylinux1_x86_64.whl
Archive:  wheelhouse/poc-0.0.0-cp27-cp27mu-manylinux1_x86_64.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
      167  2017-11-08 11:28   poc-0.0.0.dist-info/METADATA
        4  2017-11-08 11:28   poc-0.0.0.dist-info/top_level.txt
       10  2017-11-08 11:28   poc-0.0.0.dist-info/DESCRIPTION.rst
      211  2017-11-08 11:28   poc-0.0.0.dist-info/metadata.json
     1400  2017-11-08 12:08   poc-0.0.0.dist-info/RECORD
      110  2017-11-08 12:08   poc-0.0.0.dist-info/WHEEL
    62440  2017-11-08 11:28   poc/do_stuff.so
        2  2017-11-08 11:28   poc/__init__.py
   131712  2017-11-08 12:08   poc/cython_extensions/helloworld.so
        2  2017-11-08 11:28   poc/cython_extensions/__init__.py
   230744  2017-11-08 12:08   poc/.libs/libsundials_cvodes-50fde5ee.so.2.0.0
  7005072  2017-11-08 12:08   poc/.libs/liblapack-549933c4.so.3.7.1
   264024  2017-11-08 12:08   poc/.libs/libquadmath-0d7c3070.so.0.0.0
  2039960  2017-11-08 12:08   poc/.libs/libgfortran-2df4b07d.so.4.0.0
    17736  2017-11-08 12:08   poc/.libs/libsundials_nvecserial-42b4120e.so.0.0.2
   452432  2017-11-08 12:08   poc/.libs/libblas-52fa99c8.so.3.7.1
---------                     -------
 10206026                     16 files

Если мы удалим сторонние библиотеки, ранее установленное колесо перестанет работать:

[email protected]:~/cython-example/dist# apt-get remove libsundials-serial-dev && apt-get autoremove
[...]
[email protected]:~/cython-example/dist# python -c "from poc.do_stuff import hello; hello()"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "poc/do_stuff.pyx", line 1, in init poc.do_stuff
ImportError: libsundials_cvodes.so.2: cannot open shared object file: No such file or directory

Но колесо со связанными библиотеками будет работать нормально:

[email protected]:~/cython-example/dist# pip uninstall poc
[...]
[email protected]:~/cython-example/dist# pip install wheelhouse/poc-0.0.0-cp27-cp27mu-manylinux1_x86_64.whl
[...]
[email protected]:~/cython-example/dist# python -c "from poc.do_stuff import hello; hello()"
hello cython
0.841470984808
trying to load the sundials program

3-species kinetics problem

At t = 2.6391e-01      y =  9.899653e-01    3.470564e-05    1.000000e-02
    rootsfound[] =   0   1
At t = 4.0000e-01      y =  9.851641e-01    3.386242e-05    1.480205e-02
[...]

В OSX используйте delocate.

delocate для OSX, по-видимому, работает очень похоже на auditwheel. К сожалению, у меня нет операционной системы OSX, чтобы обеспечить прохождение игры.

Комбинированный пример:

Один проект, который использует оба инструмента, - SciPy. Этот репозиторий, несмотря на его название, содержит официальный процесс сборки SciPy для всех платформ, а не только для Mac. В частности, сравните сборку Linux script (которая использует auditwheel), с OSX build script (который использует delocate).

Чтобы увидеть результат этого процесса, вы можете загрузить и распаковать некоторые из колес SciPy от PyPI . Например, scipy-1.0.0-cp27-cp27m-manylinux1_x86_64.whl содержит следующее:

 38513408  2017-10-25 06:02   scipy/.libs/libopenblasp-r0-39a31c03.2.18.so
  1023960  2017-10-25 06:02   scipy/.libs/libgfortran-ed201abd.so.3.0.0

Пока scipy-1.0.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl содержит следующее:

   273072  2017-10-25 07:03   scipy/.dylibs/libgcc_s.1.dylib
  1550456  2017-10-25 07:03   scipy/.dylibs/libgfortran.3.dylib
   279932  2017-10-25 07:03   scipy/.dylibs/libquadmath.0.dylib

Ответ 2

Чтобы повысить mhsmith отличный ответ, вот шаги, выполняемые в MacOS с delocate

  • Установите sundials, например, с помощью Homebrew:

    $ brew install sundials
    
  • Создайте пакет:

    $ python setup.py bdist_wheel
    
  • Подвески auditwheel show/auditwheel repair равны delocate-listdeps/delocate-wheel, поэтому сначала проанализируйте полученный файл колес:

    $ delocate-listdeps --all dist/poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl
    /usr/lib/libSystem.B.dylib
    /usr/local/Cellar/sundials/2.7.0_3/lib/libsundials_cvodes.2.9.0.dylib
    /usr/local/Cellar/sundials/2.7.0_3/lib/libsundials_nvecserial.2.7.0.dylib
    
  • Фиксация файла колеса:

    $ delocate-wheel -v -w dist_fixed dist/poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl 
    Fixing: dist/poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl
    Copied to package .dylibs directory:
      /usr/local/Cellar/sundials/2.7.0_3/lib/libsundials_cvodes.2.9.0.dylib
      /usr/local/Cellar/sundials/2.7.0_3/lib/libsundials_nvecserial.2.7.0.dylib
    

В каталоге dist_fixed у вас будет комплектное колесо. Вы заметите разницу в размерах:

$ ls -l dist/ dist_fixed/
dist/:
total 72
-rw-r--r--  1 hoefling  wheel  36030 10 Nov 20:25 poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl

dist_fixed/:
total 240
-rw-r--r--  1 hoefling  wheel  120101 10 Nov 20:34 poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl

Если вы укажете deps для связанного колеса, вы заметите, что необходимые библиотеки теперь в комплекте (обозначается префиксом @loader_path):

$ delocate-listdeps --all dist_fixed/poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl 
/usr/lib/libSystem.B.dylib
@loader_path/../.dylibs/libsundials_cvodes.2.9.0.dylib
@loader_path/../.dylibs/libsundials_nvecserial.2.7.0.dylib

Установка входящего в комплект колеса (обратите внимание, что вложенные библиотеки установлены правильно):

$ pip install dist_fixed/poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl 
Processing ./dist_fixed/poc-0.0.0-cp27-cp27m-macosx_10_13_intel.whl
Installing collected packages: poc
Successfully installed poc-0.0.0
$ pip show -f poc
Name: poc
Version: 0.0.0
Summary: UNKNOWN
Home-page: UNKNOWN
Author: UNKNOWN
Author-email: UNKNOWN
License: UNKNOWN
Location: /Users/hoefling/.virtualenvs/stackoverflow-py27/lib/python2.7/site-packages
Requires: 
Files:
  poc-0.0.0.dist-info/DESCRIPTION.rst
  poc-0.0.0.dist-info/INSTALLER
  poc-0.0.0.dist-info/METADATA
  poc-0.0.0.dist-info/RECORD
  poc-0.0.0.dist-info/WHEEL
  poc-0.0.0.dist-info/metadata.json
  poc-0.0.0.dist-info/top_level.txt
  poc/.dylibs/libsundials_cvodes.2.9.0.dylib
  poc/.dylibs/libsundials_nvecserial.2.7.0.dylib
  poc/__init__.py
  poc/__init__.pyc
  poc/cython_extensions/__init__.py
  poc/cython_extensions/__init__.pyc
  poc/cython_extensions/helloworld.so
  poc/do_stuff.so

Ответ 3

Я бы предложил использовать совершенно другой подход. Настройте инфраструктуру управления пакетами Linux. В Ubuntu/Debian это можно сделать с помощью reprepro. https://wiki.ubuntuusers.de/reprepro/ может быть началом, но доступно гораздо больше учебников. Затем вы можете создать свой собственный пакет Linux, распространяя ваши библиотеки и все необходимые файлы вместе с вашим приложением Python.

Это будет очень чистый и удобный подход для ваших клиентов. Особенно в отношении обновлений. (Вы даже можете обращаться к разным выпускам ОС по мере необходимости в то же время.)

Как всегда, чистый подход связан со стоимостью. Этот чистый подход требует от вас довольно много усилий для реализации. Вам нужно не только настроить сервер - что проще - но приготовьтесь к созданию пакетов - что не сложно, но вам нужно будет немного почитать, как это сделать, и сделать довольно много экспериментов, чтобы закончить с пакетами в точности так, как вы хотите. Однако тогда все будет таким, каким вы хотите. И будущие обновления действительно легки для вас, а также для ваших клиентских машин.

Я бы рекомендовал этот подход, если вы хотите упростить обновления в будущем, хотите узнать о Linux и, возможно, в будущем будете иметь требования к собственным пакетам. Или большое количество клиентов.


Что касается очень "высокого уровня" подхода. Напротив, подход "низкого уровня" был бы следующим:

  • Проверьте наличие ваших библиотек при запуске вашей программы.
  • Если нет: завершите приложение. Распечатайте текст, относящийся к script, как установить необходимые библиотеки. Это может быть даже URL-адрес, где можно скачать script, f.e. с:

bash <(curl -s http://mywebsite.com/myscript.txt)