pyproject.toml 里那几行 [build-system] 到底是谁读、谁执行?这篇不用任何第三方构建后端,而是自己写了一个最小的 PEP 517 后端,让它在被调用时把 pid、Python 路径都记下来——这样「前端」和「后端」的分工就看得一清二楚。
pyproject.toml [build-system] requires = [] build-backend = "mybackend" backend-path = ["backend"] [project] name = "hello-pkg" version = "0.1.0" description = "演示用" requires-python = ">=3.10"
backend-path 让后端就放在项目里;requires 是空列表,没有任何构建工具需要安装。后端只是一个 Python 模块,实现几个约定好的函数(钩子):get_requires_for_build_wheel、prepare_metadata_for_build_wheel、build_wheel。每个钩子被调用时,先往日志里写下自己的 pid、父进程 pid、sys.executable 和当前目录。
前端(build)的 python = <scratch>/venv/bin/python $ python -m build --wheel (默认: 隔离环境) 返回码 0 后端被调用: get_requires_for_build_wheel pid=84510 ppid=84509 python=/private/var/folders/km/b69gr9cs3dv8qsm8rjjb4qqc0000gn/T/build-env-l1zipbe0/bin/python cwd=<项目>/proj 后端被调用: build_wheel pid=84511 ppid=84509 python=/private/var/folders/km/b69gr9cs3dv8qsm8rjjb4qqc0000gn/T/build-env-l1zipbe0/bin/python cwd=<项目>/proj 产物: ['hello_pkg-0.1.0-py3-none-any.whl'] $ python -m build --wheel --no-isolation 返回码 0 后端被调用: get_requires_for_build_wheel pid=84513 ppid=84512 python=<scratch>/venv/bin/python cwd=<项目>/proj 后端被调用: build_wheel pid=84514 ppid=84512 python=<scratch>/venv/bin/python cwd=<项目>/proj
看两栏关键数据:pid 每个钩子都不同,ppid 却都相同——前端进程只启动子进程去调用后端,自己不 import 后端。默认(隔离)模式下,后端使用的是 build-env-…/bin/python,一个临时创建的虚拟环境,所以它看不到你当前环境里装的包,只能用 requires 里声明的;加上 --no-isolation 就直接使用当前环境的 Python。构建之前的所有准备工作,都是前端做的。
$ pip install --target site_t . 返回码 0 后端被调用: get_requires_for_build_wheel pid=84518 ppid=84516 python=<scratch>/venv/bin/python cwd=<项目>/proj 后端被调用: prepare_metadata_for_build_wheel pid=84519 ppid=84516 python=<scratch>/venv/bin/python cwd=<项目>/proj 后端被调用: build_wheel pid=84521 ppid=84516 python=<scratch>/venv/bin/python cwd=<项目>/proj 安装出来的文件: ['hello_pkg', 'hello_pkg-0.1.0.dist-info'] import hello_pkg; hello_pkg.hi() = hello from hello_pkg
pip 装一个源码目录时,先调 get_requires_for_build_wheel 看还需要什么,再调 prepare_metadata_for_build_wheel 只生成元数据(用来做依赖解析,不必真的构建),最后才 build_wheel。装完的目录里只有包本身和 dist-info,import hello_pkg 直接可用。
hello_pkg/__init__.py 40 字节
hello_pkg-0.1.0.dist-info/METADATA 72 字节
hello_pkg-0.1.0.dist-info/WHEEL 80 字节
hello_pkg-0.1.0.dist-info/RECORD 286 字节
METADATA 内容:
Metadata-Version: 2.1
Name: hello-pkg
Version: 0.1.0
Summary: 演示用wheel 就是个 zip:包文件、METADATA(名称、版本、摘要,来自 [project])、WHEEL(格式说明)、RECORD(每个文件的哈希和大小)。我的后端手写了这几样,pip 完全认账——说明「怎么把源码变成 wheel」这件事,pip 并不关心,它只认接口。
返回码 1 | stderr 最后两行: TIP pass --env-dir and --sdist-extract-dir to keep the build environment and sources, then see https://build.pypa.io/en/stable/how-to/troubleshooting.html#debug-a-failed-build for help debugging a failed build / ERROR Backend subprocess exited when trying to invoke build_wheel
后端的异常发生在子进程里,前端只报告「Backend subprocess exited when trying to invoke build_wheel」并给出排错提示(--env-dir 保留构建环境)。所以构建失败时,真正的原因要往上翻子进程打印的回溯,而不是看最后这一行。
build 1.6.1、pip 26.2.1(装在临时虚拟环境);后端是我自己写的最小实现,没有用 setuptools / hatchlingrequires 时的行为、可编辑安装钩子(见第 96 篇)