bigywater.tech
Python · 94

Python 构建系统:pyproject.toml 到底由谁执行

pyproject.toml 里那几行 [build-system] 到底是谁读、谁执行?这篇不用任何第三方构建后端,而是自己写了一个最小的 PEP 517 后端,让它在被调用时把 pid、Python 路径都记下来——这样「前端」和「后端」的分工就看得一清二楚。

每个钩子一个进程
后端每个钩子都在独立子进程里执行,父进程是前端
3 个钩子
pip install . 依次调用 get_requires / prepare_metadata / build_wheel
wheel = zip
产物只是包代码加 dist-info

做的事:自己写一个 build backend

实测 · 本机执行输出
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 作为前端——每个钩子一个子进程

实测 · 本机执行输出
  前端(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 也是前端,只是调用得更细

实测 · 本机执行输出
$ 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 直接可用。

验证三:wheel 里有什么

实测 · 本机执行输出
  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 保留构建环境)。所以构建失败时,真正的原因要往上翻子进程打印的回溯,而不是看最后这一行。

分步演示:一次 python -m build 的流程

进程号、路径都取自后端实际写下的日志
pyproject.toml 由谁执行
当前主角 / 数据

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7;build 1.6.1、pip 26.2.1(装在临时虚拟环境);后端是我自己写的最小实现,没有用 setuptools / hatchling
    • 没有验证:setuptools、hatchling 等真实后端的钩子调用差异、sdist 构建、需要联网安装 requires 时的行为、可编辑安装钩子(见第 96 篇)
    京ICP备2026067502号-1京公网安备11010502063487号