python把c扩展模块打包成免编译且可pip install包的方法
python用c语言编写的扩展模块,在setup.py sdist或者bdist打包时,默认是把c源码编译进去,在pip install的时候先编译再安装的,即使我把so加到了MANIFEST.in文件中,查看编出来的包里包含了so,但是仍然不会进行so的安装。搜索查阅了一下,没有找到相关的文章可以解决这个问题。因此只能自己通过debug研究python-prctl的安装流程,把相关的流程走通了。总结记录一下:
背景
首先说明一下背景:
我们的业务逻辑有一部分需要使用c语言来写,这部分c语言写的代码只能在特定的编译机上编译成.so,我们使用的python版本统一定为python3.8。因此我编写了一个python的c扩展模块,并对底层c接口进行了相应的python函数包装,在使用的时候,需要放到内网pip源,用pip install的方式安装。所有使用人的os版本、python版本都是一致的,且没有编译需要的相关文件。
原理
调试方式
由于时间比较紧,所以没有详细研究pip install的流程,python-prctl这个模块的项目结构和我们的类似,不同的是它是pip install的时候先编译再安装的,因此用它当作例子来学习。
在pycharm上配置ssh远程解释器,然后配置执行方式:
一些简单观察到的流程(没有实际看完整流程,只是靠debug和猜测推断了下)
pip install的流程
1. egg_info
2. install
1) build
a) build_py
b) build_clib
c) build_ext
d) build_scripts
2) install_lib
3) install_egg_info
3) install_scripts
编译的.so,会拷贝到build/lib.linux-x86_64-3.8目录
实现思路
经过多次踩坑(一直不安装so,甚至改install命令自己把so拷贝到site-packages,但是uninstall时又不会卸载等等),最终确定一个实现的思路,虽然不编译,但是在build_ext的流程中,把对应的so拷贝到build/lib.linux-x86_64-3.8目录,让后续的安装流程能够正常安装进行,这样后面的流程能够正常的把so拷贝到site-packages目录,且卸载时正常删除。这样对原流程的改动最小,影响也最少。
build_ext命令的定制
我们的业务工程代码,对于Makefile有整体的封装,怎么搜索头文件,怎么搜索lib库都有封装,我们想要调用这些封装的内容来去找我们的依赖模块,这样我们依赖的模块有修改的话,该模块不用修改。所以不能在setup.py中写死include目录及libraries。因此对build_ext做一些定制。把include目录、链接的.a文件、源码通过Makefile传递进去。
class my_build_ext(build_ext):
user_options = build_ext.user_options
user_options.extend([
('extra-objects=', None, "list of extra_objects"),
('sources=', None, "list of sources")
])
def initialize_options(self):
super(my_build_ext, self).initialize_options()
self.extra_objects = None
self.sources = None
def run(self):
include_dirs = []
for include_dir in self.include_dirs:
include_dirs.extend(include_dir.split())
self.include_dirs = include_dirs
for extension in self.extensions:
if self.sources:
extension.sources = self.sources.split()
if self.extra_objects:
extension.extra_objects = self.extra_objects.split()
super(my_build_ext, self).run()
setup.py的定制
根据实现思路的描述,这里主要处理pip install的时候的流程,跳过编译阶段,直接把so拷贝到build目录
def build_extensions(self):
if not self.sources:
# 没有传sources 说明是pip install, 跳过编译阶段, 直接把包里面的so拷贝到build目录
for extension in self.extensions:
# 找编译的目标目录
ext_path = self.get_ext_fullpath(extension.name)
ext_name = os.path.basename(ext_path)
ext_dir = os.path.dirname(ext_path)
mkpath(ext_dir, 0o777, dry_run=0)
copy_file(ext_name, ext_dir)
return
# build
super(my_build_ext, self).build_extensions()
makefile编写
在定义make all和make clean
make all时,先调用setup.py build_ext把so编译出来,然后拷贝到当前目录,最后调用setup.py sdist编译为可pip安装的包
all:
python3.8 setup.py build_ext --include-dirs="include" --sources="_sample_extension_mod.c"
\cp build/lib.linux-x86_64-3.8/_sample_extension_mod.cpython-38-x86_64-linux-gnu.so .
python3.8 setup.py sdist
clean:
rm -rf build dist *.so
最终效果
可以看到最终实现了预期, 安装的时候, 自动把so给安装上了. 卸载的时候, 也顺利把so及相关文件卸载了.
转载自:https://juejin.cn/post/7359577911990566947