先回答一个问题:make guest 需要的 clang、CPython 静态库和 Wasm 工具从哪里来?答案不是 Go module,也不是 build-guest.sh 临时下载。make setup-build 先在 Linux x86_64 上把一组锁定的外部输入准备到一个目录;CPython 的 WASI 交叉构建完成后,后续 Guest 重建只消费这份本地目录。
build-inputs.lock.json
→ 版本、URL、SHA-256、许可元数据
setup-build-inputs.py
→ build/inputs/downloads/ 已校验归档
→ build/inputs/cpython/ CPython 源码与交叉构建树
→ build/inputs/tools/ SDK、wasm-tools、wasmtime、wasi-vfs
→ cpython/cross-build/wasm32-wasip1/*.a
↓
build-guest.sh 消费这些本地文件
这里的“本地工具链”指当前仓库实际声明并使用的 Linux x86_64 组合:CPython 3.14.0、WASI SDK 33.0、wasm-tools 1.254.0、Wasmtime 44.0.1、wasi-vfs 0.6.3。版本来自锁文件;本次没有检查这些目录是否已经存在,也没有下载或执行构建。
1. 准备脚本的职责
tools/setup-build-inputs.py · L1–L86 · 打开完整源码
#!/usr/bin/env python3
"""One-time Linux x86_64 CPython/WASI preparation; later Guest rebuilds are local.
No sudo, no remote build host. Downloads are pinned; interrupted builds can resume.
"""
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
def main():
if platform.system() != 'Linux' or platform.machine() != 'x86_64':
raise SystemExit('Build inputs require Linux x86_64.')
if sys.version_info < (3, 11):
raise SystemExit('Use Python 3.11 or newer for the CPython WASI build driver.')
for tool in ('cc', 'make', 'curl', 'tar', 'unzip'):
if not shutil.which(tool):
raise SystemExit(f'Missing {tool}; install build tools before running setup.')
base = Path(os.environ.get('SPINE_BUILD_INPUTS', ROOT / 'build/inputs')).resolve()
base.mkdir(parents=True, exist_ok=True)
downloads = base / 'downloads'
downloads.mkdir(exist_ok=True)
lock = json.loads((ROOT / 'tools/build-inputs.lock.json').read_text())
jobs = int(os.environ.get('SPINE_BUILD_JOBS', '6'))
if jobs < 1: raise SystemExit('SPINE_BUILD_JOBS must be positive')
os.sched_setaffinity(0, sorted(os.sched_getaffinity(0))[:jobs])
env = os.environ.copy()
env['MAKEFLAGS'] = f'-j{jobs}'
layout = {
'cpython-source': ('cpython', False),
'wasi-sdk-linux-x86_64': ('tools/wasi-sdk', False),
'wasm-tools-linux-x86_64': ('tools/wasm-tools', False),
'wasmtime-linux-x86_64': ('tools/wasmtime', False),
'wasi-vfs-cli-linux-x86_64': ('tools/wasi-vfs-cli', True),
'wasi-vfs-static-library': ('tools/wasi-vfs-lib', True),
}
def digest(file):
with file.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
for name, (directory, zipped) in layout.items():
source = lock[name]
archive = downloads / name
if not archive.exists() or digest(archive) != source['sha256']:
partial = archive.with_suffix('.part')
print(f'Downloading {name} {source["version"]}', flush=True)
subprocess.run(['curl', '-fL', '--retry', '2', '--connect-timeout', '20', '--max-time', '900', '-C', '-', '-o', str(partial), source['url']], check=True)
if digest(partial) != source['sha256']:
raise SystemExit(f'Checksum mismatch: {partial}; no extraction performed')
partial.replace(archive)
dest = base / directory
if not dest.exists():
dest.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=base, prefix='extract-') as temporary:
if zipped:
subprocess.run(['unzip', '-q', str(archive), '-d', temporary], check=True)
else:
subprocess.run(['tar', '-xf', str(archive), '-C', temporary, '--strip-components=1'], check=True)
Path(temporary).rename(dest)
for relative in ('tools/wasmtime/wasmtime', 'tools/wasm-tools/wasm-tools', 'tools/wasi-vfs-cli/wasi-vfs'):
(base / relative).chmod(0o755)
python = base / 'cpython'
config = python / 'Tools/wasm/wasi/config.site-wasm32-wasi'
text = config.read_text()
if 'ac_cv_func_clock_nanosleep=no' not in text:
config.write_text(text + '\n# wazero: use relative clock sleeps\nac_cv_func_clock_nanosleep=no\nac_cv_lib_rt_clock_nanosleep=no\n')
env['WASI_SDK_PATH'] = str(base / 'tools/wasi-sdk')
env['WASMTIME'] = str(base / 'tools/wasmtime/wasmtime')
env['PATH'] = str(base / 'tools/wasmtime') + os.pathsep + env['PATH']
print('Building CPython locally (incremental on repeat runs)', flush=True)
subprocess.run([sys.executable, 'Tools/wasm/wasi', 'build', '--wasi-sdk', env['WASI_SDK_PATH']], cwd=python, env=env, check=True)
target = python / 'cross-build/wasm32-wasip1'
for file in ('libpython3.14.a', 'pyconfig.h', 'Modules/_decimal/libmpdec/libmpdec.a', 'Modules/expat/libexpat.a'):
if not (target / file).is_file(): raise SystemExit(f'Missing build output: {file}')
if not list((target / 'Modules/_hacl').glob('*.a')):
raise SystemExit('Missing HACL archives')
print(f'Ready: {base}\nRun bash build-guest.sh to rebuild this project\'s actual Guest.', flush=True)
if __name__ == '__main__':
main()
这个 Python 文件是 Host 侧构建驱动,不是 Guest 内运行用户程序的 Python。入口处先固定仓库根目录,再要求 Linux、x86_64 和 Python 3.11 以上,并检查 cc、make、curl、tar、unzip 是否在 PATH 中。平台和命令检查先于下载,避免把输入准备到一半才发现当前机器不能完成这条构建路径。
输入根目录由 SPINE_BUILD_INPUTS 决定,默认是 build/inputs。脚本创建根目录和 downloads/,读取 tools/build-inputs.lock.json,并把 SPINE_BUILD_JOBS 转成正整数。它用前几个可用 CPU 设置当前驱动进程的 affinity,再把 MAKEFLAGS=-jN 传给 CPython 的 make。前者限制驱动进程可运行的 CPU 集合,后者才直接提示 make 的并行度;两者不是同一个开关。
layout 把锁文件中的六个名字映射到本地目录,并标记 tar 或 zip 解压方式:
cpython-source→cpython;- WASI SDK、
wasm-tools、Wasmtime →tools/下各自目录; wasi-vfsCLI →tools/wasi-vfs-cli;wasi-vfs静态库 →tools/wasi-vfs-lib。
之后每个锁定输入都按同一顺序处理:从锁记录取 URL 和摘要,归档放入 downloads/<name>;归档缺失或摘要不符时,curl 写入 .part,支持断点续传和有限重试,随后重新计算 SHA-256。只有摘要匹配,.part 才替换为正式归档;不匹配时停止且不解压。
解压也有一个提交点。脚本先把内容解到 extract-* 临时目录,tar 去掉归档最外层目录,zip 使用 unzip,两者都要求命令成功后才把临时目录改名为目标目录。已有目标目录会跳过解压,但脚本不会在这个分支重新验证目录内容;所以“目录存在”是缓存命中条件,不是目录完整性证明。
准备好工具后,脚本为 Wasmtime、wasm-tools 和 wasi-vfs CLI 设置可执行权限,修改 CPython 的 WASI 配置,使相关 clock sleep 配置为 no,再把 WASI_SDK_PATH、WASMTIME 和 PATH 前缀传给 CPython 构建驱动。最后调用 Tools/wasm/wasi build,并检查 libpython3.14.a、pyconfig.h、mpdecimal、expat 以及至少一个 HACL 静态库。所有检查通过后才打印 Ready,并提示下一步运行 bash build-guest.sh。
subprocess.run(..., check=True) 贯穿下载、解压和 CPython 构建:外部命令非零会立即进入失败路径,不会被 Python 包装成成功。main guard 则保证只有直接运行脚本时才开始这一流程,导入它不会自动下载。
2. 锁文件怎样描述六类输入
tools/build-inputs.lock.json · L1–L38 · 打开完整源码
{
"cpython-source": {
"version": "3.14.0",
"url": "https://www.python.org/ftp/python/3.14.0/Python-3.14.0.tgz",
"sha256": "88d2da4eed42fa9a5f42ff58a8bc8988881bd6c547e297e46682c2687638a851",
"license": "PSF-2.0"
},
"wasi-sdk-linux-x86_64": {
"version": "33.0",
"url": "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz",
"sha256": "0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce",
"license": "Apache-2.0 WITH LLVM-exception"
},
"wasm-tools-linux-x86_64": {
"version": "1.254.0",
"url": "https://github.com/bytecodealliance/wasm-tools/releases/download/v1.254.0/wasm-tools-1.254.0-x86_64-linux.tar.gz",
"sha256": "d0efb16c9c859137b7ab3e99151c299cd47caee106ed4594561002f7a04945bd",
"license": "Apache-2.0 WITH LLVM-exception"
},
"wasmtime-linux-x86_64": {
"version": "44.0.1",
"url": "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-linux.tar.xz",
"sha256": "afd58715f105e3a7f454169daed22168c5736ec5f225fb04c4ac62c54c9508a3",
"license": "Apache-2.0 WITH LLVM-exception"
},
"wasi-vfs-cli-linux-x86_64": {
"version": "0.6.3",
"url": "https://github.com/kateinoigakukun/wasi-vfs/releases/download/v0.6.3/wasi-vfs-cli-x86_64-unknown-linux-gnu.zip",
"sha256": "c9ee8179f6f0882abc37024fbe0cd678311aa8a29083fa364915ed4d29a69485",
"license": "Apache-2.0"
},
"wasi-vfs-static-library": {
"version": "0.6.3",
"url": "https://github.com/kateinoigakukun/wasi-vfs/releases/download/v0.6.3/libwasi_vfs-wasm32-unknown-unknown.zip",
"sha256": "f94dc7fdabdefc0fed78b70f1ab100207db0558cbabb004dd9e5a0bdeec4256f",
"license": "Apache-2.0"
}
}
锁文件的每条记录有同一组字段:version 供人和日志识别,url 指定下载位置,sha256 约束归档字节,license 记录来源许可。脚本实际用来决定能否继续的是 URL 和 SHA-256;当前脚本没有根据 license 字段做程序分支,也不会因为字段存在就自动附带许可文本。
六条记录可以按构建职责理解:
- CPython 源码:3.14.0,提供 Python 解释器源码、标准库以及后续交叉构建所需的配置和静态库产物。
- WASI SDK:33.0,提供面向
wasm32-wasip1的 clang、sysroot 和相关 C 目标环境;这是guest/runtime.c后续编译所需的编译器边界。 wasm-tools:1.254.0,后续用于验证打包后的 Wasm。- Wasmtime:44.0.1,准备脚本将其放到 PATH 前端并传给 CPython WASI 构建流程使用。
wasi-vfsCLI:0.6.3,后续把 Python 标准库目录打包进 Wasm 文件系统。wasi-vfs静态库:同为 0.6.3,但提供给 Guest 链接命令,而不是给打包 CLI 使用。
版本、URL 和摘要是三种不同信息:版本相同不等于下载字节相同,URL 存在也不等于内容已校验。锁文件是准备输入的描述;真正交付给下一阶段的是解压后的目录和 CPython 交叉构建输出。
3. 输入根目录与下一阶段的交接
setup-build-inputs.py 和 build-guest.sh 都读取 SPINE_BUILD_INPUTS,默认值也相同。这个共享根目录是阶段之间的交接协议:准备脚本把 SDK 放到 tools/wasi-sdk、CPython 放到 cpython,Guest 脚本再从这些相对位置取 clang、头文件和 .a 文件。
如果只存在下载归档,还不能运行 make guest;Guest 脚本需要已经解压的工具目录和 CPython 交叉构建输出。如果只存在 libpython3.14.a,也不能说明整个输入完整,因为 clang、sysroot、HACL、expat、mpdecimal、wasi-vfs 静态库和打包/验证工具也在下一阶段使用。
这层允许联网,且首次运行可能需要下载六个外部归档。它不等于离线验收:make offline 不会调用 setup-build,而是要求这些输入已经在本机,再把消费它们的检查命令放进禁网子进程。
4. 读完后应能回答
- 为什么正确的下载归档仍不足以让
make guest成功? - 为什么
.part文件只有通过摘要检查后才会成为正式缓存? - 为什么准备脚本和 Guest 构建脚本必须使用同一个
SPINE_BUILD_INPUTS?
下一步读Guest 构建,看本地 clang、CPython 静态库和仓库源文件怎样汇合成临时 raw.wasm,再被打包成正式 artifact。本章覆盖准备脚本和锁文件的全部非空源码行;没有下载、解压、CPython 构建或运行测试,因此没有把当前机器的缓存状态当成已验证事实。