python - 包含和分发具有 Python C 扩展的第三方库

标签 python c setuptools python-c-api python-extensions

我正在构建一个使用“第三方”库的 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.dylibpackage_data ,但这不起作用,即使我添加 -Wl,-rpath,@loader_path/grumbo/lib到扩展的 extra_link_args .

最佳答案

这篇文章的目标是拥有一个 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提供了为包添加数据的功能,所以让我们从你的模块中创建一个包(无论如何这可能是个好主意)。
包的新结构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 添加所需的包含。 -通过package-data 分发的文件夹:
...
kwargs = {
      ...,
      'package_data' : { 'grumbo': ['include/*.h']},
}
...
这样我们的包含文件就会被复制到源代码分发中。然而,因为它将在我们还不知道的“某处”构建,添加 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.soldd 的帮助下可以看到
  • libplumbus.so放入site-packages/grumbo/lib

  • 但是,我们仍然无法导入扩展名,如 import grumbo.grumbo导致

    ImportError: libplumbus.so: cannot open shared object file: No such file or directory


    因为加载程序找不到位于文件夹 .\lib 中的所需共享对象。相对于我们的扩展。我们可以使用 rpath “帮助”装载机:
    ...
    native_module = Extension(
                    ...
                    extra_link_args = ["-Wl,-rpath=$ORIGIN/lib/."],
                  )
    ...
    
    现在我们完成了:
    >>> import grumbo.grumbo
    # works!
    
    build 和安装轮子也应该工作:
    python setup.py bdist_wheel
    
    进而:
    pip install grumbo-1.0-xxxx.whl
    
    实现了第一 block 里程碑。现在我们对其进行了扩展,因此它也适用于其他平台。

    Linux 和 Macos 的相同源代码分发:
    为了能够在 Linux 和 MacOS 上安装相同的源代码分发,共享库的两个版本(适用于 Linux 和 MacOS)都必须存在。一种选择是为共享对象的名称添加后缀:例如有libplumbus.linux.solibplumbis.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 -file,我们需要将其放入lib -子文件夹。
  • 在运行时加载 C 扩展时,它需要 plumbus.dll -文件。
  • Windows 没有 rpath 的概念,因此我们需要将 dll 放在扩展名旁边,以便可以找到它(另请参阅此 SO-post 了解更多详细信息)。

  • 这意味着文件夹结构应如下所示:
    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所以dlllib被捡起:
    ...
    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)
    

    关于python - 包含和分发具有 Python C 扩展的第三方库,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63804883/

    相关文章:

    python - 循环并制作新列python

    c - 使用 read() 系统调用

    C 数组与结构

    c - 使用 C/Intel 汇编,测试 128 字节内存块是否包含全零的最快方法是什么?

    python - 'setup.py test' egg 安装位置?

    python - 如何在 setup.py 中执行(安全的)bash shell 命令?

    python - 从 pip 安装的可执行脚本使用了错误的 Python 路径

    python - 使用 Python re 转换注释//with/*

    python - 我如何获得图像识别的概率

    python - 异常值 :failed to find libmagic. 检查您在 Windows 7 中的安装