很多人把生成 .whl 文件称为“编译 whl”。更准确地说,wheel 是 Python 的二进制分发格式:纯 Python 项目主要是打包,包含 C、C++、Rust 等原生扩展的项目才会在构建时编译二进制代码。
判断一个 wheel 能不能安装,最有用的线索往往就是它的文件名。下面从这个文件开始:
my_demo_package-1.2.0-1-cp312-abi3-manylinux_2_17_x86_64.whl
按照 wheel 规范,它的基本格式是:
{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
其中 build tag 是可选的,所以常见的纯 Python wheel 可能是:
my_demo_package-1.2.0-py3-none-any.whl
本文只围绕这些字段展开:每一段表示什么、pip 如何使用它、构建时可以通过什么方式影响它,以及哪些事情不能靠改文件名解决。
一、先记住一个完整例子
my_demo_package-1.2.0-1-cp312-abi3-manylinux_2_17_x86_64.whl│ │ │ │ │ └─ platform tag│ │ │ │ └─────── abi tag│ │ │ └────────────── python tag│ │ └──────────────── build tag│ └────────────────────── version└────────────────────────────────────── distribution
可以把它理解成一份“安装筛选信息”:
distributionversionbuild tagpython tagabi tagplatform tag
pip 会把这些标签和当前解释器支持的标签进行匹配。匹配不上时,即使文件确实存在,pip 也不会选择它。
二、distribution:项目名,不是 import 名
看下面的配置:
[project]name = "my-demo-package"version = "1.2.0"
构建后通常得到:
my_demo_package-1.2.0-py3-none-any.whl
这里有三个容易混淆的名字:
发行项目名(distribution):my-demo-packagewheel 文件名中的项目名: my_demo_packagePython 导入名(import): my_demo_package
发行项目名用于 pip install:
python -m pip install my-demo-package
导入名用于 Python 代码:
import my_demo_package
它们不要求相同。
项目名为什么变成下划线
wheel 文件名中的 distribution 字段不能直接保留任意分隔符。根据 PEP 427,项目名中的一段或多段 -、_、. 会在 wheel 文件名中转义为一个 _。
因此,下面这些项目名在 wheel 文件名中通常都会变成同一个形式:
my-demo-packagemy_demo_packagemy.demo.package
结果通常是:
my_demo_package-1.2.0-py3-none-any.whl
这和 PEP 440 的版本号规范化不是一回事。PEP 440 管版本号;这里讨论的是项目名字段和 wheel 文件名转义。
能不能让项目名部分保留中划线
不能把标准 wheel 的 distribution 字段强行写成:
my-demo-package-1.2.0-py3-none-any.whl
wheel 文件名使用中划线分隔字段。项目名部分再包含中划线时,解析器无法可靠区分项目名和后面的版本、标签。因此,正常构建会把项目名中的中划线转成下划线。
不要构建后直接执行:
mv my_demo_package-1.2.0-py3-none-any.whl \ my-demo-package-1.2.0-py3-none-any.whl
这样既没有修改 wheel 内部的 .dist-info 元数据,也可能导致 pip 报 Invalid wheel filename。如果业务系统要求展示带中划线的名称,可以把它放在制品库目录、下载链接或外层归档名中,wheel 文件本身保持标准名称。
和项目名有关的构建选项
现代项目在 pyproject.toml 中设置:
[project]name = "my-demo-package"
然后构建:
python -m build --wheel
--wheel 只控制构建 wheel,不会改变项目名的转义规则。setuptools、Hatchling、Flit 等后端都应遵守 wheel 文件名规范。
三、version:版本号,受 PEP 440 约束
示例中的:
my_demo_package-1.2.0-py3-none-any.whl └───── version
版本号来自项目元数据:
[project]version = "1.2.0"
建议使用 PEP 440 版本,例如:
1.2.01.2.0rc11.2.0.post11.2.0.dev31.2.0+cuda12
这里才是 PEP 440 发挥作用的地方。不要把“版本号规范化”和“项目名在 wheel 文件名中转义为下划线”混为一谈。关于版本号的规范,我将在下一篇文章详细介绍。
和版本有关的构建配置
最推荐在 pyproject.toml 中声明固定版本:
[project]version = "1.2.0"
如果版本来自 Git 标签或 CI,一般使用构建后端的动态版本配置,例如 setuptools-scm:
[project]dynamic = ["version"][tool.setuptools_scm]
具体配置取决于后端,不建议在构建完成后手改 wheel 名称。
若要表示 CUDA、CPU、客户版等差异,可以考虑本地版本:
my_demo_package-1.2.0+cuda12-py3-none-any.whl
但本地版本是否能发布到目标仓库、如何参与版本比较,要遵守 PEP 440 和仓库策略。另一种方式是使用不同的发行项目名,例如 my-demo-package-cuda,或把变体放到制品库目录中。
四、build tag:同一版本的构建编号
带 build tag 的示例:
my_demo_package-1.2.0-1-cp312-abi3-manylinux_2_17_x86_64.whl └─ build tag
它用于区分同一项目、同一版本和同一兼容标签下的不同构建结果。例如修复了构建脚本、重新打包但没有改变项目版本时,可以增加构建编号。
PEP 427 要求 build tag 以数字开头,所以 1、1custom 这类形式才符合要求;随意写成 cuda 不合规。
和 build tag 有关的构建选项
遗留的 setuptools/wheel 命令可以这样写:
python setup.py bdist_wheel --build-number 1
现代项目不建议把 setup.py 当作主要入口,应优先使用:
python -m build --wheel
至于 build tag 是否能通过 pyproject.toml 设置,没有跨构建后端统一的标准配置;如果后端没有提供对应选项,就不要自行拼接文件名。
build tag 不是平台标签,也不是 CUDA 标签。下面这种写法不要使用:
my_demo_package-1.2.0-py3-none-any-cuda.whl
它破坏了 wheel 文件名的字段结构。
五、python tag:支持哪个 Python 实现
示例中的:
my_demo_package-1.2.0-1-cp312-abi3-manylinux_2_17_x86_64.whl └───── python tag
常见值包括:
| |
|---|
py3 | |
py39 | 面向 Python 3.9 的纯 Python 代码 |
cp312 | |
pp39 | |
纯 Python 包常见的是:
py3-none-any
如果 wheel 里有 CPython 原生扩展,通常会出现 cp311、cp312 等标签。
和 python tag 有关的构建选项
旧式 bdist_wheel 支持:
python setup.py bdist_wheel --python-tag py3
但这个参数只是声明标签,不会把只支持 CPython 3.12 的二进制扩展变成通用 py3。标签必须和实际代码兼容性一致。
现代构建通常由后端根据项目内容自动生成:
python -m build --wheel
需要构建多个 Python 版本时,应该在多个 Python 环境或 CI 矩阵中分别构建,或者使用 cibuildwheel:
python -m cibuildwheel --output-dir wheelhouse
不要在构建完成后仅修改 cp312、cp311 等文本。改名不会重新编译扩展。
六、abi tag:二进制接口是否兼容
示例中的:
my_demo_package-1.2.0-1-cp312-abi3-manylinux_2_17_x86_64.whl └──── abi tag
常见值包括:
| |
|---|
none | 不依赖特定 Python ABI,纯 Python wheel 常见 |
cp312 | |
abi3 | 使用 CPython 稳定 ABI,可覆盖多个 CPython 版本 |
例如:
my_demo_package-1.2.0-cp312-cp312-manylinux_2_17_x86_64.whlmy_demo_package-1.2.0-cp39-abi3-manylinux_2_17_x86_64.whl
第一种通常只适合 CPython 3.12;第二种表示以 CPython 3.9 为最低 Python 标签并使用稳定 ABI,实际兼容范围还取决于扩展代码和平台标签。
和 abi tag 有关的构建配置
ABI 不是一个应该随意填写的字符串。它通常由扩展构建工具根据编译结果决定。若使用 setuptools 构建 CPython 稳定 ABI,需要在扩展配置中启用 limited API,并使用与后端匹配的配置方式,例如部分项目会设置 py_limited_api。
关键原则是:
编译选项和扩展代码决定 ABI,文件名只是对结果的声明。
--config-setting 可以向 PEP 517 构建后端传递配置:
python -m build --wheel -Ckey=valuepython -m pip wheel . --config-settings key=value
但 key=value 的含义由具体后端决定,不存在一个对所有项目通用的“设置 abi3”参数。改文件名中的 none 或 abi3 不会改变二进制接口。
七、platform tag:支持哪个操作系统和架构
示例中的最后一段:
my_demo_package-1.2.0-1-cp312-abi3-manylinux_2_17_x86_64.whl └──────────────────── platform tag
常见值包括:
| |
|---|
any | |
win_amd64 | |
macosx_11_0_arm64 | |
manylinux_2_17_x86_64 | 满足 manylinux glibc 2.17 兼容要求的 Linux x86_64 |
musllinux_1_2_x86_64 | 面向 musl libc 的 Linux x86_64 |
纯 Python 项目通常生成 py3-none-any,含原生扩展的项目则会带操作系统和架构信息。
和 platform tag 有关的构建选项
旧式 wheel 命令支持:
python setup.py bdist_wheel \ --plat-name manylinux_2_17_x86_64
这个选项只是为构建结果指定平台标签。它不会把 Windows 二进制变成 Linux 二进制,也不会自动满足 manylinux 的系统库约束。
更可靠的做法是在目标平台或标准构建容器中构建,使用 cibuildwheel 生成多平台产物:
python -m cibuildwheel --output-dir wheelhouse
auditwheel、delocate 等工具可以帮助检查或修复部分平台依赖,但它们同样不能替代正确的编译环境。
下面这种改名是错误的:
mv package-1.0.0-cp312-cp312-linux_x86_64.whl \ package-1.0.0-cp312-cp312-win_amd64.whl
文件内的动态库格式、系统调用和依赖没有变化。pip 可能因此在 Windows 上错误地选择这个文件,最终安装或导入失败。
八、文件名里的中划线和下划线到底怎么理解
wheel 文件名中的中划线主要是字段分隔符:
distribution-version-build-python-abi-platform.whl
所以文件名中一定会出现中划线。它不是项目名中划线的保留方式。
下划线常见于两个地方:
- distribution 字段:项目名中的
-、_、. 按 wheel 规则转义为 _; - platform tag:例如
win_amd64、manylinux_2_17_x86_64,下划线用于平台标签内部的结构。
因此,下面这个文件名里的下划线含义并不相同:
my_demo_package-1.2.0-py3-none-manylinux_2_17_x86_64.whl└── 项目名转义 └── 平台标签内部组成
九、从文件名反推构建和排错
pip 为什么不选择某个 wheel
先看当前解释器支持的标签:
python -m pip debug --verbose
再对照文件名中的 python tag、abi tag、platform tag。例如当前是 CPython 3.11,而目录里只有 cp312-cp312,pip 不选它是正常行为。
如何一次构建多个平台
不要复制一个 wheel 然后修改最后一段。使用构建矩阵或 cibuildwheel:
python -m cibuildwheel --output-dir wheelhouse
最终应该得到多个真实构建的文件,例如:
demo-1.0.0-cp311-cp311-manylinux_2_17_x86_64.whldemo-1.0.0-cp311-cp311-win_amd64.whldemo-1.0.0-cp311-cp311-macosx_11_0_arm64.whl
如何指定输出目录
这不会改变文件名字段,只改变产物位置:
python -m build --wheel --outdir wheelhousepython -m pip wheel . --wheel-dir wheelhouse
pip wheel 还可以使用:
python -m pip wheel . \ --no-deps \ --no-build-isolation \ --config-settings key=value \ --wheel-dir wheelhouse
这些参数分别控制是否构建依赖、是否使用隔离环境、向后端传递配置以及输出目录;它们不会让项目名保留中划线,也不会凭空改变 ABI 或平台兼容性。
十、检查文件名和内部元数据是否一致
wheel 是 ZIP 格式,可以查看内容:
python -m zipfile -l \ dist/my_demo_package-1.2.0-py3-none-any.whl
至少检查:
.dist-info/METADATA.dist-info/WHEEL
还可以执行:
python -m pip install twinepython -m twine check dist/*
最后在干净虚拟环境中安装测试。文件名看起来正确,并不代表内部二进制真的兼容。
总结
一个 wheel 文件名可以看成一张兼容性标签卡:
distribution-version-build-python-abi-platform.whl
- distribution 决定“这是哪个项目”,项目名中的
-、_、. 在 wheel 文件名中通常转义为 _; - version 遵循 PEP 440,版本号的规范化规则与项目名规则不同;
- build tag 用于区分同版本的多个构建,必须以数字开头;
- python tag 描述 Python 实现和版本;
- platform tag 描述操作系统、架构和系统 ABI。
构建选项的原则也很简单:
构建选项负责生成真实产物,文件名标签负责声明产物兼容性。
--python-tag、--plat-name、--build-number 等旧式参数可以影响部分字段,但不会替代编译;现代项目优先使用 python -m build --wheel,多平台场景使用构建矩阵或 cibuildwheel。不要通过 mv 手工改 wheel 文件名来伪造项目名、ABI 或平台。
参考资料
- PEP 427:The Wheel Binary Package Format
- PEP 440:Version Identification and Dependency Specification
- PyPA:Binary distribution format
buildpip wheel