python - 包含和分发具有 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
.
解决方案
这篇文章的目标是有一个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
-folder 添加到分发中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.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
-file,我们需要将其放入lib
-subfolder。 - 在运行时加载 C 扩展时,它需要
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
sodll
和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)
推荐阅读
- java - 在 glassfish3 服务器上上传应用程序文件 (ords.war) 时发生部署错误
- transactions - 比特币核心 - 如何在将 BTC 发送到某个地址之前获取交易大小
- python - Flask 消息闪烁不适用于重定向
- python - 当列名未知时,从 Python 中的 DataFrame 中识别列名
- python - requests in Flask return 404
- excel - 从网站导出文件后如何使用 excel 日期
- c# - Foreach循环不循环通过复选框列表asp .net
- javascript - 客户端 js 文件不工作 | 节点.js
- angular - 尽管通过了测试用例,但点亮元素的代码覆盖率显示为 0
- sql - 仅一组的最后 6 行