MongoDB 的 Bazel 安装规则(install_rules)架构解析:从构建产物到可发布安装树
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
本文深入解析 MongoDB 仓库中bazel/install_rules目录实现的安装规则(Install Rules)体系。它负责把 Bazel 构建出来的二进制、动态库、测试与数据文件编排成一套完整的"安装树",并提供bazel-bin/install便捷树与 tgz/zip 归档。读完本文,你将掌握该规则的分析(Analysis)、物化(Materialization)与共享便捷树(Shared Convenience Tree)三段式架构,理解其如何规避并发竞态、处理符号链接与跨平台路径校验,并能在自己的BUILD.bazel中正确使用mongo_install宏。
一、定位与背景:安装规则解决什么问题
MongoDB 是一个拥有数千个 Bazel 目标的大型 C++ 工程。构建完成之后,mongod、mongos、mongoshell、wiredtiger 工具等产物散落在bazel-out/<配置>/bin/...下,还带有大量动态库、调试信息(.dwp、.dSYM、.pdb)、测试二进制及其 runfiles 数据。安装规则的目标是:
- 把一个目标的完整产物收敛到一棵结构化的安装树中(
bin/、lib/等); - 为每个
mongo_install_rule目标生成一个安装 action; - 在
bazel-bin/install发布一棵"便捷树",让开发者可以直接在构建根下访问安装结果; - 同时为打包生成 zip/tgz 归档。
整个流程可以用 README 中的架构图概括(原文见 bazel/install_rules/README.md):
inputs and transitive source maps │ ▼ Bazel analysis: normalize destinations, assign owners, reject overlaps │ ▼ declared outputs + install depfile │ ▼ one Python install action ┌────┴────┐ ▼ ▼ action-private shared staging tree output tree (outside the lock) │ ▼ lock + atomic rename │ ▼ bazel-bin/install这条流水线把"每个目标一棵私有安装树"与"全局共享便捷树"两种需求分离:分析阶段在 Bazel 内完成冲突检查,物化阶段由 Python 脚本 install_rules.py 执行,共享树则通过"无锁暂存 + 加锁原子改名"的方式并发安全地发布。
二、分析阶段:所有权映射、冲突拒绝与 depfile
分析阶段全部在 Bazel 分析期完成,核心实现位于 install_rules.bzl 的mongo_install_rule_impl(L423-L707)。
2.1 所有权映射与规范化
规则会收集每一个请求的安装目的地并存入install_owners映射。目的地先经过_normalize_install_destination(L254-L278)规范化:
- 拒绝空路径、包含反斜杠、绝对路径或含
:的路径; - 拒绝包含
""、.、..等非法组件的路径; - 路径不能逃逸安装树(不允许
../前缀); - 最终返回
(normalized, normalized.lower())二元组——比较是大小写不敏感的,注释明确说明:即使当前执行文件系统大小写敏感,也要保守地拒绝仅大小写不同的别名,因为安装树和归档会在不同文件系统之间迁移。
2.2 三类冲突在 action 启动前被拒绝
_declare_install_output(L295-L374)在分析期就完成冲突判定,避免安装 action 之间发生竞态:
- 精确冲突(exact conflict):同一目的地被不同源占用,报
install destination collision; - 文件/目录冲突:
lib/tool与lib/tool/config这类前缀重叠(prefix overlap),报install destination prefix collision——无论新目标是祖先还是后代; - 重复引用去重:相同的源 + 目的地 + 类型组合通过多条依赖路径到达时,只声明一个输出,同一目的地由多个源写入则直接失败。
这些行为都有对应的分析期测试固化在 install_rules_test.bzl 中:_exact_collision_test期望报错install destination collision,_prefix_collision_test期望报错install destination prefix collision,_invalid_path_test期望报错invalid install destination,而_diamond_deduplication_test验证"通过菱形依赖(diamond)到达的同一产物只声明一个输出"。
2.3 输出声明与 depfile 格式
规则一次性声明完整输出集合(declare_file/declare_directory),并写出描述安装清单的 depfile。depfile 是一个 JSON 文件,包含四类键(见 L598-L607):
{ "roots": { "/abs/path/file": "folder" }, "includes":{ "/abs/path/file": "lib/renamed.so" }, "bins": [ "/abs/path/bin1", "/abs/path/bin2" ], "libs": [ "/abs/path/lib1" ] }bins/libs:可执行文件与动态库,分别落入bin/与lib/;roots:任意文件 + 目标目录,空目录表示放到安装树根部;includes:显式重命名的头文件/文件,映射到精确目标路径(is_rename=True)。
从源码结构看,文件在分析期就被分类到六个桶中(L435-L442):binaries、binaries_debug、dynamic_libs、dynamic_libs_debug、root_files、include_files。分类逻辑sort_file(L376-L421)按平台判断:Linux 上lib*前缀视为库,Windows 按.dll/.exe/.pdb/.ps1扩展名判断(_WINDOWS_BINARY_EXTENSIONS);调试文件按.debug/.dwp、.dSYM、.pdb分类(_LINUX_DEBUG_EXTENSIONS、_MACOS_DEBUG_EXTENSIONS、_WINDOWS_DEBUG_EXTENSIONS)。
2.4 依赖传递与"子树不重复发布"
deps中的子安装目标通过MongoInstallInfoprovider(定义于 providers.bzl,字段见 L71-L80)向上传递src_map、source_files、install_owners等。聚合目标会把子目标的源文件映射"扁平化"进自己的 depfile,但不会消费子安装 action 的输出——即子安装树不会被父目标重新发布。_transitive_source_input_test专门断言:父 action 必须声明子 manifest 命名的原始文件,但不得把子安装目录或子 depfile 作为输入。
分析期还顺带生成install_deps/<pkg>_<name>/<name>依赖文件与installed_tests.txt测试清单(L553-L574),其中测试路径会被改写成bazel-bin/install/...前缀,方便后续在安装树上直接定位测试二进制。
三、mongo_install 宏:一个目标、三类变体、多种归档
普通BUILD.bazel不直接调用mongo_install_rule,而是使用mongo_install宏(L728-L944)。宏为每个name展开三类安装变体(L757):
install-<name>:完整安装,包含二进制与调试信息;install-<name>-stripped:仅安装 stripped 后的二进制;install-<name>-debug:仅安装调试信息。
同时生成对应归档目标:archive-<name>_tar(tgz,Linux/macOS)、archive-<name>_zip(Windows)、可选archive-<name>_zst(启用 zstd 时,使用@pigz//:bin或@zstd//:bin作为压缩器),统一由archive-<name>filegroup 按平台 select 聚合。
规则属性(L709-L726)包括:
| 属性 | 说明 |
|---|---|
srcs | 要安装的目标列表,携带test_binary_aspect收集测试二进制 |
deps | 其他安装规则目标,作为依赖子树 |
root_files | {label: 目录}映射,把任意文件放到指定目录 |
include_files | {label: 精确路径}映射,显式重命名安装 |
debug | ""/"stripped"/"debug"三态 |
publish_debug_in_stripped | stripped 变体是否同时发布调试文件 |
create_dwp | 是否生成.dwp调试包(受//bazel/config:dwp_supported配置控制) |
在仓库根目录的 BUILD.bazel 中可以找到大量真实用法,例如:
- L201-L209:
mongo_install(name = "mongod", srcs = ["//src/mongo/db:mongod", ...]); - L212-L217:
mongo_install(name = "mongos", ...); - L220-L225:
mongo_install(name = "mongo", srcs = ["//src/mongo/shell:mongo"]); - L227-L235:
mongo_install(name = "core", deps = ["mongod", "mongos"])演示纯聚合目标; - L237-L249:
mongo_install(name = "devcore", root_files = {"//x509:generate_main_certificates": "bin/x509"}, deps = [...])演示root_files用法。
由此可以推断:使用方式为bazel build //:install-mongod构建安装树,用bazel build //:archive-mongod产出发布归档。
四、物化阶段:action 私有输出树与文件发布策略
安装 action 由ctx.actions.run发起(L661-L680),mnemonic 为MongoInstallRule,执行 install_rules.py。脚本先清空并重建--install-dir(action 私有的输出树),然后逐文件写入。
4.1 原子写入与硬链接优先
每个文件都通过临时路径写入,再用os.replace发布,避免暴露半写状态(_copy_file_atomically,L274-L292)。发布策略在_link_or_copy_file(L295-L324)中分级:
- 普通文件:文件系统允许时使用硬链接(
os.link(..., follow_symlinks=False)); - 稳定的 Bazel 输出符号链接(
_stable_output_symlink_target判定为指向bazel-out中持久文件的链接):不解析目标直接硬链接;若硬链接跨设备(如 Linux 容器中 action 输出与共享树分属不同挂载点),则退化为指向持久输出的绝对符号链接,而不是把可能数 GiB 的目标复制出来; - 源码树内、相对或沙箱内符号链接:直接硬链接会在沙箱被移除后悬空,因此改为复制并解引用(
shutil.copyfile+copymode); - 目录:递归复制(
_copy_directory),并用(st_dev, st_ino)身份集合做符号链接环检测(L327-L364),检测到环时抛Directory symlink cycle错误。
值得注意的边界处理:Windows 上只读文件(源 inode 带只读位)必须复制而非硬链接——因为清除一个硬链接的只读位会同时改变 Bazel 源 inode;同理,清理只读目录时只 chmod 目录本身,绝不 chmod 硬链接文件(_remove_readonly/_remove_tree,L30-L62)。test_directory_install_preserves_source_mode与test_directory_install_hardlinks_regular_files等用例在 install_rules_script_test.py 中验证了这些保证。
4.2 命令行参数
脚本参数(L23-L27):
--depfile 可重复,指向安装清单 JSON --install-dir 安装输出目录 --install-mode copy|symlink|hardlink(默认 hardlink;当前仅 hardlink 实现)脚本按install_once(L568-L581)以目标路径为键去重:同一逻辑目标重复出现(包括同一 depfile 传两次)只安装一次;不同源争抢同一目标则报Install destination ... has multiple sources(见test_duplicate_depfiles_install_each_entry_once与test_conflicting_sources_for_one_destination_fail)。
五、共享便捷树:无锁暂存 + 加锁原子改名
共享便捷树是这套设计中最精巧的部分,目标是在多 action 并发时既不出半成品树、又尽量少地持有锁。
5.1 暂存无锁、发布加锁
共享树按配置(configuration)隔离(即bazel-out/<配置>/bin中的配置名)。物化分两步(_install_shared_destination,L416-L500):
- 在
.staging目录创建tempfile.mkdtemp工作区,不持有锁完成昂贵的复制/暂存(对 dSYM、工具链目录这种大树尤其重要,能让无关安装 action 并行推进); - 持有配置级排它锁(
_exclusive_file_lock,Unix 用fcntl.flock,Windows 用msvcrt.locking),仅执行常数时间的 rename:先把旧目标改名到工作区的old,再把暂存的new原子改名为目标。
锁只覆盖最后的 rename 操作,因此并发安装 action 不会互相暴露部分写入的树,同时锁竞争时间被压到最低。发布后,被顶替的旧目标(可能也是 dSYM 大树)在锁外异步清理。
5.2 中断回滚与权限恢复
- 回滚:如果发布期间被异步中断(SIGTERM、KeyboardInterrupt 或 IO 错误),脚本会检查工作区而非依赖内存标志——若
old仍存在,则把new挪回暂存位、把old还原为目标路径,绝不删除旧目标。若回滚本身也失败,则报错并保留工作区作为恢复数据(recovery data remains at ...)。test_shared_install_restores_previous_destination_when_publish_fails用os.replace注入 EIO/EPERM 完整覆盖了这些路径; - SIGTERM 转异常:
_termination_as_exception(L90-L101)把 SIGTERM 转成SystemExit(128+signum),让安装事务可以正常走完清理逻辑; - 权限恢复:发布 rename 前对共享目录、目标父目录、暂存目录恢复 owner 写/搜索权限(
_make_directory_writable,L65-L72),因为 Bazel 可能在本地 action 完成后把输出树目录设为只读。test_shared_install_reopens_readonly_publication_directory专门回归验证这一场景; - 不穿越他人输出目录:发布路径在使用前先
realpath解析父目录并校验仍在共享根内(_destination_within,L534-L546),避免便捷树误入另一个 action 的输出目录。
5.3 路径校验的完整清单
_install_relative_path(L503-L531)与 bzl 侧校验共同覆盖:
- 目录穿越(
../、..)、绝对路径; - Windows 反斜杠分隔符、盘符前缀(
C:/); - 尾部点、尾部空格(Windows);
- Windows 保留 DOS 设备名:
CON、PRN、AUX、NUL、COM1–COM9、LPT1–LPT9(完整集合见 L29-L52 的_WINDOWS_RESERVED_BASENAMES)。
test_install_destination_cannot_escape_tree对../escaped、绝对路径、C:/escaped、lib\escaped逐一断言拒绝。
六、为什么禁用缓存与远程执行:与 wrapper 的配合
由于共享便捷树位于 action 声明输出之外,MongoInstallRule在execution_requirements中显式声明(L672-L679):
execution_requirements = { "no-cache": "1", "no-remote": "1", }否则该 action 的结果会被缓存或远程执行,而便捷树发布不在声明输出内。相应地,wrapper 负责选择本地或容器执行策略,并把发布路径暴露为可见、可写,而不是藏在 action 沙箱里。
共享根目录的位置策略(install_rules.py L183-L216)在 wrapper 侧有对应实现(hermetic_container_integration.py 的_linux_shared_install_dir/_host_temp_shared_install_dir等):
- Linux 容器 action:通过环境变量
MONGO_BAZEL_SHARED_INSTALL_DIR拿到"output-base 同级"的共享根(<output-base>-mongo-shared-install),并以可写方式挂载,从而与本地 native 回退动作发布到同一棵树; - Linux 本地 native action:未显式提供该变量时,回退到宿主临时目录(
<output-base 名>-mongo-shared-install),同样避免输出根被保护; - macOS:跨主机安装 action 在环境缺少该变量时使用相同的 host-temp 布局(
_darwin_shared_install_root); - 每次构建后,wrapper 把
bazel-bin/install符号链接指向共享根下的对应配置目录(_publish_external_convenience_symlink)。
把共享树放在 Bazel 输出根层级之外,可以规避输出目录的权限问题与 finalization 竞态——这正是设计文档反复强调的动机。test_linux_default_shared_install_is_outside_output_tree、test_macos_default_shared_install_is_outside_output_tree与test_shared_install_directory_survives_action_sandbox验证了"共享树在沙箱销毁后依然有效、且bazel-bin/install正确指向共享根"这一核心行为。
七、测试体系:分析期与脚本期双层验证
安装规则自带两套测试,均挂在 BUILD.bazel 的install_rules_test测试套件下:
- 分析期测试(install_rules_test.bzl,基于 skylib
analysistest):覆盖精确冲突、前缀冲突、非法路径、菱形依赖去重、单源多目的地(bin/multi.exe+share/multi.exe)、测试数据 runfiles 安装布局、GDB 工具链按//bazel/config:install_gdb配置开关安装; - 脚本级测试(install_rules_script_test.py,Python unittest +
runpy加载脚本):覆盖 Windows 路径分隔符、空目录 root 文件、共享根在输出树外、稳定符号链接硬链接与 EXDEV 降级、目录符号链接解引用、长路径、只读目录重开、重复 depfile 去重、多源冲突、逃逸拒绝、发布回滚、并发 samefile 竞态等 20+ 场景。
这两层测试共同锁定了 README 描述的每一条保证,是理解规则行为最可靠的入口。
八、小结
MongoDB 的安装规则把"安装"拆成了三个职责清晰的阶段:
- 分析期(Bazel/Starlark):规范化目的地、建立所有权映射、拒绝精确/前缀/文件目录冲突、生成 depfile 与测试清单,保证冲突在 action 启动前暴露;
- 物化期(Python action):action 私有树 + 临时文件 +
os.replace原子发布,硬链接优先、符号链接按稳定性分级处理、目录复制带环检测; - 发布期(共享便捷树):配置隔离、无锁暂存、加锁常数时间 rename、失败回滚、权限恢复,并把
bazel-bin/install指向共享根,同时以no-cache/no-remote与 wrapper 策略确保发布不被缓存或沙箱破坏。
对任何希望"把 Bazel 构建结果变成可交付安装树"的工程而言,这套规则在并发安全、跨平台路径语义与符号链接处理上的取舍,都值得作为参考实现来阅读。继续深入可从 bazel/install_rules/README.md 出发,配合 install_rules.bzl 与 install_rules.py 逐层对照,并通过仓库根 BUILD.bazel 中的install-mongod等真实目标验证使用方式。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考