使用 wheels 打包 Python 模块

通过使用 CI/CD 构建系统,以有利的 wheel 格式提供 Python 包变得轻而易举。
1 位读者喜欢这篇文章。
Hands on a keyboard with a Python book

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

任何使用 Python 一段时间的人可能都已经接触过包。在 Python 术语中,包(或分发包)是一个或多个 Python 模块的集合,它们提供特定的功能。一般的概念类似于其他语言中的库。Python 包的一些特性使得处理它们有所不同。

Pip 和 PyPi

安装第三方 Python 包最常见的方法是使用包安装程序 pip,默认情况下提供。 Python 包索引 (PyPi) 是各种包的中心服务器,也是 pip 的默认来源。 Python 包包含指定包名称、版本和其他元信息的文件。 基于这些文件,PyPi 知道如何对包进行分类和索引。此外,这些文件可能包括 pip 处理的安装说明。

源码和二进制分发

Python 模块以多种格式分发,每种格式都有其优点和缺点。通常,这些格式可以分为两组。

源码分发 (sdist)

源码分发在 PEP 517 中定义,是带有文件扩展名 *.tar.gz 的 gzip 压缩的 tar 存档。该存档包含所有与包相关的源文件和安装说明。源码分发通常依赖于构建系统,例如 distutilssetuptools,这会导致安装期间执行代码。在安装时执行(任意)代码可能会引起安全问题。

对于 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 517PEP 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
Wheel package

(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

CI integration

(Stephan Avenwedde, CC BY-SA 4.0)

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

Archive for various platforms

(Stephan Avenwedde, CC BY-SA 4.0)

如果您想在 PyPi 上发布这些 wheels,您仍然需要手动打包它们。使用 CI/CD,可以自动将交付过程发布到 PyPi。您可以在 cibuildwheels 文档 中找到更多说明。

总结

对于初学者来说,各种格式可能会使 Python 模块的打包成为一个令人费解的过程。对于包维护者来说,了解不同的包格式、其用途以及打包过程中涉及的工具是必不可少的。我希望本文能够阐明 Python 打包的世界。最后,通过使用 CI/CD 构建系统,以有利的 wheel 格式提供包变得轻而易举。

标签
User profile image.
Stephan 是一位技术爱好者,他欣赏开源,因为它能深入了解事物的运作方式。 Stephan 作为全职支持工程师,主要从事工业自动化软件领域的工作。如果可能,他会从事基于 Python 的开源项目,撰写文章或骑摩托车。

评论已关闭。

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.