掘金 后端 ( ) • 2024-04-20 17:54

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远程解释器,然后配置执行方式:

image.png

一些简单观察到的流程(没有实际看完整流程,只是靠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

最终效果

image.png

image.png

可以看到最终实现了预期, 安装的时候, 自动把so给安装上了. 卸载的时候, 也顺利把so及相关文件卸载了.