setuptools基本用法的清晰介绍,好文
MANIFEST.in文件的官方讲解
根目录setup.py
, MANIFEST.in
, README.md
等文件会自动加入包里,不需要指定什么。
python setup.py clean --all
清除之前编译时的中间结果(也即build
目录),如果没有清理,修改setup.py
, MANIFEST.in
文件中的内容后重新编译很可能不会生效,程序依然直接根据之前编译的中间结果去打包文件。(后记:不过实测出来光clean
还不够,还要rm -rf *.egg-info/
,否则还是会打包之前指定的py文件)
开局一句from setuptools import setup
写在前面,
关于setup
函数的参数,我们一个一个来讲(讲某个参数的时候其他参数忽略不写):
setup(name="demo", version="1.0")
包里只有根目录下的setup.py
, MANIFEST.in
, README.md
等文件
setup(packages=["mod1", "mod2"]
假设根目录下面有很多包,我们只需要mod1
跟mod2
包,那么把它们加进packages
参数,则这些目录下的所有.py
文件都会被打包
setup(include_package_data=True)
根据MANIFEST.in
的内容精细化指定需要和不需要打包的文件
setup(exclude_package_data={'mod1':['.gitignore']})
排除mod1
目录下的.gitignore
文件,如果'mod1'
换成''
表示排除所有目录下的.gitignore
文件
setup(package_data={"": ["*.txt", "*.pth"]})
跟exclude_package_data
的用法基本一致,需要注意的是它只对在packages
中的目录进行检查,而不会添加其他目录下的数据文件,即使这里写的''
看起来像是整个根目录下的所有目录。package_data
感觉没有MANIFEST.in
用起来方便。
python setup.py install
是直接将当前目录下的内容安装成pip list
里头的一个包,而python setup.py develop
更适合处于调试阶段的代码,它并没有在site-packages
目录下生成一个包,而是拉了一条软链到当前目录,这样我们在当前目录下修改代码,都能立即反映上去,不需要每次都重复python setup.py install
。
python setup.py sdist
可以创建一个源码包,python setup.py bdist_wheel
创建一个wheel发布包
更多命令可以通过python setup.py --help-commands
查看。
python setup.py install/develop
会在当前目录下创建一个module_name.egg-info/
目录,后续如果python setup.py sdist && pip install dist/module_name-version.tar.gz
就会报一些奇奇怪怪的错误,这时把module_name.egg-info/
目录删掉可以正常编译安装了。
更进一步:在打包时添加c++文件拓展
首先是自己制作可被python调用的C++动态链接库也就是so文件,有几种常见的技术路径比如ctypes、Boost和pybind11(参考链接1,参考链接2,参考链接3),这里选择pybind11进行讲解。
复习一下编译和链接的关系:看这里。编译是把我们自己写的代码转换成二进制目标文件.o
或.obj
,链接是将所有的目标文件以及系统组件组合成一个可执行文件.exe
。
使用cmake编译纯C++代码可以看我的另一篇文章。
首先安装pybind11,源码安装跟pip install
安装的结果还不太一样,后者可以import pybind11
前者不行,把usr/local/include/pybind11
加入PYTHONPATH
也无济于事,前者可以找到pybind11-config.cmake
文件后者不行,wheel包就是不包含这个文件,所以这里分开讲用法。pip安装:pip install pybind11
然后用C++写一个简单的求和函数,命名为example.cpp
(从这里拷过来的):
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j)
{
return i + j;
}
PYBIND11_MODULE(test, m)
{
// optional module docstring
m.doc() = "pybind11 example plugin";
// expose add function, and add keyword arguments and default arguments
m.def("add", &add, "A function which adds two numbers", py::arg("i")=1, py::arg("j")=2);
// exporting variables
m.attr("the_answer") = 42;
py::object world = py::cast("World");
m.attr("what") = world;
}
然后进行编译:c++ -O3 -Wall -shared -std=c++11 -fPIC $(python -m pybind11 --includes) example.cpp -o test$(python3-config --extension-suffix)
,超长的命令,官网的示例命令就是这么长,离谱。
各参数的含义为(参考链接):
-O3:优化等级设为第3级
-Wall:启用最大程度的警告信息
-shared:生成一个共享库文件;
-fPIC:生成位置无关目标代码,适用于动态连接;
-L path:将path库文件搜索路径列表;
-I path:将path加入头文件搜索路径列表;
-o file:指定输出文件名,也就是so文件名字的前半段,以及python import时候的包名,file必须与PYBIND11_MODULE()的第一个参数相同;
其他参数可以通过c++ -v --help
查看。
python -m pybind11 --includes
会给出pybind11用到的库文件的搜索路径,形如"-I/usr/include/python3.9 -I/usr/local/lib/python3.9/dist-packages/pybind11/include"
python3-config --extension-suffix
给出当前环境下so文件的后缀,形如".cpython-39-x86_64-linux-gnu.so"
所以如果把$()
包着的两个参数解开,整一条命令就是:c++ -O3 -Wall -shared -std=c++11 -fPIC -I/usr/include/python3.9 -I/usr/local/lib/python3.9/dist-packages/pybind11/include example.cpp -o test.cpython-39-x86_64-linux-gnu.so
如果so文件已经生成好了,在python里头import却说找不到,这时可以通过下面的命令看看当前python解释器支持哪些后缀的so文件的导入(参考链接):
import importlib.machinery
print(importlib.machinery.all_suffixes())
如果发现so文件名称不对,例如so文件中的python版本数字本来应该是39结果变成了38,就要去确认一下python3
跟python3-config
命令软链的目标是否跟预期一致。
到这一步,cpp文件目录下就应当有一个test开头的so文件,且可以在python中执行下列命令:
import test
test.add(1, 2)
至此一个基于pybind11和c++
编译命令的python和C++混合用例就完成了~
接下来使用cmake
进行编译,与cpp文件同目录的CMakeLists.txt内容如下:
# 给定cmake版本最低要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名
project(example)
# 添加头文件的搜索路径
include_directories("/usr/include/python3.9")
include_directories("/usr/local/lib/python3.9/dist-packages/pybind11/include")
# 设置变量,这部分与单纯用`c++`命令编译时的参数一致
SET(CMAKE_CXX_FLAGS "-std=c++11 -O3")
# 把一或多个源文件编译成库文件
add_library(test SHARED example.cpp)
对cmake命令的更详细介绍请看这里。
然后执行cmake -B build/ && cd build/ && make
,就会得到一个libtest.so
文件。这里我没有找到自动去掉前面的lib前缀的方法,但是这个so文件里头其实是对应着模块名test,所以还需要手动改回test.so,用其他名字都无法import成功。
mv libtest.so test.so
,最后检验一下导入是否成功:
import test
test.add(1, 2)
至此一个基于pybind11和cmake
的python和C++混合用例就完成了!
接下来讲一下源码安装方式,网上大多都是用这种方式安装pybind11
:
#有一些python跟C++的混合项目编译用的是eigen,也要装一下
git clone git@github.com:pybind/pybind11.git \
&& mkdir pybind11/build && cd pybind11/build \
&& cmake .. \
&& make -j12 \
&& make install \
&& apt-get install libeigen3-dev \
&& ln -s /usr/include/eigen3/Eigen /usr/include/Eigen \
&& ln -s /usr/include/eigen3/unsupported /usr/include/unsupported
&& export PYTHONPATH=$PYTHONPATH:/usr/local/include/pybind11
此时,CMakeLists.txt应该这样写:
# 给定cmake版本最低要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名,此后可以通过 ${PROJECT_NAME} 使用项目名
project(example)
# 寻找pybind11,此时应有 /usr/local/include/pybind11 目录,否则程序无法工作
find_package(pybind11 REQUIRED)
# 将example.cpp文件中的内容添加到输出文件test中,最后将生成一个以test开头的so文件供python导入
# example.cpp中应有PYBIND11_MODULE函数,且它的第一个参数应于这里传给pybind11_add_module的第一个参数一致,
# 否则后续会报错ImportError: dynamic module does not define module export function
pybind11_add_module(test example.cpp)
然后执行cmake -B build/ && cd build/ && make
,就会得到一个test.xxx.so
文件,最后检验一下导入是否成功:
import test
test.add(1, 2)
完成!
下面展示一个引入c++文件的setup.py:
import os
import subprocess
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
project_name = 'projection_render'
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=''):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
if not os.path.exists(self.sourcedir):
os.makedirs(self.sourcedir)
class CMakeBuild(build_ext):
r"""
During the process of `python setup.py install`, it will automatically call `python setup.py build_ext`
to build C/C++ extensions, so we need to rewrite the event of build_ext command
"""
def run(self):
for ext in self.extensions:
self.build_extension(ext)
def build_extension(self, ext):
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
extdir = self.get_ext_fullpath(ext.name)
if not os.path.exists(extdir):
os.makedirs(extdir)
# This is the temp directory where your build output should go
install_prefix = os.path.abspath(os.path.dirname(extdir))
if not os.path.exists(install_prefix):
os.makedirs(install_prefix)
cmake_list_dir = os.path.join(install_prefix, project_name)
# run cmake to build a .so file according to CMakeLists.txt
subprocess.check_call(['cmake', cmake_list_dir], cwd=self.build_temp)
subprocess.check_call(['cmake', '--build', '.'], cwd=self.build_temp)
setup(
name=project_name,
# 将include_package_data设为True,通过MANIFEST.in文件将非.py的编译所需文件(如.cpp)打包
include_package_data=True,
# 没有写__init__.py但又需要打包的.py文件目录,写在packages里头
packages=[
'projection_render', 'projection_render.include',
'projection_render.src', 'projection_render.cuda_renderer'
],
version="0.1",
ext_modules=[CMakeExtension('projection_render')],
# 含C++包的需要重写python setup.py build_ext命令,在里头编译cpp文件
cmdclass={
"build_ext": CMakeBuild,
},
description='python verison of projection_render',
)
MANIFEST.in
文件的内容为:
include projection_render/README.md
include projection_render/cuda_renderer/*
include projection_render/include/*
graft projection_render/pybind/
include projection_render/src/*
include projection_render/CMakeLists.txt
如果一个顶层目录下有多个C++模块,各有各的CMakeLists.txt
,情况又有不同。
引入C++模块最简单的写法是使用ext_modules
参数,通过sources
指定参与编译的源文件,通过name
指定编译成的文件名(参考链接)。但在需要比较多的源文件参与编译的时候,这个参数就显得有点乏力了。这时我们需要使用更强大的工具,通过cmdclass
参数重写build_ext
函数。
setup(cmdclass={"build_ext": MyCommand})
这个MyCommand
必须继承自distutils.core.Command
类,setuptools
已经包了一层,一般是继承setuptools
包好的类,比如build_ext
命令对应的MyCommand
就继承自setuptools.command.build_ext.build_ext
类,我们如果不做重写,python setup.py build_ext
命令运行的就是它这个类。
重写主要是重写run()
函数,在进入run()
的时候,self.extensions
已经被赋值为输入setup()
的ext_modules
,可以被MyCommand实例使用了,暂时没有搞明白到底是在哪个阶段赋值的,不管它,先用了再说
不管运行的命令是install
, develop
还是build_ext
还是啥,setup()
里头的参数都会在Command
实例get_finalized_command()
的时候被解析出来,其中就包括ext_modules
。
这里再给出一个多模块的setup.py
例子:
import os
import subprocess
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
project_name = "algo_utils"
# 因为编译所需的文件由MANIFEST.in指定,具体的操作由CMakeLists.txt指定,所以Extension的sources参数就没有用了,所以这里才直接赋了个固定空列表
# 不加makedirs的操作会报CMake Error: The source directory "xxx" does not exist,原因不明
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=""):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
if not os.path.exists(self.sourcedir):
os.makedirs(self.sourcedir)
class CMakeBuild(build_ext):
r"""
During the process of `python setup.py install`, it will automatically call `python setup.py build_ext`
to build C/C++ extensions, so we need to rewrite the event of build_ext command
"""
def run(self):
for ext in self.extensions:
self.build_extension(ext)
def build_extension(self, ext):
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
extdir = self.get_ext_fullpath(ext.name)
if not os.path.exists(extdir):
os.makedirs(extdir)
# This is the temp directory where your build output should go
install_prefix = os.path.abspath(os.path.dirname(extdir))
if not os.path.exists(install_prefix):
os.makedirs(install_prefix)
cmake_list_dir = os.path.join(install_prefix, project_name, ext.name)
# run cmake to build a .so file according to CMakeLists.txt
subprocess.check_call(["cmake", cmake_list_dir], cwd=self.build_temp)
subprocess.check_call(["cmake", "--build", "."], cwd=self.build_temp)
setup(
name=project_name,
include_package_data=True,
version="0.1",
# 二级模块名写在这里
ext_modules=[
CMakeExtension("projection_render"),
],
cmdclass={
"build_ext": CMakeBuild,
},
description="algorithm utility package containing .cpp file",
)
安装好之后检查一下:
from algo_utils import projection_render
没有问题就完成了。
进阶:多个包共用一个命名空间
随着代码仓库规模扩大,有时候我们会有将某些功能模块独立出来单独进行版本控制的需求,分离出来的模块以一个单独的python包的形式存在,也需要安装,有自己的版本号。但是简单的分离成一个独立的库,在导入时跟之前的写法就不一样了,有很多import
语句需要更改。setuptools
给出了一种解决方案,通过namespace_packages
参数指出命名空间,详情请看参考链接和示例项目(还有一个谷歌的项目也可以参考)。简单地说,就是在setup.py
中的setup()
函数添加一个参数namespace_packages=["顶层模块名"]
,然后按示例项目的写法在对应的__init__.py
中写上__import__("pkg_resources").declare_namespace(__name__)
,这句话的含义是临时导入pkg_resources
包,调用它的declare_namespace()
方法将顶层模块指定为命名空间(参考链接)。需要注意的是原有的主仓库跟新分离出来的子仓库都要对setup()
和__init__.py
做上述处理,如果只是子仓库做了主仓库没做,在python setup.py develop
之后主仓库的命名空间会被子仓库所覆盖,import 顶层模块名
只能找到子仓库,主仓库找不到了,这点一定要注意。如果书写正确,代码分离前后import
命令是不需要改动的。
另一种方案是基于pkgutils
库,跟上面的pkg_resources
方法类似,在对应的__init__.py
里写上__path__ = __import__('pkgutil').extend_path(__path__, __name__)
,然后setup()
函数不需要加namespace_packages
参数。根据文档的说法,这个方案对Python2和Python3的兼容性更强,个人觉得pkg_resources
方法有namespace_packages
参数可以直截了当地看出顶层包名,是比较方便的,在不需要兼容Python2的情况下选用pkg_resources
方案应该更合适。
其他
扩充setup.py支持的命令:需要使用cmdclass参数
setup(cmdclass={"build_ext": CMakeBuild})
新增一个build_ext命令,通过python setup.py build_ext
调用,这里的CMakeBuild是一个父类为setuptools.command.build_ext.build_ext的类,需要重写run方法,在运行python setup.py build_ext
的时候实际上就是在调用CMakeBuild类的run方法。