Bitcoin Core depends 依赖构建系统深度解析:确定性构建、build-id 缓存与跨编译设计
本文以 depends/description.md 为核心,系统拆解 Bitcoin Core depends 目录下的依赖构建与缓存系统的七项核心设计——构建机/目标机无关、无时间戳依赖、干净的 sysroot、基于 build-id 的增量缓存、相对确定性产物、源码自动抓取校验以及自清理机制,并结合 depends/Makefile、depends/funcs.mk、depends/gen_id 等源码说明每项设计的落地实现。读完后,你将理解这套系统如何让 Bitcoin Core 的依赖构建在不同机器上可复现、可分发、可被自动化构建机消费,并能熟练使用 make HOST=...、NO_QT=1 等选项完成原生与交叉构建。
一、系统定位:为构建 Bitcoin Core 而生的依赖工厂
depends 是"一套构建并缓存构建 Bitcoin Core 所需依赖的系统"(原文首句)。它的职责是:把 Qt、Boost、SQLite、libevent、ZeroMQ、expat 等第三方库(以及构建侧所需的 native 工具)按统一的配方编译好,打进一个干净的 sysroot,最终生成供主构建系统 CMake 使用的 toolchain.cmake 工具链文件。
从 depends/README.md 可知,使用流程非常直接:
# 构建当前架构+操作系统的依赖
make
# 交叉构建
make HOST=host-platform-triplet
# 例如
make HOST=x86_64-w64-mingw32 -j4
构建完成后,在主仓库根目录配置 Bitcoin Core 时必须显式指定工具链文件,CMake 默认会忽略 depends 的产物:
cmake -B build --toolchain depends/x86_64-pc-linux-gnu/toolchain.cmake
depends/Makefile 在依赖全部就绪后还会打印一条提示(第 193 行):
To build Bitcoin Core with these packages, pass
'--toolchain <dir>/toolchain.cmake'to the first CMake invocation.
description.md 强调,这套系统有若干特点,使其"与大多数同类系统不同"。下面逐条对照源码展开。
二、设计一:Builder 与 Host 无关(构建机与目标机解耦)
原文表述:
In theory, binaries for any target OS/architecture can be created, from a builder running any OS/architecture. In practice, build-side tools must be specified when the defaults don't fit, and packages must be amended to work on new hosts.
即理论上任意构建机都可以产出任意目标平台/架构的二进制。实现上,Makefile 用 config.guess/config.sub 两个脚本做规范化:
BUILD = $(shell unset CC && ./config.guess) # 构建机三元组
canonical_host:=$(shell ./config.sub $(HOST)) # 目标机规范化三元组
随后按"两个维度、各自 include"的方式加载规则(depends/Makefile#L114-L118):
include hosts/$(host_os).mk
include hosts/default.mk
include builders/$(build_os).mk
include builders/default.mk
include packages/packages.mk
- depends/hosts/default.mk 定义目标侧工具(
default_host_CC = $(host_toolchain)gcc,交叉编译时host_toolchain为$(host)-前缀),并提供add_host_tool_func/add_host_flags_func两级回退(命令行变量 > 平台变量 > 默认值); - depends/builders/default.mk 定义构建侧(native 工具,如
native_capnp、native_qt用的)CC/CXX/AR/TAR/SHA256SUM/DOWNLOAD/TOUCH等工具,默认gcc/g++,各 BSD/macOS 构建机在builders/*.mk中覆盖为 clang 系。
这就是原文所说"实践中,当默认值不适用时需显式指定构建侧工具,新增 host 需修改包"的含义:新增一个目标平台,要新增/修改 hosts/<os>.mk;新增一种构建机,要适配 builders/<os>.mk。
三、设计二:不依赖时间戳,只认文件存在性与内容哈希
原文表述:
File presence is used to determine what needs to be built. This makes the results distributable and easily digestible by automated builders.
depends 的增量判断不靠 mtime 比较,而是靠stamp 文件是否存在 + build-id 变化。depends/funcs.mk 中每个包都生成一整套阶段目标,其阶段名固定为(funcs.mk 第 307 行):
stages = fetched extracted preprocessed configured built staged postprocessed cached cached_checksum
每个阶段以文件存在为完成标志,例如(funcs.mk 第 250–299 行):
$($(1)_fetched): ... # 下载 + sha256 校验通过,落盘 .hash stamp
$($(1)_extracted): | $($(1)_fetched) ... # 解包
$($(1)_configured): | $($(1)_dependencies) $($(1)_preprocessed) ...
$($(1)_built): | $($(1)_configured) ...
$($(1)_staged): | $($(1)_built) ...
$($(1)_cached): | $($(1)_dependencies) $($(1)_postprocessed) ...
$($(1)_cached_checksum): $($(1)_cached) ...
关键细节:build-id 直接编码在目录名里(funcs.mk 第 86–93 行):
$(1)_staging_dir=$(base_staging_dir)/$(host)/$(1)/$($(1)_version)-$($(1)_build_id)
$(1)_cached:=$(BASE_CACHE)/$(host)/$(1)/$(1)-$($(1)_version)-$($(1)_build_id).tar.gz
配方一变,build_id 变,目录/缓存文件名就变,旧产物自然失效——无需任何时间戳比较。这正是"结果可分发、易被自动化构建机消化"的底层原因:CI 只需拉取缓存 tarball 与校验文件即可复用。
四、设计三:每次构建只看到被声明的依赖(干净的 sysroot)
原文表述:
For each build, the sysroot is wiped and the (recursive) dependencies are installed. This makes each build deterministic, since there will never be any unknown files available to cause side-effects.
在 configured 阶段的规则中可以清楚看到"先清空、再只安装递归依赖"(funcs.mk 第 267–272 行):
$($(1)_configured): | $($(1)_dependencies) $($(1)_preprocessed)
echo Configuring $(1)...
rm -rf $(host_prefix); mkdir -p $(host_prefix)/lib; cd $(host_prefix); $(foreach package,$($(1)_all_dependencies), $(build_TAR) --no-same-owner -xf $($(package)_cached); )
rm -rf $(host_prefix):每个包开始 configure 前,sysroot(host prefix)被整体清掉;- 随后只把
$($(1)_all_dependencies)(由int_get_all_dependencies递归展开的直接+间接依赖)的缓存 tarball 解进去。
因此在编译包 A 时,sysroot 里只有 A 声明的依赖,任何"环境里恰好存在某个头文件/库"造成的隐性副作用都被排除。递归依赖的计算在 funcs.mk 第 25–27 行的 int_get_all_dependencies(递归 foreach + sort 去重)中完成。
五、设计四:build-id 驱动缓存,改动自动级联重建
这是 description.md 中最核心的一条:
Before building, a unique build-id is generated for each package. This id consists of a hash of all files used to build the package (Makefiles, packages, etc), and as well as a hash of the same data for each recursive dependency. If any portion of a package's build recipe changes, it will be rebuilt as well as any other package that depends on it. If any of the main makefiles (Makefile, funcs.mk, etc) are changed, all packages will be rebuilt. After building, the results are cached into a tarball that can be reused and distributed.
源码中这条逻辑由三个函数实现:
-
配方哈希
recipe_hash(funcs.mk 第 65–75 行int_get_build_recipe_hash):对主 Makefile 文件组 + 本包配方 + 补丁文件做 sha256:$(1)_all_file_checksums:=$(shell $(build_SHA256SUM) $(meta_depends) packages/$(1).mk ... $(addprefix $($(1)_patches_path)/,$($(1)_patches)) | cut -d" " -f1)其中
meta_depends在 depends/Makefile#L181 定义为:meta_depends = Makefile config.guess config.sub funcs.mk builders/default.mk hosts/default.mk hosts/$(host_os).mk builders/$(build_os).mk由于每个包的配方哈希都包含全部
meta_depends,所以主 Makefile/funcs.mk 任一变更,所有包 id 全变,触发全量重建——与原文描述完全一致。 -
build-id 生成
int_get_build_id(funcs.mk 第 77–83 行):build_id_long由"包名-版本-recipe_hash-release_type + 所有递归依赖的包名-版本-recipe_hash+ 工具链 id($($(1)_type)_id)"拼成,再 sha256 截断为 11 字符(Makefile 第 54 行HASH_LENGTH:=11)。依赖的 build-id 变化会通过$(dep)-$(version)-$(recipe_hash)串级联进来,实现"依赖包变了,下游包跟着重建"。 -
工具链 id 来自 depends/gen_id:Makefile 第 143–152 行把
build_CC/CXX/AR/NM/RANLIB/STRIP/SHA256SUM、各*FLAGS、DEBUG、LTO及可选盐(BUILD_ID_SALT/HOST_ID_SALT,默认salt)作为环境传给gen_id。gen_id会把编译器-v输出、AR/NM/RANLIB/STRIP --version、ld.lld --version、mold --version、标准版本等拼成预像做 sha256(第 27–100 行)。注释中特意说明两点工程考量:unset SOURCE_DATE_EPOCH防止时间戳泄漏进工具输出、用bash -c调用确保"command not found"报错行号恒为 1,保证哈希稳定——"它不set -e,因为 id 判定以机会主义为主:失败可以,但要失败得一致"。
缓存侧还有主动的一致性自检(Makefile 第 238–259 行):check-packages/check-sources 会对每个包的缓存 tarball 与源码重新执行 sha256sum -c,校验失败则删除缓存并提示 Checksum mismatch for $(package). Forcing rebuild.. / Checksum missing or mismatched ... Forcing re-download.。install 目标显式依赖 check-packages(Makefile 第 267 行)。
此外,Makefile 第 185–186 行还计算了全局 final_build_id(所有包 build_id_long 加 toolchain.cmake.in 哈希后取前 11 位),.stamp_$(final_build_id) 目标负责把全部缓存 tarball 解包进最终 host 目录——整个依赖树的指纹,工具链文件的生成以它为前置(第 202 行)。
六、设计五:产物"相对"确定性
原文表述(注意其谨慎措辞):
Each package is configured and patched so that it will yield the same build-results with each consequent build, within a reasonable set of constraints. Some things like timestamp insertion are unavoidable, and are beyond the scope of this system. Additionally, the toolchain itself must be capable of deterministic results. When revisions are properly bumped, a cached build should represent an exact single payload.
配套措施在仓库中可见:
- tarball 打包确定性(funcs.mk 第 290–292 行):
find . ... | TZ=UTC xargs -0r $(build_TOUCH)先统一 mtime,再find . | LC_ALL=C sort | $(build_TAR) --numeric-owner --no-recursion -czf ... -T -——固定时区 touch、locale 无关排序、--numeric-owner避免 uid/gid 差异,让缓存 tarball 本身可比对、可分发; - 构建机 touch 工具固化时间:depends/builders/default.mk#L9
default_build_TOUCH = touch -h -m -t 200001011200,把文件时间钉死在固定值; - gen_id 排除时间因素:
unset SOURCE_DATE_EPOCH(gen_id 第 31–33 行),既防泄漏进工具输出,也最大化包缓存复用。
原文同时明确了边界:编译进二进制的版本字符串等时间戳注入不在本系统职责内,且工具链本身必须支持确定性输出。
七、设计六:源码自动抓取与强制校验
原文表述:
Each package must define its source location and checksum. The build will fail if the fetched source does not match. Sources may be pre-seeded and/or cached as desired.
每个包的"身份"四元组以 depends/packages/expat.mk 为例:
package=expat
$(package)_version=2.7.3
$(package)_download_path=https://github.com/libexpat/libexpat/releases/download/R_2_7_3/
$(package)_file_name=$(package)-$($(package)_version).tar.gz
$(package)_sha256_hash=821ac9710d2c073eaf13e1b1895a9c9aa66c1157a99635c639fbff65cdbdd732
抓取与校验的默认实现在 funcs.mk 第 29–41 行:
define fetch_file_inner
( mkdir -p $$($(1)_download_dir) && echo Fetching $(3) from $(2) && \
$(build_DOWNLOAD) "$$($(1)_download_dir)/$(4).temp" "$(2)/$(3)" && \
echo "$(5) $$($(1)_download_dir)/$(4).temp" > $$($(1)_download_dir)/.$(4).hash && \
$(build_SHA256SUM) -c $$($(1)_download_dir)/.$(4).hash && \
mv $$($(1)_download_dir)/$(4).temp $$($(1)_source_dir)/$(4) && \
rm -rf $$($(1)_download_dir) )
endef
define fetch_file
( $(call fetch_file_inner,$(1),$(2),$(3),$(4),$(5)) || \
$(call fetch_file_inner,$(1),$(FALLBACK_DOWNLOAD_PATH),$(4),$(4),$(5)))
endef
要点:
- 下载后立即
sha256sum -c,不匹配即构建失败(对应原文 "The build will fail if the fetched source does not match"); - 主源站失败后自动回退到
FALLBACK_DOWNLOAD_PATH(Makefile 第 46 行,默认https://bitcoincore.org/depends-sources,可通过命令行覆盖); download-one目标(Makefile 第 270 行)支持"只下载不构建",另有download-osx/download-linux/download-win三个平台化快捷目标(第 272–278 行),源码可预置(pre-seed)到SOURCES_PATH(Makefile 第 33 行,默认$(BASEDIR)/sources);- 解包阶段同样再校验一次哈希(funcs.mk 第 129 行
extract_cmds默认值); - 特殊地,定义
$(package)_local_dir的"本地包"会把本地目录打成 tarball 并把其哈希纳入 build-id,目录内容变化即触发该包及下游重建(funcs.mk 第 47–63 行fetch_local_dir_sha256,以及 depends/packages.md 的 "Local packages" 一节)。
各包变量的完整语义(_version/_download_path/_file_name/_sha256_hash 及可选的 _build_subdir/_dependencies/_patches/_extra_sources)在 depends/packages.md 中有逐条说明,是新增包时的规范文档。
八、设计七:自清理,面向自动化构建机
原文表述:
Build and staging dirs are wiped after use, and any previous version of a cached result is removed following a successful build. Automated builders should be able to build each revision and store the results with no further intervention.
对应实现分三层:
-
目录级清理:
staged阶段完成后立即rm -rf $($(1)_extract_dir)(funcs.mk 第 282 行);cached阶段把 tarball 移入缓存目录后rm -rf $($(1)_staging_dir)(第 296 行); -
全局清理目标(Makefile 第 261–265 行):
clean-all: clean @rm -rf $(SOURCES_PATH) x86_64* i686* mips* arm* aarch64* powerpc* riscv32* riscv64* s390x* clean: @rm -rf $(WORK_PATH) $(BASE_CACHE) $(BUILD) *.logclean清掉work/(WORK_PATH)、built/(BASE_CACHE,即缓存区)与.log;clean-all再清源码目录与所有 host 前缀目录; -
旧缓存淘汰:由于缓存文件名内嵌 build-id(见第五节),配方变更后新生成的缓存是新文件,而
check-packages的哈希校验(sha256sum -c $($(package)_cached_checksum) || rm ...)会把损坏/不匹配旧缓存删掉并强制重建(Makefile 第 238–243 行check_or_remove_cached)。
这三层共同保证:自动化构建机可以为每个 revision 干净地构建、存储结果,无需人工干预。
九、实操速查:目录结构、选项与常用命令
结合 depends/README.md 与 depends/Makefile,常用操作汇总如下。
9.1 默认路径布局(Makefile 第 33–36 行,均可命令行覆盖)
| 变量 | 默认值 | 用途 |
|---|---|---|
SOURCES_PATH |
$(BASEDIR)/sources |
下载源码存放处 |
WORK_PATH |
$(BASEDIR)/work |
构建/暂存目录(work/build、work/staging、work/download) |
BASE_CACHE |
$(BASEDIR)/built |
缓存 tarball 存放处 |
SDK_PATH |
$(BASEDIR)/SDKs |
macOS SDK 目录(交叉编译用) |
FALLBACK_DOWNLOAD_PATH |
https://bitcoincore.org/depends-sources |
主源失败时的回退下载源 |
9.2 功能开关(make FOO=bar 形式)
NO_BOOST/NO_QT/NO_QR/NO_ZMQ/NO_WALLET/NO_USDT/NO_IPC:不下载/构建/缓存对应功能依赖。Makefile 中通过boost_packages_$(NO_BOOST)这类"按开关取变量"的写法实现(第 154–165 行),其中NO_IPC在 Windows 目标下默认为 1(第 44 行);DEBUG:置空时为 release(release_type=release,第 60–64 行),非空时切 debug 并启用更多运行时检查,同时gen_id会把哈希预像打到 stderr 便于调试缓存问题(gen_id 第 101–104 行);C_STANDARD/CXX_STANDARD:默认c11/c++20(Makefile 第 48–49 行);HOST_ID_SALT/BUILD_ID_SALT:id 盐值,默认salt(第 57–58 行),用于在共享缓存环境下隔离不同工具链配置;LTO:开启 LTO 所需选项(不向*FLAGS追加-flto);LOG:为单个包启用文件日志,出错时自动打印日志内容。
README 还指出一个重要的联动行为:若跳过了某些包(如 make NO_WALLET=1),生成的 toolchain.cmake 会替主构建系统设置对应 CMake 缓存变量,例如 -DENABLE_WALLET=OFF。这一点可在 depends/toolchain.cmake.in 中逐条验证:@wallet_packages@ 为空则 set(ENABLE_WALLET OFF),@qt_packages@ 为空则 BUILD_GUI OFF,@ipc_packages@ 为空则 ENABLE_IPC OFF(第 137–177 行)。同时该文件把 CMAKE_FIND_ROOT_PATH 指向 depends 输出目录并设置 CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/INCLUDE/PACKAGE 为 ONLY(第 88–92 行),从机制上保证主构建只从 sysroot 找库,与第四节"每次构建只看到被声明的依赖"的设计互为表里。
9.3 单包调试目标
按 depends/packages.md "Build targets" 一节,调试单个包时可用:
make ${package}
make ${package}_fetched
make ${package}_extracted
make ${package}_preprocessed
make ${package}_configured
make ${package}_built
make ${package}_staged
make ${package}_postprocessed
make ${package}_cached
make ${package}_cached_checksum
这些目标由 funcs.mk 第 309–313 行 ext_add_stages 对 stages 列表批量生成,与第三节的九阶段管线一一对应。
9.4 交叉编译工具链准备
README 给出了各目标的常见 triplet(x86_64-w64-mingw32、arm64-apple-darwin、aarch64-linux-gnu、riscv64-linux-gnu 等)及对应的 mingw/clang/交叉 g++ 安装命令,并说明 macOS 交叉编译需将 SDK 解压到 depends/SDKs/。这些命令属于"适用前提":发行版包名随系统而变,且交叉工具链必须真实存在——与 description.md 第一条"新 host 需要适配"的提示一致。
十、总结
depends/description.md 用七条特征概括了这套系统,源码逐条印证:
- 构建机/目标机无关——
config.guess/config.sub+hosts/*.mk、builders/*.mk双维度规则(depends/Makefile、depends/hosts/default.mk); - 无时间戳依赖——九阶段 stamp 目标 + 内嵌 build-id 的缓存文件名(depends/funcs.mk);
- 干净 sysroot——configure 前
rm -rf $(host_prefix)再只解包递归依赖; - build-id 缓存——
meta_depends全量哈希 + 递归依赖 id 级联 +gen_id工具链指纹,改主 Makefile 全量重建、改单包配方级联重建; - 相对确定性——固定 touch 时间、
TZ=UTC+LC_ALL=C排序的 tar、--numeric-owner,并明确声明时间戳注入等边界外事项; - 源码自动抓取校验——四元组身份定义 + 下载/解包双重 sha256 校验 +
FALLBACK_DOWNLOAD_PATH回退 +download*预取目标; - 自清理——阶段间目录回收、
clean/clean-all目标、check-packages哈希淘汰。
这套机制使 Bitcoin Core 的第三方依赖构建成为"可复现、可分发、可自动化"的工程资产:任何自动化构建机拉取代码、运行 make、按 build-id 取/放缓存 tarball 即可完成每个 revision 的依赖构建,无需人工干预。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00