任何使用 Python 一段时间的人可能都已经接触过包。在 Python 术语中,包(或分发包)是一个或多个 Python 模块的集合,它们提供特定的功能。一般的概念类似于其他语言中的库。Python 包的一些特性使得处理它们有所不同。
Pip 和 PyPi
安装第三方 Python 包最常见的方法是使用包安装程序 pip,默认情况下提供。 Python 包索引 (PyPi) 是各种包的中心服务器,也是 pip 的默认来源。 Python 包包含指定包名称、版本和其他元信息的文件。 基于这些文件,PyPi 知道如何对包进行分类和索引。此外,这些文件可能包括 pip 处理的安装说明。
源码和二进制分发
Python 模块以多种格式分发,每种格式都有其优点和缺点。通常,这些格式可以分为两组。
源码分发 (sdist)
源码分发在 PEP 517 中定义,是带有文件扩展名 *.tar.gz
的 gzip 压缩的 tar 存档。该存档包含所有与包相关的源文件和安装说明。源码分发通常依赖于构建系统,例如 distutils 或 setuptools,这会导致安装期间执行代码。在安装时执行(任意)代码可能会引起安全问题。
对于 Python C/C++ 扩展,源码分发包含纯 C/C++ 文件。这些文件必须在安装时编译,因此必须存在适当的 C/C++ 工具链。
构建分发 (bdist)
相比之下,通常可以直接使用构建分发。构建分发的想法是提供一种包格式,而无需引入额外的依赖项。当涉及到 Python C/C++ 扩展时,构建分发会提供为用户平台准备好的二进制文件。
使用最广泛的构建分发格式是 Python wheel,在 PEP 427 中指定。
Python wheels
Wheels 是带有文件扩展名 .whl
的 ZIP 存档。 wheel 可能包含二进制文件、脚本或纯 Python 文件。如果 wheel 包含 C/C++ 扩展模块的二进制文件,它会通过在其文件名中包含目标平台来指示这一点。纯 Python 文件 (.py
) 在安装 wheel 期间被编译成 Python 字节码 (.pyc
)。
如果您尝试使用 pip 从 PyPi 安装包,它总是会选择 Python wheel 而不是源码分发。但是,当 pip 找不到兼容的 wheel 时,它会尝试获取源码分发。作为包维护者,最好在 pip 上同时提供这两种格式。对于包用户而言,使用 wheel 而不是源码分发是有利的,因为安装过程更安全,体积更小,因此安装时间更快。
为了满足广泛用户的需求,包维护者必须为各种平台和 Python 版本提供 wheels。
在我之前的文章 为 Python 编写 C++ 扩展模块 中,我演示了如何为 CPython 解释器创建 Python C++ 扩展。您可以重用文章的 示例代码 来构建您的第一个 wheel。
使用 setuptools 定义构建配置
演示存储库包含以下文件,这些文件包含元信息和构建过程的描述
pyproject.toml
[build-system]
requires = [
"setuptools>=58"
]
build-backend = "setuptools.build_meta"
自 PEP 517 和 PEP 518 以来,此文件是 setup.py
的后续文件。此文件实际上是打包过程的入口点。 build-backend 键告诉 pip 使用 setuptools 作为构建系统。
setup.cfg
此文件包含包的静态、永不更改的元数据
[metadata]
name = MyModule
version = 0.0.1
description = Example C/C++ extension module
long_description = Does nothing except incremention a number
license = GPLv3
classifiers =
Operating System::Microsoft
Operating System::POSIX::Linux
Programming Language::C++
setup.py
此文件定义了 Python 模块的通用构建过程。必须在安装时执行的每个操作都放在这里。
由于安全问题,只有在绝对必要时才应存在此文件。
from setuptools import setup, Extension
MyModule = Extension(
'MyModule',
sources = ['my_py_module.cpp', 'my_class_py_type.cpp'],
extra_compile_args=['-std=c++17']
)
setup(ext_modules = [MyModule])
此示例包实际上是一个 Python C/C++ 扩展,因此它需要在用户的系统上有一个 C/C++ 工具链才能编译。在之前的文章中,我使用了 CMake 来生成构建配置。这次,我使用 setuptools 进行构建过程。在构建容器内运行 CMake 时,我遇到了挑战(稍后我会回到这一点)。 setup.py
文件包含构建扩展模块所需的所有信息。
在此示例中,setup.py
列出了涉及的源文件和一些(可选的)编译参数。您可以在 文档 中找到对 setuptools 构建的引用。
构建过程
要启动构建过程,请在 存储库 的根文件夹中打开一个终端并运行
$ python3 -m build --wheel
之后,找到包含 .whl
文件的子文件夹 dist
。例如
MyModule-0.0.1-cp39-cp39-linux_x86_64
文件名包含大量信息。在模块名称和版本之后,它指定了 Python 解释器 (CPython 3.9) 和目标架构 (x86_64)。
此时,您可以安装并测试新创建的 wheel
$ python3 -m venv venv_test_wheel/
$ source venv_test_wheel/bin/activate
$ python3 -m pip install dist/MyModule-0.0.1-cp39-cp39-linux_x86_64.whl

(Stephan Avenwedde, CC BY-SA 4.0)
现在您有了一个 wheel,您可以将其转发给在同一架构上的同一解释器中使用的人。这是最低要求,因此我将更进一步,向您展示如何为其他平台创建 wheels。
构建配置
作为包维护者,您应该为尽可能多的平台提供合适的 wheel。幸运的是,有一些工具可以使您轻松做到这一点。
维护 Linux 兼容性
构建 Python C/C++ 扩展时,生成的二进制文件会链接到构建系统的标准库。这可能会在 Linux 上引起一些不兼容性,因为 Linux 有各种版本的 glibc
。由于例如缺少某个共享库,在一个 Linux 系统上构建的 Python C/C++ 扩展模块可能无法在另一个可比的 Linux 系统上工作。为了避免这种情况,PEP 513 提出了一种适用于许多 Linux 平台的 wheel 标签: manylinux。
为 manylinux 平台构建会导致链接到定义的内核和用户空间 ABI。符合此标准的模块预计可在许多 Linux 系统上运行。 manylinux 标签随着时间的推移而发展,在其最新标准 (PEP 600) 中,它直接命名了模块链接到的 glibc
版本(例如,manylinux_2_17_x86_64
)。
除了 manylinux 之外,还有 musllinux 平台 (PEP 656),它定义了利用 musl libc 的发行版的构建配置,例如 Alpine Linux。
CI 构建 wheel
cibuildwheel 项目为许多平台和使用最广泛的 CI/CD 系统提供 CI 构建配置。
许多 Git 托管平台都内置了 CI/CD 功能。该项目托管在 GitHub 上,因此您可以使用 GitHub Actions 作为 CI 服务器。只需按照 GitHub Actions 的说明 并在您的存储库中提供一个工作流文件:.github/workflows/build_wheels.yml。

(Stephan Avenwedde, CC BY-SA 4.0)
推送到 GitHub 会触发工作流。工作流完成后(请注意,它花费了超过 15 分钟才能完成),您可以下载包含各种平台的 wheel 的存档

(Stephan Avenwedde, CC BY-SA 4.0)
如果您想在 PyPi 上发布这些 wheels,您仍然需要手动打包它们。使用 CI/CD,可以自动将交付过程发布到 PyPi。您可以在 cibuildwheels 文档 中找到更多说明。
总结
对于初学者来说,各种格式可能会使 Python 模块的打包成为一个令人费解的过程。对于包维护者来说,了解不同的包格式、其用途以及打包过程中涉及的工具是必不可少的。我希望本文能够阐明 Python 打包的世界。最后,通过使用 CI/CD 构建系统,以有利的 wheel 格式提供包变得轻而易举。
评论已关闭。