【问题标题】:Including and distributing third party libraries with a Python C extension包含和分发具有 Python C 扩展的第三方库
【发布时间】:2020-12-27 11:41:51
【问题描述】:

我正在构建一个使用“第三方”库的 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

【问题讨论】:

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


    【解决方案1】:

    这篇文章的目标是有一个setup.py,它将创建一个源代码分发。这意味着在运行之后

    python setup.py sdist
    

    生成的dist/grumbo-1.0.tar.gz 可用于安装通过

    pip install grumbo-1.0.tar.gz
    

    我们将开始为 Linux/MacOS 提供 setup.py,但随后进行调整以使其也适用于 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找到。构建失败,因为缺少包含。

    现在让我们通过package-datainclude文件夹中所需的包含添加到分发中:

    ...
    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.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.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-文件,我们需要将其放入lib-子文件夹中。
    • 在运行时加载C-extension时,需要plumbus.dll-file。
    • 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)
    

    【讨论】:

    • 这个答案太棒了!显示的这个 path_to_build_folder() 函数对我不起作用,不适用于 pip -e . 构建,因此我添加了两次 include_dir 路径以包含 repo 中的位置以及构建文件夹路径。
    猜你喜欢
    • 1970-01-01
    • 2018-06-21
    • 2016-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-12
    • 2013-10-11
    • 1970-01-01
    相关资源
    最近更新 更多