setup.py里有什么?
文章目录
- setup.py里有什么?
- C/C++扩展总结
- gcc/g++的编译参数:
- Windows Visual Studio
- Cmake
- setup.py C/C++扩展模块
- 为什么需要分发打包?
- Distutils
- 一个简单的例子
- 通用的 Python 术语
- 使用 Setuptools 构建和分发软件包
- 源码包与二进制包
- eggs VS wheels
- `distutils.core.setup`(*arguments*)
- setup函数参数
- 列出全部的包
- 列出单独的模块
- 描述扩展模块
- 扩展名和软件包
- 扩展的源文件
- 预处理器选项
- 头文件
- 预处理器宏
- 库选项
- 其它选项
- 安装软件包数据
- 安装其它文件
- 打包so
- require
- install_requires
- extras_require
- 附加元数据
- 将扩展模块化
- cuda 扩展
- C/C++ 扩展
- 调用上述模块
- 发布扩展模块
- Reference
C/C++扩展总结
对于C或C++ 代码,当我们在编译的时候,需要指定 包含的头文件目录,源代码目录,引用的库文件名称,引用的库文件的目录,这样编译器才能找到 我们自己编写的头文件及源文件的对应关系,主要是其中函数的对应关系,也能找到我们引用的外部库的头文件和库的对应符号关系。说白了,库就是打包后的源文件。如果没有指定某些库的库目录,编译器会默认从系统目录下查找。
gcc/g++的编译参数:
-I (i 的大写):指定头文件的所在的目录,可以使用相对路径。
-L :表示要链接的库所在的目录。 -L/usr/lib 表示要连接的库在/usr/lib下。一般情况下,编译器会自动搜索 /usr/lib 和 /usr/local/lib 目录,可以不用指明,自己定义的库需要格外指定路径;
-L. :表示要链接的库在当前目录
-l (L的小写):表示需要链接库的名称。注意不是库文件名称,Linux下的库文件有一个约定,全部以lib开头,因此可以省去lib,比如库文件为 librandy.so,那么库名称为randy;
-shared :指定生成动态链接库;
-fPIC: 表示编译为位置独立的代码,表示编译后的代码是位置不相关的,其他程序都可以独立使用。
-static : 表示指定使用静态链接库
// -I 参数指定头文件搜索目录
gcc/g++ hello.c -I /home/randy/include -o hello// -L 参数指定库文件搜索路径, -l 指定库名称
gcc hello.c -L /home/randy/lib -l mylib -o hello// -static 强制使用静态链接库
gcc hello.c -L /home/randy/lib -static -l mylib -o hellogcc -o hello main.c -static -L. –lhellos
Windows Visual Studio
VC++目录:
包含目录:寻找#include中的randyx.h的搜索目录
库目录:寻找.lib文件的搜索目录
C/C++:
常规->附加包含目录:寻找#include中的randyx.h的搜索目录(每一项对应一个文件夹randyX,文件夹中包含了编译时所需的头文件,使用时直接#include即可)
链接器:
常规->附加库目录:寻找.lib文件的搜索目录
输入->附加依赖项:lib库(C++的库会把函数、类的声明放在*.h中,实现放在*.cpp或*.cc中。编译之后,.cpp,.cc,*.c会被打包成一个.lib文件,这样可以保护源代码)
Cmake
在编写CMakeLists.txt 文件时,也是同样要指明上述路径及文件名称:
-
include_directories : 添加路径到头文件的搜索路径
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...]) include_directories(${PROJECT_SOURCE_DIR}/testFunc/inc
-
add_executable : 利用源码文件生成目标可执行程序
add_executable(<name> [WIN32] [MACOSX_BUNDLE][EXCLUDE_FROM_ALL]source1 [source2 ...])
-
link_libraries : 将库链接到以后添加的所有目标
link_libraries([item1 [item2 [...]]] [[debug|optimized|general] <item>] ...)
-
add_library : 生成动态库或静态库
add_library(<name> [STATIC | SHARED | MODULE] [source1] [source2 ...]) add_library(<指定库的名字> [STATIC静态 | SHARED动态 | MODULE] [source1源文件] [source2源文件 ...])add_library(${PROJECT_NAME} SHARED${CMAKE_CURRENT_SOURCE_DIR}/source/ )
-
find_library :在指定路径下查找库,并把库的绝对路径存放到变量里
find_library(变量名称 库名称 提示 路径 ${PROJECT_SOURCE_DIR}/testFunc/lib)find_library(CUDNN_STATIC_LIBRARY NAMES ${CUDNN_STATIC_LIB_NAME}
-
target_link_libraries : 把目标文件与库文件进行链接
target_link_libraries(${PROJECT_NAME} ${catkin_LIBRARIES} )
setup.py C/C++扩展模块
setup.py 添加C或C++的扩展模块的时候,同样需要制定 源文件目录 sources 、 头文件目录 include_dirs 、库名称 libraries 及 库目录 library_dirs。
from distutils.core import setup, Extensionmodule1 = Extension('demo',define_macros = [('MAJOR_VERSION', '1'),('MINOR_VERSION', '0')],include_dirs = ['/usr/local/include'],libraries = ['tcl83'],library_dirs = ['/usr/local/lib'],sources = ['demo.c'])setup (name = 'PackageName',version = '1.0',description = 'This is a demo package',author = 'Randy',author_email = 'randy@jeff',url = 'https://docs.python.org/extending/building',long_description = '''
This is really just a demo package.
''',ext_modules = [module1])
进行 python setup.py develop
或者 python setup.py install
的时候,python 会调用 g++ 进行编译,将对应参数传递过去即可,本质上是一样的。
g++ -pthread -B /home/randy/anaconda3/envs/randy-v1.0/compiler_compat -Wl,--sysroot=/ -pthread -shared -B /home/randy/anaconda3/envs/randy-v1.0/compiler_compat -L/home/randy/anaconda3/envs/randy-v1.0/lib -Wl,-rpath=/home/randy/anaconda3/envs/randy-v1.0/lib -Wl,--no-as-needed -Wl,--sysroot=/ /home/randy/codes/lidarops/build/temp.linux-x86_64-cpython-38/lidarops/rslabel/src/RSLabelGenerator.o -L/home/randy/anaconda3/envs/randy-v1.0/lib/python3.8/site-packages/torch/lib -L/home/randy/anaconda3/envs/randy-v1.0/lib/python3.8/site-packages/torch/lib -L/home/randy/anaconda3/envs/randy-v1.0/lib/python3.8/site-packages/torch/lib -lc10 -ltorch -ltorch_cpu -ltorch_python -lc10 -ltorch -ltorch_cpu -ltorch_python -lc10 -ltorch -ltorch_cpu -ltorch_python -o build/lib.linux-x86_64-cpython-38/lidarops/rslabel/label_generator.cpython-38-x86_64-linux-gnu.so
为什么需要分发打包?
平常我们习惯了使用 pip 来安装一些第三方模块,有点像我们在 windows 系统上安装 exe 程序,方便后面直接点击快捷键使用一样,也像 linux 系统里面,我们通过 apt-get install
各类软件包一样,这些 exe 程序或者安装的软件包,都是别人替我们封装好的程序或者库,封装的过程就是 打包
。
打包,就是将源代码或者库文件进行封装,安装后释放到指定位置,这样使用者可以即装即用,直接引用接口或者调用程序即可,不用关心其具体的实现,同时也有利于开发者封装自己的源代码,保护源码。
Distutils
distutils
(distribute utils)是 Python 的一个标准库,即分发工具,所有后续的打包工具,全部都是基于它进行开发的。
Distutils 用起来非常简单,对于模块开发者或安装第三方模块的用户/管理员均是如此。开发者的责任(当然还有编写可靠、良好文档和经过良好测试的代码!)就是:
- 编写一个设置脚本 (
setup.py
by convention) - (可选)编写设置脚本的配置文件
- 创建源码的发行版
- (可选)创建一个或多个编译好(二进制)的发行版
一个简单的例子
setup 脚本通常很简单,尽管是用 Python 编写的,它能干的事情没有限制。
如果只想发布一个名为 foo
的模块,位于 foo.py
文件中,那么 setup 脚本可以如此简单:
from distutils.core import setup
setup(name='foo',version='1.0',py_modules=['foo'],)
注意:
- 提供给 Distutils 的大部分信息将作为关键字参数发给
setup()
函数。 - 这些关键字参数分为两类:包的元数据(名称、版本号)和包中内容的描述信息(本例中是纯 Python 模块的列表)。
- 模块由模块名指定,而不是文件名(包和扩展也是如此)。
- 建议多提供一些元数据,特别是开发者姓名、电子邮件地址和项目的URL。
为该模块创建一个源码发布版本:
python setup.py sdist
sdist 将创建一个归档文件(例如在 Unix 中为 tarball,在 Windows 中为 ZIP 文件),其中包含你的配置脚本 setup.py
以及你的模块 foo.py
。 此归档文件将被命名为 foo-1.0.tar.gz
(或 .zip
),并将解包到目录 foo-1.0
当中。
如果最终用户希望安装 foo
模块,只需下载 foo-1.0.tar.gz
(或 .zip
)并解压,进入 foo-1.0
目录运行:
python setup.py install
这会把 foo.py
复制到 Python 安装环境的第三方模块目录中。
如果想让用户真正轻松使用,你可以为他们创建一个或多个内置发行版。例如,如果您在 Windows 计算机上运行,并且想让其他 Windows 用户轻松使用,则可以使用 bdist_wininst
命令创建可执行安装程序(最适合此平台的内置发行版类型)。例如:
python setup.py bdist_wininst
将在当前目录中创建一个可执行安装程序 foo-1.0.win32.exe。
linux 系统可以使用下述命令,将源码编译成 .whl
文件:
python setup.py bdist_wheel
.whl文件就像压缩包一样,可以点击打开,如果源文件包含python 代码,则python 代码仍然可见源码,而 C/C++ 文件则编译成 so 库文件
其他有用的内置分发格式是 RPM,可由 bdist_rpm 、Solaris pkgtool(:command:
bdist_pkgtool)和 HP-UX swinstall(:command:
bdist_sdux)实现。比如,以下命令将创建一个名为 foo-1.0.noarch.rpm
的RPM文件:
python setup.py bdist_rpm
(bdist_rpm 命令用到了 rpm 可执行文件,因此必须运行在基于 RPM 的系统中,如 Red Hat Linux 、 SuSE Linux 或 Mandrake Linux)。
可以随时运行以下命令,以便了解当前可用的分发格式:
python setup.py bdist --help-formatsList of available distribution formats:--formats=rpm RPM distribution--formats=gztar gzip'ed tar file--formats=bztar bzip2'ed tar file--formats=xztar xz'ed tar file--formats=ztar compressed tar file--formats=tar tar file--formats=zip ZIP file--formats=egg Python .egg file
通用的 Python 术语
-
module
实现 Python 代码重用的基本单位:可被其他代码导入的一段代码。有三种类型的模块与本文有关:纯 Python 模块、扩展模块和包。
-
纯 Python 模块
用 Python 编写的模块,包含在某
.py
文件中(可能还会有相关的.pyc
文件)。有时被称为 “纯模块”。 -
extension module
– 扩展模块用低级语言编写的 Python 模块。Python 用 C/C++ ,而 Jython 则用Java。
通常包含在一个可动态加载的预编译文件中,比如 Unix 中的 Python 扩展是一个共享对象(
.so
)文件,Windows 中的 Python 扩展则是一个 DLL (扩展名为.pyd
),而 Jython 的扩展是个 Java class 文件。(注意,目前,Distutils 只处理 Python 的 C/C++ 扩展。) -
package
(包)包含其他模块的模块;
通常位于文件系统的某个目录中,区别于其他目录的标记就是存在一个
__init__.py
文件,如
packages=find_packages(exclude=("configs", "tests",))
find_packages(exclude=("configs", "tests",))
是递归地包含当前目录下除了configs和tests外所有文件夹的包。
-
root package
(根包)包的层次结构的根。(其并非一个真正的包,因为没有
__init__.py
文件。但总得给它起个名字)。 绝大多数标准库都在根包中,还有许多不属于任何大型模块的小型、独立的第三方模块。与普通的包不同,根包中的模块可能会在很多目录中出现:事实上,sys.path
列出的每个目录都会为根包提供模块。
使用 Setuptools 构建和分发软件包
Setuptools 是 Python distutils 的增强功能集合,可让开发人员更轻松地构建和分发 Python 包,尤其是那些依赖于其他包的包。
使用 setuptools 构建和分发的包在用户看来就像基于 distutils 的普通 Python 包一样。
源码包与二进制包
-
以源码包的方式发布
源码包安装的过程,是先解压,再编译,最后才安装,可以跨平台。本质是一个压缩包,其常见的格式有:
格式 后缀 zip .zip gztar .tar.gz bztar .tar.bz2 ztar .tar.Z tar .tar -
以二进制包形式发布
二进制包的安装过程省去了编译的过程,直接进行解压安装,所以安装速度较源码包来说更快。
由于不同平台的编译出来的包无法通用,所以在发布时,需事先编译好多个平台的包。
二进制包的常见格式有:
格式 | 后缀 |
---|---|
egg | egg |
wheel | .whl |
eggs VS wheels
Wheel 的出现是为了替代 Egg,实际上是一个zip包,其现在被认为是 Python 的二进制包的标准格式。
Wheel 和 Egg 的主要区别:
- Wheel 有一个官方的 PEP427 来定义,而 Egg 没有 PEP 定义
- Wheel 是一种分发格式,即打包格式。而 Egg 既是一种分发格式,也是一种运行时安装的格式(如果保持压缩状态),并且是可以被直接 import
- Wheel 文件不会包含 .pyc 文件。因此,当发行版仅包含 Python 文件(即没有编译的扩展)并且与 Python 2 和 3 兼容时,wheel 可能是“通用”的,类似于 sdist。
- Wheel 使用和 PEP376 兼容的 .dist-info 目录,而 Egg 使用 .egg-info 目录
- Wheel 有着更丰富的命名规则。单个 wheel 存档可以指示它与许多 Python 语言版本和实现、ABI 和系统架构的兼容性。
- Wheel 已进行版本控制。每个 Wheel 文件都包含 wheel 规范的版本和打包它的实现。
- Wheel 在内部被 sysconfig 路径类型进行组织,因此可以更轻松地转换为其他格式。
- Egg其实是对源文件的一种引用,文件内是源码的地址,本身不包含源码。
(base) qiancj@Randy-HP-ZBook-G8:~$ conda activate randy-v1.0
(randy-v1.0) qiancj@Randy-HP-ZBook-G8:~$ pip show lidarops
Name: lidarops
Version: 1.0.0+master.8123482
Summary:
Home-page:
Author: FAW.LiDAR.OPS
Author-email:
License:
Location: /home/randy/codes/ops/lidarops
Editable project location: /home/randy/codes/ops/lidarops
Requires:
Required-by: (randy-v1.0) qiancj@Randy-HP-ZBook-G8:~$ cat ~/anaconda3/envs/randy-v1.0/lib/python3.8/site-packages/lidarops.egg-link
/home/randy/codes/ops/lidarops
wheel 包可以通过 pip 来安装,只不过需要先安装 wheel 模块,然后再使用 pip 的命令。
-
$ pip install wheel $ pip wheel --wheel-dir=/local/wheels pkg(randy-v1.0) qiancj@Randy-HP-ZBook-G8:~$ pip wheel --wheel-dir=./wheels pkg Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple Collecting pkgFile was already downloaded /home/randy/wheels/pkg-0.2-py3-none-any.whl (randy-v1.0) qiancj@Randy-HP-ZBook-G8:~$ ls wheels/ pkg-0.2-py3-none-any.whl
distutils.core.setup
(arguments)
基本的包办一切的函数,它完成了您可以从Distutils方法中请求的大部分事情。
setup函数参数
参数名称 | value | type |
---|---|---|
name | 包的名字 | 字符串 |
version | 包的版本号; | 字符串 |
description | 单行的包的描述 | 字符串 |
long_description | 更长的包描述 | 字符串 |
author | 包的作者 | 字符串 |
author_email | 包的作者的电子邮件 | 字符串 |
maintainer | 当前维护者的名字,如果与作者不同的话。 请注意,如果提供了维护者,distutils 将使用它作为 PKG-INFO 中的作者。 | 字符串 |
maintainer_email | 当前维护者的电子邮件地址(如果与作者不同) | 字符串 |
url | 包的URL(主页) | 字符串 |
download_url | 包的下载地址 | 字符串 |
packages | distutils将操作的Python软件包的列表 | 字符串列表 |
py_modules | distutils 会操作的 Python 模块列表 | 字符串列表 |
scripts | 要构建和安装的独立脚本文件的列表 | 字符串列表 |
ext_modules | 要构建的 Python 扩展的列表 | 类 distutils.core.Extension 的实例的列表 |
classifiers | 包的类别列表 | 一个字符串列表;可用的分类器已在 PyPI上列出。 |
distclass | 要使用的类 Distribution 类 | distutils.core.Distribution 的子类 |
script_name | setup.py 脚本名称 —— 默认为 sys.argv[0] | 字符串 |
script_args | 提供给安装脚本的参数 | 字符串列表 |
options | 安装脚本的默认选项 | 字典 |
license | 包的许可证 | 字符串 |
keywords | 描述性的元数据,参见 PEP 314 | 字符串列表或逗号分隔的字符串 |
platforms | 字符串列表或逗号分隔的字符串 | |
cmdclass | 一个从命令名称到 Command 子类的映射 | 字典 |
data_files | 要安装的数据文件列表 | 列表 |
package_dir | 包到目录名的映射 | 字典 |
列出全部的包
packages
选项通知 Distutils 处理(构建、分发、安装等)在 packages
列表中提及的每个包中找到的所有纯 Python 模块。
包名称和文件系统的目录之间必须保持对应关系。即 distutils
包即相对于分发包根目录的 distutils
目录。
因此,当在 setup 脚本中写 packages = ['foo']
时,你就承诺了 Distutils 将在相对于你的 setup 脚本所在目录下找到一个文件 foo/__init__.py
。 如果违反此承诺,Distutils 将发出警告但无论如何仍会处理损坏的包。
如果你使用不同的约定来布局你的源目录,那也没有问题:只需提供 package_dir
选项来告知 Distutils 你的约定。
例如,假设你将所 Python 源代码保存在 lib
下,以便“根包”中的模块(即不属于任何包的模块)位于 lib
中,foo
包中的模块位于 lib/foo
中,依此类推。 那么你应当编写:
package_dir = {'': 'lib'}
在你的 setup 脚本中,这个字典的键是包名,空的包名代表根包。 字典的值是相对于你的发布包根目录的目录名。
在本例中,当你声明 packages = ['foo']
时,你将承诺存在 lib/foo/__init__.py
这个文件。
另一种可能的约定是将 foo
包直接放在 lib
中,将 foo.bar
包放在 lib/bar
中等等。 这可以在 setup 脚本中写为
package_dir = {'foo': 'lib'}
package_dir
字典中的 package: dir
条目隐式地应用于 package 以下的所有包,因而在这里 foo.bar
的情况会被自动地处理。
在这个示例中,使用 packages = ['foo', 'foo.bar']
告知 Distutils 要查找 lib/__init__.py
和 lib/bar/__init__.py
。 (请记住虽然 package_dir
会递归地应用,但你必须在 packages
中显式地列出所有的包:Distutils 不会 递归地扫描你的源代码树来查找任何带有 __init__.py
文件的目录。)
文件路径可能是这样的:
/path/to/your/project/
|-- setup.py
|-- lib/
| |-- __init__.py # foo包的初始化文件
| |-- bar/
| |-- __init__.py # foo.bar包的初始化文件
值得注意的是,你通常不需要在packages
中显式列出子包,除非有特殊的理由。setuptools
有一个名为find_packages()
的函数,它可以自动发现项目中的所有包。因此,更常见的做法是使用find_packages()
而不是手动列出所有的包。例如:
from setuptools import setup, find_packagessetup(# ... 其他参数 ...packages=find_packages(where='lib'), # 从lib/目录查找所有的包package_dir={'': 'lib'}, # 告诉setuptools源代码在lib/目录下
)
列出单独的模块
对于一个小型的模块分发包,你可能会倾向于列出所有的模块而不是只列出包 — 特别是在“根包”中只有一个模块(即根本不存在包)的情况下。
py_modules = ['mod1', 'pkg.mod2']
这描述了两个模块,其中一个是在 “root” 包中,另一个是在 pkg
包中。 同样地,默认的包/目录布局表明这两个模块位于 mod1.py
和 pkg/mod2.py
中,并且也存在 pkg/__init__.py
。 同样地,你可以使用 package_dir
选项来重载包/目录的对应关系。
描述扩展模块
与纯模块不同,仅仅列出模块或包并期望 Distutils 出去找到正确的文件是不够的;必须指定扩展名、源文件和任何编译/链接要求(包括要链接的目录、库等)。
所有这些都是通过 setup()
的另一个关键字参数 ext_modules
选项实现的。
ext_modules
是 由 Extension
实例组成的列表,每个实例描述一个扩展模块。
假设你的发行版包括一个扩展模块,名为 foo
并由 foo.c
实现。 如果不需要编译器/链接器的额外指令,描述这个扩展很简单:
Extension('foo', ['foo.c'])
Extension
类可以与 setup()
一起从 distutils.core
导入。 因此,只包含这一个扩展而不包含其他扩展的模块分发版的安装脚本可能是:
from distutils.core import setup, Extension
setup(name='foo',version='1.0',ext_modules=[Extension('foo', ['foo.c'])],)
Extension
类(实际上,由 build_ext 命令实现的底层扩展构建机制)在描述 Python 扩展时支持很大的灵活性。
distutils也支持创建二进制包,用户无需编译器而distutils就能安装扩展。
一个distutils包包含了一个驱动脚本 setup.py
。这是个纯Python文件,大多数时候也很简单,看起来如下:
from distutils.core import setup, Extensionmodule1 = Extension('foo',sources = ['foo.c'])setup (name = 'PackageName',version = '1.0',description = 'This is a demo package',ext_modules = [module1])
通过文件 setup.py
,和文件 foo.c
,运行如下
python setup.py build
这会编译 foo.c
,然后产生一个扩展模块叫做 foo
在目录 build
里。依赖于系统,模块文件会放在某个子目录形如 build/lib.system
,名字可能是 foo.so
或 foo.pyd
。
在文件 setup.py
里,所有动作的入口通过 setup
函数。该函数可以接受可变数量个关键字参数,上面的例子只使用了一个子集。特别需要注意的例子指定了构建包的元信息,以及指定了包内容。通常一个包会包括多个模块,就像Python的源码模块、文档、子包等。
如上例子函数 setup()
的 ext_modules
参数是一列扩展模块,每个是一个 Extension
类的实例。例子中的实例定义了扩展命名为 foo
,从单一源码文件构建 foo.c
。
更多时候,构建一个扩展会复杂的多,需要额外的预处理器定义和库。如下例子展示了这些。
from distutils.core import setup, Extensionmodule1 = Extension('foo',define_macros = [('MAJOR_VERSION', '1'),('MINOR_VERSION', '0')],include_dirs = ['/usr/local/include'],libraries = ['tcl83'],library_dirs = ['/usr/local/lib'],sources = ['foo.c'])setup (name = 'PackageName',version = '1.0',description = 'This is a demo package',author = 'Randy Heng',author_email = 'Randy@heng.com.cn',url = 'https://docs.python.org/extending/building',long_description = '''
This is really just a demo package.
''',ext_modules = [module1])
例子中函数 setup()
在调用时额外传递了元信息,是推荐发布包构建时的内容。
对于这个扩展,其指定了预处理器定义,include目录,库目录,库名称。
依赖于编译器,distutils还会用其他方式传递信息给编译器。例如在Unix上,结果是如下编译命令
gcc -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -fPIC -DMAJOR_VERSION=1 -DMINOR_VERSION=0 -I/usr/local/include -I/usr/local/include/python2.2 -c foo.c -o build/temp.linux-i686-2.2/foo.ogcc -shared build/temp.linux-i686-2.2/foo.o -L/usr/local/lib -ltcl83 -o build/lib.linux-i686-2.2/foo.so
下面详细解说一下 Extension 扩展。
扩展名和软件包
构造器的第一个参数始终是扩展的名称,包括任何包名称。 例如:
Extension('foo', ['src/foo1.c', 'src/foo2.c'])
描述根包中的一个扩展:
from setuptools import Extension, setupsetup(ext_modules=[Extension(name="pkg.foo", # as it would be imported# may include packages/namespaces separated by `.`sources=['src/foo1.c', 'src/foo2.c'], # all sources are compiled into a single binary file),]
)
描述了 pkg
包中的相同扩展。 在这两种情况下,源文件和生成的目标代码是相同的;唯一的区别是所产生的扩展处在文件系统中的何处(相应地在 Python 的命名空间层级结构中处在何处)。
如果在同一个包中(或在同一基本包下)有多个扩展,请使用 setup()
的关键字参数 ext_package
。 例如:
setup(...,ext_package='pkg',ext_modules=[Extension('foo', ['foo.c']),Extension('subpkg.bar', ['bar.c'])],)
将把 foo.c
编译为扩展 pkg.foo
,把 bar.c
编译为 pkg.subpkg.bar
。
扩展的源文件
Extension 构造器的第二个参数是源文件列表。
由于 Distutils 目前只支持 C、C++ 和 Objective-C 扩展,因此这些扩展通常是 C/C++/Objective-C 源文件。 (请确保使用适当的扩展名来区分 C++ 源文件: .cc
和 .cpp
看来能够同时被 Unix 和 Windows 编译器所识别。)
但是,可以在列表中包括 SWIG 接口 (.i
) 文件;build_ext 命令知道如何处理 SWIG 扩展:它将在接口文件上运行 SWIG,并将生成的 C/C++ 文件编译为你的扩展。
尽管有此警告,SWIG的选项目前可以这样传递:
setup(...,ext_modules=[Extension('_foo', ['foo.i'],swig_opts=['-modern', '-I../include'])],py_modules=['foo'],)
或者在命令行上,像这样:
> python setup.py build_ext --swig-opts="-modern -I../include"
在某些平台上,可以包含由编译器处理并包含在扩展中的非源文件。目前,这只是指Visual C++的Windows消息文本(.mc
)文件和资源定义(.rc
)。这些文件将被编译为二进制资源(.res
)文件,并链接到可执行文件中。
预处理器选项
如果需要指定要搜索的 include 目录或预处理器宏来define/undefine
,那么Extension
的三个可选参数将有所帮助:include_dirs
、define_macros``和``undef_macros
。
头文件
例如,如果扩展需要分发根目录下的 include
目录中的头文件,请使用 include_dirs
选项:
Extension('foo', ['foo.c'], include_dirs=['include'])
你可以在那里指定绝对目录;如果你知道你的扩展只能在安装到 /usr
的 X11R6
的 Unix
系统上构建,那么你就可以:
Extension('foo', ['foo.c'], include_dirs=['/usr/include/X11'])
如果你计划分发代码,应该避免这种不可移植的用法,最好像这样编写C代码:
#include <X11/Xlib.h>
如果你需要包含来自其它 Python 扩展的头文件,你可以利用这样一个事实:即 Distutils install_headers 命令以一致的方式安装头文件。
例如,Numerical Python
头文件 (在标准 Unix 安装版上) 会安装到 /usr/local/include/python1.5/Numerical
(具体位置将根据你的平台和 Python 安装版而有所不同)。
由于在构建Python 扩展时,Python include 目录 — 在本例中为 /usr/local/include/python1.5
— 总是会包含在搜索路径中,因此最好的做法是像这样编写 C 代码:
#include <Numerical/arrayobject.h>
但是,如果必须将 Numerical
include 目录放在头文件搜索路径中,则可以使用 Distutils distutils.sysconfig
模块找到该目录:
from distutils.sysconfig import get_python_inc
incdir = os.path.join(get_python_inc(plat_specific=1), 'Numerical')
setup(...,Extension(..., include_dirs=[incdir]),)
尽管这是非常可移植的——无论平台如何,它都可以在任何Python安装中工作——但以合理的方式编写C代码可能更容易。
预处理器宏
可以使用define_macros
和undef_macros
选项 define
和 undefine
预处理器宏。
define_macros
采用一个(name, value)
元组列表,其中name
是要定义的宏的名称(字符串),value
是其值:字符串或“None
。(将宏FOO
定义为None
相当于C源代码中的空#define FOO
:对于大多数编译器,这会将FOO
设置为字符串1
。)
undef_macros
只是一个undefine
的宏列表。
例如:
Extension(...,define_macros=[('NDEBUG', '1'),('HAVE_STRFTIME', None)],undef_macros=['HAVE_FOO', 'HAVE_BAR'])
相当于将其放在每个C源文件的顶部
#define NDEBUG 1
#define HAVE_STRFTIME
#undef HAVE_FOO
#undef HAVE_BAR
库选项
可以指定在构建扩展时要链接的库,以及搜索这些库的目录。
-
libraries
选项是要链接的库列表 -
library_dirs
是要在链接时搜索库的目录列表 -
runtime_library_dirs
是要在运行时搜索共享(动态加载)库的目录的列表
例如,如果你需要链接到目标系统上标准库搜索路径中已知的库
Extension(...,libraries=['gdbm', 'readline'])
如果你需要链接到非标准位置的库,则必须将该位置包含在 library_dirs 中:
Extension(...,library_dirs=['/usr/X11R6/lib'],libraries=['X11', 'Xt'])
其它选项
-
optional
:选项是一个布尔值;如果它为真值,则扩展中的生成失败不会中止生成过程,而是简单地不安装失败的扩展。 -
extra_objects
:选项是要传递给链接器的对象文件列表。 这些文件不能有扩展名,因为使用的是编译器的默认扩展名。 -
extra_compile_args
和extra_link_args
:可用于为相应的编译器和链接器命令行指定额外的命令行选项。 -
export_symbols
:仅在 Windows 上适用。 它可以包含要导出的符号(函数或变量)列表。 生成编译的扩展时不需要此选项:Distutils 会自动将initmodule
添加到导出的符号列表中。 -
depends
:是扩展所依赖的文件列表(例如头文件)。 build 命令将调用源上的编译器以重新生成扩展(如果自上一次生成以来此文件上的任何扩展已被修改)。
安装软件包数据
通常,需要将额外的文件安装到软件包中。这些文件通常是与软件包的实现密切相关的数据,或者是包含使用软件包的程序员可能感兴趣的文档的文本文件。这些文件被称为 package data。
可以使用 setup()
函数的关键字参数 package_data
将软件包数据添加到软件包中。 该值必须是从软件包名到应该复制到软件包中的相对路径名列表的映射。
路径被解释为相对于包含软件包的目录(如果合适,将使用来自 package_dir
映射的信息);也就是说,这些文件应该是软件包源目录中的一部分。 它们也可能包含 glob
模式。
路径名称可以包含目录部分;任何必要的目录都将在安装过程中创建。
例如,如果一个软件包应该包含一个有多个数据文件的子目录,那么这些文件可以在源树中按如下方式排列
setup.py
src/mypkg/__init__.pymodule.pydata/tables.datspoons.datforks.dat
对 setup()
的相应调用可能是
setup(...,packages=['mypkg'],package_dir={'mypkg': 'src/mypkg'},package_data={'mypkg': ['data/*.dat']},)
安装其它文件
data_files
选项可用于指定模块分发所需的其它文件:配置文件、消息目录、数据文件,以及任何不属于前述类别的文件。
data_files
以如下方式指定 (directory, files) 对的序列:
setup(...,data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),('config', ['cfg/data.cfg'])],)
序列中的每队(directory,files)指定安装目录和要在其中安装的文件。
files 中的每个文件名都是相对于软件包源发行版顶部的 setup.py
脚本进行解释的。 请注意,您可以指定安装数据文件的目录,但不能重命名数据文件本身。
directory 应该是一个相对路径。它是相对于安装前缀来解释的(Python 的 sys.prefix
用于系统安装;site.USER_BASE
用于用户安装)。 Distutils 允许将 directory 作为一个绝对的安装路径,但这是不鼓励的,因为它与 wheel 封装格式不兼容。不使用来自 files 的目录信息来确定已安装文件的最终位置;只使用文件的名称。
你可以将 data_files
选项指定为一个简单的文件序列,而无需指定目标目录,但不建议这样做,在这种情况下,install 命令将打印警告。 要将数据文件直接安装到目标目录中,应提供一个空字符串作为目录。
打包so
将私有的库和package一起打包。 比如,需要把librandy.so
作为数据打包,文件目录如下:
.
├── pkg_name
│ ├── __init__.py
│ └── randy_wrapper
│ ├── __init__.py
│ └── librandy.so
└── setup.py
setup.py中设置:
setup(...package_data={'pkg_name': ['randy_wrapper/librandy.so']},
)
require
install_requires
这个参数把孤立的Python包按照依赖关系组织起来,在python setup.py install
或pip install .
时自动安装。
在install_requires
流行以前,往往会使用requrements.txt
来声明依赖:
docutils >= 0.3
Django >= 1.11, != 1.11.1, \<= 2
requests[security, socks] >= 2.18.4
setuptools==38.2.4
install_requires 就可以这么要求:
setup(...install_requires=['argparse','setuptools==38.2.4','docutils >= 0.3','Django >= 1.11, != 1.11.1, <= 2','requests[security, socks] >= 2.18.4',],
)
其中,'argparse'
,只包含包名。 这种形式只检查包的存在性,不检查版本。 方便,但不利于控制风险。
指定包名是必须的,而版本控制与可选依赖,则是高级形式。
如果其中某些依赖,在官方的PyPI中不存在,而是发布在某些私有源中,则需要指定dependency_links
。
setup(...dependency_links=['https://pypi.python.org/simple','http://my.company.com/pypi/',...],
)
有时,一个Python包,在不同操作系统,需要使用不同的依赖包。 比如,pykerberos只支持Linux平台,而Windows平台则需要使用 winkerberos,因此,可以使用 setuptools 支持的方式:
install_requires=['winkerberos; platform_system == "Windows"','pykerberos; platform_system == "Linux"',],
extras_require
extras_require
指定了可选的功能与依赖。 某些特殊的、偏门的功能,可能绝大多数用户不会去使用。
上述不同平台的依赖包也可以在 extras_require 中指定:
extras_require={':sys_platform == "win32"': ['winkerberos'],':"linux" in sys_platform': ['pykerberos'],}
这里使用了sys.platform
作为平台的区分方式。 这种做法广为流传,也很有效,但更推荐使用platform.system
。
extras_require
需要一个dict,其中按(自定义的)功能名称进行分组,每组一个列表,与install_requires
规则相同。
setup(...extras_require={'security': ['pyOpenSSL>=0.14', 'cryptography>=1.3.4', 'idna>=2.0.0'],'socks': ['PySocks>=1.5.6, !=1.5.7'],},
)
附加元数据
安装脚本可以包括除了名称和版本之外的附加元数据。这些信息包括:
元数据 | 描述 | 值 |
---|---|---|
name | 包名称 | 短字符串 |
version | 此发布的版本 | 短字符串 |
author | 软件包作者的姓名 | 短字符串 |
author_email | 软件包的作者的电子邮件地址 | 电子邮件地址 |
maintainer | 软件包维护者的名字 | 短字符串 |
maintainer_email | 软件包维护者的电子邮件地址 | 电子邮件地址 |
url | 软件包的网址 | 网址 |
description | 软件包的简短摘要说明 | 短字符串 |
long_description | 软件包的详细说明 | 长字符串 |
download_url | 可以下载软件包的网址 | 网址 |
classifiers | 分类列表 | 字符串列表 |
platforms | 平台清单 | 字符串列表 |
keywords | 关键字列表 | 字符串列表 |
license | 软件包许可证 | 短字符串 |
将扩展模块化
对于cpp 或者 cuda 的扩展,可以编写不同的扩展函数,实际根据不同代码,将 包含文件及路径传递进去即可:
cuda 扩展
def make_cuda_ext(name,module,sources,sources_cuda=[],extra_args=[],extra_include_path=[]):define_macros = []extra_compile_args = {'cxx': [] + extra_args}if torch.cuda.is_available() or os.getenv('FORCE_CUDA', '0') == '1':define_macros += [('WITH_CUDA', None)]extension = CUDAExtensionextra_compile_args['nvcc'] = extra_args + ['-D__CUDA_NO_HALF_OPERATORS__','-D__CUDA_NO_HALF_CONVERSIONS__','-D__CUDA_NO_HALF2_OPERATORS__','-O2',]sources += sources_cudaelse:print('Compiling {} without CUDA'.format(name))extension = CppExtensionraise EnvironmentError('CUDA is required to compile!')return extension(name='{}.{}'.format(module, name),sources=[os.path.join(*module.split('.'), p) for p in sources],include_dirs=extra_include_path,define_macros=define_macros,extra_compile_args=extra_compile_args)
C/C++ 扩展
def make_cpp_ext(name,module,sources,extra_args=[],extra_include_path=[],extra_libraries=[],extra_library_dirs=[]):define_macros = []extra_compile_args = {'cxx': [] + extra_args}return CppExtension(name='{}.{}'.format(module, name),sources=[os.path.join(*module.split('.'), p) for p in sources],include_dirs=extra_include_path,define_macros=define_macros,extra_compile_args=extra_compile_args,libraries=extra_libraries,library_dirs=extra_library_dirs)
调用上述模块
setup(name='san-jie-ji-yuan',version="1.0.0",description='',install_requires=[],author='Randy',author_email='',license='',packages=find_packages(exclude=['tools']),cmdclass={'build_ext': BuildExtension},ext_modules=[make_cpp_ext(name='tools',module='tools.makemoney',sources=['src/MakeMoney.cpp',]),make_cuda_ext(name='randy_cuda',module='tools.cudatools',sources=['src/make_money.cpp','src/randy_api.cpp','src/work_hard.cpp','src/randy_kernel.cu',])],
)
发布扩展模块
当一个扩展已经成功的构建过,有三种方式使用。
- 最终用户通常想要安装模块,可以这么运行
python setup.py install
若你的项目还处于开发阶段,频繁的安装模块,会非常麻烦。
可以使用以下命令,在系统环境中创建一个软链接指向包实际所在目录,使得修改包之后不用再安装就能生效,便于调试。
python setup.py develop
- 模块维护者应该制作源码包;要实现可以运行
python setup.py sdist
使用 sdist 将根据当前平台创建默认格式的存档。在类 Unix 平台上,将创建后缀后为 .tar.gz
的 gzip 压缩的tar文件分发包,而在Windows上为 ZIP 文件。
安装压缩好的包: easy_install randy.tar.gz
当然,可以指定发布包的格式:
python setup.py sdist --formats=gztar,zip
有些情况下,需要在源码发布包里包含额外的文件;这通过 MANIFEST.in
文件实现)
如果希望归档文件的所有文件归 root 拥有:
python setup.py sdist --owner=root --group=root
- 如果源码发行包成功构建了,维护者也可以创建二进制发行包。依赖于平台,一个可用的命令如下
python setup.py bdist_wininst # Windows 打包成 exe 这样的二进制软件包
python setup.py bdist_rpm # 在 Linux 中,构建 rpm 包
python setup.py bdist_dumb
python setup.py bdist_egg # 使用 easy_install 或者 pip 来安装离线包。你可以将其打包成 egg 包
Reference
-
Building and Distributing Packages with Setuptools : https://setuptools.pypa.io/en/latest/setuptools.html
-
花了两天,终于把 Python 的 setup.py 给整明白了:https://blog.csdn.net/xiaoxianerqq/article/details/113882592
-
Distributing Python Modules:https://docs.python.org/tr/3.8/distutils/
-
Wheel vs Egg:https://docstestmark.readthedocs.io/en/latest/discussions/wheel-vs-egg.html?highlight=wheel%20vs%20egg
-
构建C/C++扩展 : https://docs.python.org/zh-cn/3.7/extending/building.html
-
Extension
: https://docs.python.org/zh-cn/3.8/distutils/apiref.html#distutils.core.Extension -
Writing the Setup Script : https://docs.python.org/3.8/distutils/setupscript.html
-
API参考引用 : https://docs.python.org/3.8/distutils/apiref.html#distutils.core.Extension
-
Data Files Support:https://setuptools.pypa.io/en/latest/userguide/datafiles.html
-
Python 包构建教程:https://www.cnblogs.com/cposture/p/9029023.html
-
在Python代码中调用C/C++代码:https://note.qidong.name/2018/01/call-cpp-in-python/
-
setup.py里的几个require:https://note.qidong.name/2018/01/python-setup-requires/
-
设置Python的wheel包自动根据系统安装不同的依赖:https://note.qidong.name/2020/03/pip-install-by-platform/
-
TORCH.UTILS.CPP_EXTENSION:https://pytorch.org/docs/master/cpp_extension.html