Включение и распространение сторонних библиотек с расширением Python C

Я создаю расширение C Python, которое использует стороннюю библиотеку — в данном случае ту, которую я создал с использованием отдельного процесса сборки и цепочки инструментов. Назовите эту библиотеку libplumbus.dylib.

Структура каталогов будет:

grumbo/
  include/
    plumbus.h
  lib/
    libplumbus.so
  grumbo.c
  setup.py

Мой setup.py выглядит примерно так:

from setuptools import Extension, setup

native_module = Extension(
    'grumbo',
    define_macros = [('MAJOR_VERSION', '1'),
                     ('MINOR_VERSION', '0')],
    sources       = ['grumbo.c'],
    include_dirs  = ['include'],
    libraries     = ['plumbus'],
    library_dirs  = ['lib'])


setup(
    name = 'grumbo',
    version = '1.0',
    ext_modules = [native_module] )

Поскольку libplumbus — это внешняя библиотека, при запуске import grumbo я получаю:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: dlopen(/path/to/grumbo/grumbo.cpython-37m-darwin.so, 2): Library not loaded: lib/libplumbus.dylib
  Referenced from: /path/to/grumbo/grumbo.cpython-37m-darwin.so
  Reason: image not found

Каков самый простой способ настроить все так, чтобы libplumbus было включено в дистрибутив и правильно загружалось при импорте grumbo? (Обратите внимание, что это должно работать с virtualenv).

Я попытался добавить lib/libplumbus.dylib к package_data, но это не сработает, даже если я добавлю -Wl,-rpath,@loader_path/grumbo/lib к extra_link_args расширения.


person trbabb    schedule 09.09.2020    source источник


Ответы (1)


Цель этого поста — создать setup.py, который создаст исходный дистрибутив. Это означает, что после запуска

python setup.py sdist

полученный dist/grumbo-1.0.tar.gz можно использовать для установки через

pip install grumbo-1.0.tar.gz

Мы начнем с версии setup.py для Linux/MacOS, но затем настроим ее так, чтобы она работала и для Windows.


Первый шаг — добавить в дистрибутив дополнительные данные (включает/библиотека). Я не уверен, что действительно невозможно добавить данные для модуля, но setuptools предлагает функциональность для добавления данных для пакетов, поэтому давайте создадим пакет из вашего модуля (что, вероятно, в любом случае является хорошей идеей).

Новая структура package grumbo выглядит следующим образом:

src/
  grumbo/
     __init__.py  # empty
     grumbo.c
     include/
       plumbus.h
     lib/
       libplumbus.so
setup.py

и изменил setup.py:

from setuptools import setup, Extension, find_packages

native_module = Extension(
                name='grumbo.grumbo',
                sources = ["src/grumbo/grumbo.c"],
              )
kwargs = {
      'name' : 'grumbo',
      'version' : '1.0',
      'ext_modules' :  [native_module],
      'packages':find_packages(where='src'),
      'package_dir':{"": "src"},
}

setup(**kwargs)

Это пока мало что дает, но, по крайней мере, наш пакет может быть найден setuptools. Сборка не удалась, потому что отсутствуют включения.

Теперь добавим нужные include из папки include в раздачу через package-data:

...
kwargs = {
      ...,
      'package_data' : { 'grumbo': ['include/*.h']},
}
...

При этом наши include-файлы копируются в исходный дистрибутив. Однако, поскольку он будет построен где-то, о чем мы еще не знаем, добавление include_dirs = ['include'] к определению Extension не поможет.

Должен быть лучший способ (и менее хрупкий) найти правильный путь включения, но это то, что я придумал:

...
import os
import sys
import sysconfig
def path_to_build_folder():
    """Returns the name of a distutils build directory"""
    f = "{dirname}.{platform}-{version[0]}.{version[1]}"
    dir_name = f.format(dirname='lib',
                    platform=sysconfig.get_platform(),
                    version=sys.version_info)
    return os.path.join('build', dir_name, 'grumbo')

native_module = Extension(
                ...,
                include_dirs  = [os.path.join(path_to_build_folder(),'include')],
)
...

Теперь расширение построено, но еще не может быть загружено, поскольку оно не связано с общим объектом libplumbus.so, и поэтому некоторые символы не разрешены.

Подобно заголовочным файлам, мы можем добавить нашу библиотеку в дистрибутив:

kwargs = {
          ...,
          'package_data' : { 'grumbo': ['include/*.h', 'lib/*.so']},
}
...

и добавьте правильный путь к библиотеке для компоновщика:

...
native_module = Extension(
                ...
                libraries     = ['plumbus'],
                library_dirs  = [os.path.join(path_to_build_folder(), 'lib')],
              )
...

Вот, мы почти у цели:

  • расширение встроено в site-packages/grumbo/
  • расширение зависит от libplumbus.so как видно с помощью ldd
  • libplumbus.so помещается в site-packages/grumbo/lib

Однако мы по-прежнему не можем импортировать расширение, так как import grumbo.grumbo ведет к

ImportError: libplumbus.so: невозможно открыть общий объектный файл: нет такого файла или каталога

потому что загрузчик не может найти нужный общий объект, который находится в папке .\lib относительно нашего расширения. Мы могли бы использовать rpath, чтобы помочь загрузчику:

...
native_module = Extension(
                ...
                extra_link_args = ["-Wl,-rpath=$ORIGIN/lib/."],
              )
...

И вот мы закончили:

>>> import grumbo.grumbo
# works!

Также сборка и установка колеса должны работать:

python setup.py bdist_wheel

а потом:

pip install grumbo-1.0-xxxx.whl

Камень первой мили пройден. Теперь мы расширяем его, чтобы он работал и на других платформах.


Один и тот же исходный дистрибутив для Linux и Macos:

Чтобы иметь возможность установить один и тот же исходный дистрибутив в Linux и MacOS, должны присутствовать обе версии общей библиотеки (для Linux и MacOS). Можно добавить суффикс к именам общих объектов: например. имея libplumbus.linux.so и libplumbis.macos.so. Правильный общий объект можно выбрать в setup.py в зависимости от платформы:

...
import platform
def pick_library():
    my_system = platform.system()
    if my_system == 'Linux':
        return "plumbus.linux"
    if my_system == 'Darwin':
        return "plumbus.macos"
    if my_system == 'Windows':
        return "plumbus"
    raise ValueError("Unknown platform: " + my_system)

native_module = Extension(
                ...
                libraries     = [pick_library()],
                ...
              )

Настройка для Windows:

В Windows динамические библиотеки — это библиотеки DLL, а не общие объекты, поэтому необходимо учитывать некоторые различия:

  • когда C-расширение построено, ему нужен plumbus.lib-файл, который нам нужно поместить в подпапку lib.
  • когда C-расширение загружается во время выполнения, ему нужен plumbus.dll-файл.
  • В Windows нет понятия rpath, поэтому нам нужно поместить dll рядом с расширением, чтобы его можно было найти (см. Также это SO -пост для более подробной информации).

Это означает, что структура папок должна быть следующей:

src/
  grumbo/
     __init__.py
     grumbo.c
     plumbus.dll           # needed for Windows
     include/
       plumbus.h
     lib/
       libplumbus.linux.so # needed on Linux
       libplumbus.macos.so # needed on Macos
       plumbus.lib         # needed on Windows
setup.py

Есть также некоторые изменения в setup.py. Во-первых, расширение package_data, чтобы dll и lib были подняты:

...
kwargs = {
      ...
      'package_data' : { 'grumbo': ['include/*.h', 'lib/*.so',
                                    'lib/*.lib', '*.dll',      # for windows
                                   ]},
}
...

Во-вторых, rpath можно использовать только в Linux/MacOS, поэтому:

def get_extra_link_args():
    if platform.system() == 'Windows':
        return []
    else:
        return ["-Wl,-rpath=$ORIGIN/lib/."]
    

native_module = Extension(
                ...
                extra_link_args = get_extra_link_args(),
              )

Это!


Полный установочный файл (вы можете добавить определение макроса или что-то подобное, что я пропустил):

from setuptools import setup, Extension, find_packages

import os
import sys
import sysconfig
def path_to_build_folder():
    """Returns the name of a distutils build directory"""
    f = "{dirname}.{platform}-{version[0]}.{version[1]}"
    dir_name = f.format(dirname='lib',
                    platform=sysconfig.get_platform(),
                    version=sys.version_info)
    return os.path.join('build', dir_name, 'grumbo')


import platform
def pick_library():
    my_system = platform.system()
    if my_system == 'Linux':
        return "plumbus.linux"
    if my_system == 'Darwin':
        return "plumbus.macos"
    if my_system == 'Windows':
        return "plumbus"
    raise ValueError("Unknown platform: " + my_system)


def get_extra_link_args():
    if platform.system() == 'Windows':
        return []
    else:
        return ["-Wl,-rpath=$ORIGIN/lib/."]
    

native_module = Extension(
                name='grumbo.grumbo',
                sources = ["src/grumbo/grumbo.c"],
                include_dirs  = [os.path.join(path_to_build_folder(),'include')],
                libraries     = [pick_library()],
                library_dirs  = [os.path.join(path_to_build_folder(), 'lib')],
                extra_link_args = get_extra_link_args(),
              )
kwargs = {
      'name' : 'grumbo',
      'version' : '1.0',
      'ext_modules' :  [native_module],
      'packages':find_packages(where='src'),
      'package_dir':{"": "src"},
      'package_data' : { 'grumbo': ['include/*.h', 'lib/*.so',
                                    'lib/*.lib', '*.dll',      # for windows
                                   ]},
}

setup(**kwargs)
person ead    schedule 10.09.2020