Advertisement

python | setup.py里有什么?

阅读量:

setup.py里有什么?

文章目录

  • 在setup.py中包含哪些内容?
    • C/C++扩展功能概述
      • 关于gcc/g++编译器参数说明:

        • 设置Visual Studio项目
        • 使用Cmake进行源代码管理
        • 安装setup.py支持的C/C++扩展模块
      • 设置Visual Studio项目时,请确保配置正确的编译选项以支持C/C++代码生成。

      • 在使用Cmake时,请参考相关文档以获取完整的编译指令集合。

      • 安装setup.py后,请按照指导文档逐步完成C/C++扩展模块的配置与调试。

      • 为什么需要分发打包?

      • Distutils

        • 一个简单的例子
        • 通用的 Python 术语

借助Setuptools构建和发布软件包

列出所有软件包:
列出单独的模块:
详细描述扩展模块:
** 扩展名称及其所属软件包:
扩展的源文件位置:
预处理选项设置:
** 头文件路径:
预处理宏定义:
(此处应包含具体的宏定义内容)

复制代码
* 库选项
* 其它选项

  * 安装软件包数据
  * * 安装其它文件
* 打包so

  * require
  * * install_requires
* extras_require

  * 附加元数据
  * * 将扩展模块化
* * cuda 扩展

* C/C++ 扩展
* * 调用上述模块

  * 发布扩展模块

* Reference

C/C++扩展总结

对于C或C++ 代码,在编译时需要配置这些头文件的位置以及代码存放的位置,并使用公共接口来实现功能的一致性需求。这样编译器才能准确识别并关联自己编写的所有头文件及其对应的源码内容,并确保能够正确定位所使用的外部公共接口及其相关的符号定义。也就是说……公共接口其实就是将多个类似的函数打包在一起形成的模块化结构体。

gcc/g++的编译参数:

-I (i 的大写):指定头文件的所在的目录,可以使用相对路径。

-L标识符指定要链接的库所属目录。
指定参数-L后面跟路径变量名时,默认值为当前工作目录下的共享库。
通常情况下, 编译器会搜索 /usr/lib 和 /usr/local/lib 目录, 因此无需显式指定。
若需自定义共享库的位置, 可以通过提供具体的路径参数来实现.

-L. :表示要链接的库在当前目录

-l(其中小写的l):用于标识所需链接的库名称。特别指出,并非直接指代库文件名,在实际操作中通常省略lib前缀(因为遵循一定的命名规则)。例如,在Linux系统中通常会遇到的情况是有一个名为 librandy.so 的共享对象模块。

-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 hello
    
    gcc -o hello main.c -static -L. –lhellos

Windows Visual Studio

VC++目录:

包含目录:寻找#include中的randyx.h的搜索目录

库目录:寻找.lib文件的搜索目录

C/C++:

操作将从"常规"转换为"附加包含目录设置":定位到包括"randyx.h"在内的所有头文件所在的位置(每一个匹配项对应一个名为"randyX"的文件夹)。这些文件夹存储了编译所需的必要头文件信息。在使用时直接将这些头文件包含进去就足够了。

链接器:

常规->附加库目录:寻找.lib文件的搜索目录

依赖项说明:软件库(C++软件库将函数和类的声明存储在.h文件中,并将实现部分存储在.cpp和.ccc文件中。编译完成后,.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以及库位置lib directories.

复制代码
    from distutils.core import setup, Extension
    
    module1 = 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 电脑上安裝可执行文件的行为相似;同样方便后续通过快捷键直接使用;类似地;在 Linux 电脑上通过 apt-get 命令安裝各种软件包的行为也是一样;这些可执行文件或安裝的软件包都是他人预先打包好了供我们使用的程序或库;而打包的过程就是将这些资源进行组织和分发的过程

编译者注:以下是经过重新表述的版本

Distutils

distutils 是 Python 的官方库(即发行系统中使用的工具),它是一个发行工具,并且所有后续打包工具都是基于它进行开发的。

该工具非常易于使用,并旨在满足模块开发者及安装第三方软件包用户的便利需求。开发者的义务不仅限于编写高质量、易于理解的代码,还需撰写详尽且友好的用户指南。
此外,请确保编写可靠、良好文档并附有详尽的用户手册。

  • 生成一个配置文件(按照Python惯例编写),命名为setup.py
  • 可选步骤:创建相应的配置文件。
  • 生成源代码发行版本。
  • 可选步骤:构建好的二进制版本。

一个简单的例子

setup 脚本通常很简单,尽管是用 Python 编写的,它能干的事情没有限制。

如果只是想部署一个名为 foo 的程序代码库,在 foo.py 文件中就可以非常简单。

复制代码
    from distutils.core import setup
    setup(name='foo',
      version='1.0',
      py_modules=['foo'],
      )

注意:

  • setup() 函数将接收 Distutils 提供的主要信息作为关键字参数。
    • 这些关键字参数主要包含两类内容:一类是包的基本元数据(如名称与版本号),另一类则是关于包内具体细节的信息(例如,在这种情况下指的是纯 Python 模块列表)。
    • 通常情况下,默认情况下模块是由其名字而非文件名来标识的。(类似的规则也适用于包与扩展)
    • 建议尽可能提供更多的元数据信息尤其是开发者姓名、邮箱地址以及项目URL等关键细节。

为该模块创建一个源码发布版本:

复制代码
    python setup.py sdist

sdist会生成一个打包文件(如Unix系统中的tarballs或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 和 Solaris [p]kgtool/CommandLineBdist_pkgtool, HP-UX swinstall/CommandLineBdist_SDUX.

复制代码
    python setup.py bdist_rpm

该命令依赖于rpm可执行文件,并因此仅能在基于RPM架构的系统上正常运行(例如Red Hat Linux、SuSE Linux或Mandrake Linux)

可以随时运行以下命令,以便了解当前可用的分发格式:

复制代码
    python setup.py bdist --help-formats
    
    List 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系统中,则使用DLL格式,并带有.pyd扩展名。然而,在Jython环境中,则直接采用Java class文件作为Python扩展。需要注意的是,在目前版本中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 serves as a powerful toolset for enhancing Python distutils, aiding in the building and distribution of Python packages, particularly those that rely on other packages.

那些通过使用setuptools构建与发布并被分发到系统中的Python包,在用户眼中看起来就像是基于distutils的普通Python包。

源码包与二进制包

以源码包的方式发布

源码包安装的过程是先解压文件夹中的所有文件包(如.tar.gz.zip),然后编译这些构建好的项目(如.so.dll),最后完成整个项目的部署工作,并且这个过程可以在不同的操作系统上实现。其本质是一个压缩包,在软件工程中被广泛采用作为打包工具和分发介质,在实际应用中通常会遇到的问题包括如何高效地管理依赖关系以及如何确保兼容性问题等

格式 后缀
zip .zip
gztar .tar.gz
bztar .tar.bz2
ztar .tar.Z
tar .tar

以二进制包形式发布

二进制包的安装流程省去了编译环节,在解压后直接完成安装操作;因此相较于源码包而言运行效率更高

因为多种平台生成的包不具有通用性,因此,在发布之前必须预先为各个不同的平台构建相应的可执行文件。

二进制包的常见格式有:

格式 后缀
egg egg
wheel .whl

eggs VS wheels

Wheel 的出现其目的是为了替代Egg,并非单纯的 zip 包而是等同于一个 zip 文件,在 Python 环境中它被广泛认为是二进制包的标准格式。

Wheel 和 Egg 的主要区别:

  • Wheel 的定义明确由官方PEP427规范制定,而Egg未有此规范.
  • Wheel被视为一种打包格式,与标准Python发行包机制相似,但其内部机制更为灵活.
    即使在不压缩的情况下,Egg依然能够直接导入使用.
  • Wheel文件中不会存在.pyc文件,因此当发行版仅包含原始Python代码(无编译扩展)且兼容Python2与3时,Wheel格式类似于sdist,提供了一种通用的解决方案.
  • Wheel遵循与PEP376兼容的.distrib目录结构,而Egg则采用独立的鸡蛋目录(.egg-info)进行管理.
  • Wheel具有更为复杂的命名规则,单个wheel存档即可明确指示其与多个Python语言版本、实现环境以及兼容性相关的属性.
  • 每个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 pkg
      File 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在完成构建任务时自动分发软件;在完成安装任务时自动部署到服务器上。

该软件包名称与文件系统的目录之间必须保证一一对应关系。例如,在使用Python时,默认情况下distutils 包将被配置为与位于分发包根目录下的同名distutils 目录进行关联。

因此,在编写 packages = ['foo'] 这样的设置指令时,在 setup 脚本中你将保证 Distutils 会查找位于当前目录下的 foo/__init__.py 文件。若违反这一约定条件,则 Distutils 将会发出警告信息但依然会继续处理可能损坏的包的情况。

即使采用不同的约定来组织你的源目录结构也不会引起麻烦:只需告诉 Distutils 提供package_dir 参数即可

比如你将那些Python 源码被保存在 lib 下。为了使属于根目录的模块位于 lib 中,“根包”中的模块则位于 lib 子目录中。而像 foo 包这样的层级结构,则意味着其所属的模块会被放置在 lib/foo 子目录中。

复制代码
    package_dir = {'': 'lib'}

在你的 setup 脚本中,在该字典中使用的每个字段都是一个软件包名称标识符,在这种情况下如果字段为空,则表示该软件包位于根目录位置上;而该字段的实际取值则对应于与你当前发布的库所处层级相关的子目录路径

当你指定 packages = ['foo']

你将保证包含

这个特定的Python文件

这个特定的Python文件

这个特定的Python文件

这个特定的Python文件

另一种可能的配置方案是建议将 foo 包直接放置于库目录下,并在子目录中安排其包含关系(例如将 foo.bar 包放置于子目录中),以此类推等建议可参考文献中的相关指导方针

复制代码
    package_dir = {'foo': 'lib'}

package_dir 字典中定义的 package: dir 条目会被默认应用到 package 以下的所有包中,在这里 foo.bar 的情况会自动被处理。

在这个示例中,请指定 packages = ['foo', 'foo.bar'] 来指示Distutils去查找这些路径中的文件。请注意,默认情况下会递归应用package_dir参数;因此,在packages参数中必须明确列出所有相关的包。

文件路径可能是这样的:

复制代码
    /path/to/your/project/
    |-- setup.py
    |-- lib/
    |   |-- __init__.py  # foo包的初始化文件
    |   |-- bar/
    |       |-- __init__.py  # foo.bar包的初始化文件

需要注意的是,在Python的packages参数中,默认情况下并不需要显式地列出所有子包(unless there are specific requirements),而setuptools.find_packages()这个函数能够自动化地发现项目的全部依赖包。因此,在常规操作中推荐优先使用这个自动化功能而非手动列出所有依赖的包。举个例子来说,在实际项目中使用find_packages()可以显著提升效率和维护性。

复制代码
    from setuptools import setup, find_packages
    
    setup(
    # ... 其他参数 ...
    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 中。 若编译器及链接器无需额外配置即可处理该扩展,则其描述较为简洁:此模块需完成以下功能:

  • 通过动态链接机制完成代码加载过程;
  • 提供必要的公共接口供主程序调用;
  • 在初始化阶段执行特定操作;
  • 在退出时执行清理任务。
    注释说明:
    foo 模块需遵循以下开发规范:
  1. 所有新增功能均需在原有库函数基础上进行封装;
  2. 各公共接口需附带详细文档;
  3. 存储空间管理必须严格遵守规定;
  4. 禁止引入外部依赖项。
复制代码
    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构建的驱动程序包含了setup.py文件。它是一个完全由Python编写的文件,在大多数情况下非常简单,并且通常情况下非常简单。例如:

复制代码
    from distutils.core import setup, Extension
    
    module1 = 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 的一个子目录中(形如 build/lib.system),其名称通常为 \text{.so} 文件或 \text{.pyd} 文件形式。

位于文件 setup.py 中的所有操作入口均位于 setup 函数中。该函数能够接受任意数量的关键字参数,并且这些参数允许用户定义特定的行为或配置选项(如前所述)。值得注意的是那些仅使用部分实例的情况;特别是在某些高级配置下(例如),需要指定构建元信息以及所包含的内容(如前所述)。通常来说,在大多数情况下这些软件包会包含多个模块,并类似于Python中的源码模块、文档以及子库等

如上所示的函数setup()中的参数ext_modules是一组扩展模块,在这种情况下每一个都是一个.Extension类实例

通常情况下,在搭建一个变得非常复杂的事物时,会被迫引入额外的预处理器定义以及依赖库的支持。

复制代码
    from distutils.core import setup, Extension
    
    module1 = 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])

在调用过程中额外提供了元信息,在构建发布包的过程中通常被认为是最佳实践

对于这个扩展,其指定了预处理器定义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.o
    
    gcc -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, setup
    
    setup(
    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 包内的相同扩展被描述如下:在两种情况下——源代码及其生成的目标代码完全一致;唯一差异在于所生成的扩展在 filesystem 中的具体位置(相应地,在 Python 的 namespace 层级结构中所在的位置)。

如果在一个包中(或同一基本包下)存在多个扩展,则应采用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 语言编写而成的源代码;请注意,在区分 .cc 和 .cpp 文件时,请确保使用适当的命名策略,并能被 Unix 和 Windows 编译器识别。

但是,在列表中包含 SWIG 接口 (.i) 项;build_ext 命令能解析这些接口,并将生成的 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消息文本及其相关资源定义。开发环境中的这些文件会被编译生成二进制资源,并与可执行程序进行链接。

预处理器选项

如果要设置包含目录以及查找预处理器宏来进行\texttt{define/undefine}操作,请确保正确配置以下三个参数:\texttt{include\_dirs}\texttt{define\_macros}以及\texttt{undef\_macros}

头文件

举个例子来说,在根目录下的 include 目录中的头文件需要进行分发时,请建议采用 include_dirs 选项:

复制代码
    Extension('foo', ['foo.c'], include_dirs=['include'])

你可以在这里设置绝对目录;如果知道你的扩展仅限于在安装至 /usr 上运行的 X11R6Unix 系统中构建,则你需要:

复制代码
    Extension('foo', ['foo.c'], include_dirs=['/usr/include/X11'])

如果你打算分发代码,请尽量避免采用非移植兼容的方式,并建议采用以下C代码结构:

复制代码
    #include <X11/Xlib.h>

如果需要包含来自其他Python扩展的头文件,则可以依据以下依据:Distutils install_headers命令以一致的方式安装头文件。

例如,在标准 Unix 安装版上使用的 Numerical Python 头文件会被安装到特定目录 /usr/local/include/python1.5/Numerical ,具体位置则取决于你的操作系统以及 Python 的安装版本。

在构建Python扩展时,在本例中为/usr/local/include/python1.5的Python include目录总是会被搜索路径所包含。因此推荐的做法是按照以下方式编写C代码:

复制代码
    #include <Numerical/arrayobject.h>

然而,在若要将 Numerical 包括在头文件搜索路径内时,则可借助 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]),
      )

虽然这种特性具有极高的可移植性,在各种平台上都能无缝运行——然而,在编写C代码时采取适当的方法会更加高效

预处理器宏

可以选择使用预定义的宏来配置系统参数。通过调用define_macrosundef_macros宏函数可以实现动态管理这些参数。

define_macros使用一个(name, value)列表 ,其中name表示要定义的宏名称(字符串),value为其对应的值:可为字符串或无值(即None)。将宏FOO设为None等同于在C源代码中使用空#define。大多数编译器会将其赋值为字符串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:这是一个布尔值;如果该参数设置为True(即存在真值),则在发生扩展中的生成失败时也不会终止生成流程,并且仅会跳过无法正常安装的扩展组件。

extra_objects 该选项的作用是指定传递给链接器的对象文件列表。这些对象不可以包含文件扩展名,并且由于编译器采用了默认的文件扩展名设置,默认情况下不带任何前缀或后缀

The extra_compile_args and extra_link_args variables are used to specify additional compiler and linker command-line options.

指定要在 Windows 系统上导出的符号表名为 export_symbols 。该参数允许指定需要从模块中导出的所有函数和变量。当构建扩展时不使用此参数时,默认情况下 Distutils 会添加 initmodule 到被导出的符号列表中。

depends 表示该扩展所基于的文件集合(其中包含头文件)。build 命令会使用源代码中的编译器重新编译该扩展(如果最近一次构建后该源代码中的任何包含的扩展已被修改)。

安装软件包数据

通常情况下, 必须将多余的文件放入软件包内. 多为与软件包实现紧密关联的数据或为程序员使用该软件包而提供的技术性文本资料. 因此这类文件被称作 package data.

可以通过 setup() 函数的关键字参数 package_data 实现软件包数据的添加。该值应为是从软件包名到应复制到软件包中的相对路径名列表的映射。

路径被视为相对于包含软件包的目录进行解析(若有条件的话,则会参考 package_dir 映射信息);即这些文件应属于软件包源目录中的成员。 它们可能包含基于 glob 的模式设置。

路径名称可以包含目录部分;任何必要的目录都将在安装过程中创建。

当一个软件包设计包含包含多个数据文件的子目录时

复制代码
    setup.py
    src/
    mypkg/
        __init__.py
        module.py
        data/
            tables.dat
            spoons.dat
            forks.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'])],
     )

序列中的每队( directoryfiles )指定安装目录和要在其中安装的文件。

在软件包源根目录下的每一个文件名都基于setup.py脚本进行了解析。

必须是基于安装前缀的位置信息。具体来说,在Python中,默认情况下sys.prefix指示系统级包的位置,默认情况下site.USER_BASE指示用户级包的位置。然而Distutils允许将此目录指定为一个绝对路径,并且这通常不被推荐因为它与wheel封装格式不兼容。相反地我们可以通过分析可选轮次包(package Wheels)中的文件名来确定已成功解压到本地磁盘上的轮件位置而不依赖于特定环境配置的信息

该选项允许你指定一个简单的文件序列(即不需要明确设置安装目录),但请注意不要这样做(因为这可能会导致警告信息出现)。
如果你想直接将数据文件放入目标目录中,则应提供一个空字符串作为安装参数(而不是默认的值)。

打包so

整合私有库与包为一个整体。例如,在实际操作中,默认情况下会将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模块,并依据其依赖关系构建一个层次分明的网络。通过使用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应用可能需要不同的依赖项。例如,在Linux平台上运行时可以使用pykerbosr(虽然目前不推荐),而在Windows平台通常会遇到wincerboes的问题。因此, 可以考虑通过安装并导入wincrboos库来解决兼容性问题

复制代码
       install_requires=[
        'winkerberos; platform_system == "Windows"',
        'pykerberos; platform_system == "Linux"',
    ],
extras_require

extras_require定义了可选的组件和依赖项。 那些独特且 niche 的功能可能会被大多数用户所忽略。

上述不同平台的依赖包也可以在 extras_require 中指定:

复制代码
    extras_require={
        ':sys_platform == "win32"': ['winkerberos'],
        ':"linux" in sys_platform': ['pykerberos'],
    }

这里采用了sys模块下的platform属性来作为平台区分的方式。 这一做法被广泛采用且可靠实用,并建议采用 platform.system这一更为便捷的方法。

extras_require由一个字典构成,在此字典中按照自定义的功能名称进行分类设置,在每个类别下设置一个列表以遵循与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 软件包许可证 短字符串
将扩展模块化

针对C++或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 = CUDAExtension
        extra_compile_args['nvcc'] = extra_args + [
            '-D__CUDA_NO_HALF_OPERATORS__',
            '-D__CUDA_NO_HALF_CONVERSIONS__',
            '-D__CUDA_NO_HALF2_OPERATORS__',
            '-O2',
        ]
        sources += sources_cuda
    else:
        print('Compiling {} without CUDA'.format(name))
        extension = CppExtension
        raise 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',
            ]
        )
    ],
    )

发布扩展模块

当一个扩展已经成功的构建过,有三种方式使用。

  1. 最终用户通常想要安装模块,可以这么运行
复制代码
    python setup.py install

若你的项目还处于开发阶段,频繁的安装模块,会非常麻烦。

通过以下命令,在系统环境中创建一个软链接指向包实际所在目录,并使修改后的包无需重新安装即可发挥作用。该操作有助于快速定位和修复问题。

复制代码
    python setup.py develop
  1. 模块维护者应该制作源码包;要实现可以运行
复制代码
    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
  1. 当源码发行包构建完成后,在线维护人员也可以生成相应的二进制发行包。基于不同的平台环境,以下是一个适用于不同平台的操作指令
复制代码
    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

Installing and distributing Python packages using setuptools is a common practice in software development.

花了两天时间终于完全搞懂了Python的setup.py文件:

耗时两天终于搞定了Python的设置流程:

用了两日时间彻底掌握了Python的setup.py配置方法:

经过一番努力后终于完成了对Python setup.py文件的理解:

花了两天的时间最终实现了对Python setup.py配置的学习目标:

Distributing Python Modules:https://docs.python.org/tr/3.8/distutils/

Wheels over Eggs:https://docstestmark.readthedocs.io/en/latest/discussions/wheel-vs-egg.html?highlight=wheel vs egg

构建C/C++扩展 : https://docs.python.org/zh-cn/3.7/extending/building.html

拓展

Creating the Setup Script : https://docs.python.org/3.8/distutils/setupscript.html

官方文档:https://docs.python.org/3.8/distutils/apiref.html#distutils.core.Extension

数据文件支持: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/

TORCHUTILSCPP_EXTENSION:可访问PyTorch官方文档中的相关说明

全部评论 (0)

还没有任何评论哟~