Bazel 输出目录布局完全指南:outputRoot、outputBase 与符号链接的运作原理
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
本文以 Bazel 官方的 Output Directory Layout 文档 为核心骨架,系统讲解 Bazel 输出目录(output directory)的设计需求、当前实现的分层布局(outputRoot → outputUserRoot → installBase / outputBase)、bazel clean的清理语义,并结合本仓库的 Java 源码与启动选项实现,深入解释--output_base、--output_user_root等 startup option 的底层作用。读完本文,你将能精准定位 Bazel 的每一份构建产物与缓存状态、理解bazel-bin/bazel-out等符号链接的真实指向,并能在多用户、多工作区、多目标配置共存的环境下安全地管理和清理构建输出。
输出目录布局的设计需求
Bazel 把一切构建产物、缓存与服务器状态集中写入一个"输出目录"体系。之所以需要一套严谨的布局,是因为构建系统必须同时满足以下要求:
- 多用户互不冲突:多个用户在同一台机器上构建时,输出目录不能互相覆盖;
- 多工作区并行:同一时刻可以构建多个不同的工作区(workspace);
- 多目标配置共存:同一个工作区内可以同时为多种目标配置(如
k8-fastbuild与k8-opt)产出结果; - 与其他工具不冲突:不能污染用户目录下其他工具的命名空间;
- 易于访问:用户能方便地找到产物;
- 易于清理,且支持选择性清理;
- 无歧义:即使用户通过符号链接进入客户端目录,输出路径的解析依然唯一;
- "一个用户的所有构建状态收敛在同一个目录下":用户希望"把我所有客户端的 .o 文件一次性清干净",这就要求每个用户的状态整体上位于一个可整体删除的根目录之下。
这 8 条需求不是凭空设想,而是后续布局设计的验收标准。任何对布局的改动都必须继续满足它们。
当前实现的目录分层
当前实现的解决方案建立在"必须从仓库内部调用 Bazel"这一前提之上:Bazel 必须从包含仓库边界文件(repository boundary file)的目录或其子目录中启动,否则会直接报错。所谓仓库边界,正是 仓库(repository) 的概念——Bazel 9 及以后基于MODULE.bazel(Bzlmod)识别工作区边界。在此基础上,输出目录被组织成如下四个层级:
outputRoot:机器级输出根目录
outputRoot是整台机器上所有 Bazel 输出的总根,其默认值随平台而异:
| 平台 | 默认 outputRoot |
|---|---|
| Linux | ~/.cache/bazel |
| macOS(Bazel 9 及更新版本) | ~/Library/Caches/bazel |
| Windows | %HOME%(若未设置则用%USERPROFILE%,再退化为调用SHGetKnownFolderPath()并传入FOLDERID_Profile的结果) |
同时存在两条环境变量覆盖规则:
- 在 Linux、macOS、Windows 上,若设置了
$XDG_CACHE_HOME,则使用${XDG_CACHE_HOME}/bazel覆盖上述默认值; - 若设置了
$TEST_TMPDIR(例如在 Bazel 自身的测试中),则该值覆盖一切默认值。
需要注意版本差异:Bazel 8.x 及更早版本在 macOS 上使用/private/var/tmp作为 outputRoot,并且忽略$XDG_CACHE_HOME。如果你在 macOS 上从 8.x 升级到 9.x,输出目录会从/private/var/tmp/_bazel_$USER迁移到~/Library/Caches/bazel/_bazel_$USER,旧的缓存不会自动搬移。
outputUserRoot:用户级目录
Bazel 用户的全部构建状态位于outputRoot/_bazel_$USER,这个目录被称为outputUserRoot。它是"按用户隔离"这一需求的直接体现——同一台机器上的不同用户各自拥有独立的_bazel_$USER子目录,从结构上杜绝了互相踩踏。
installBase 与 outputBase:两个 MD5 命名的兄弟目录
在outputUserRoot之下存在两个并列的目录,都由内容哈希命名:
- install 目录下的 installBase:目录名是 Bazel 安装清单(installation manifest)的 MD5 哈希。它存放与 Bazel 自身二进制解包相关的内容,例如主服务端 Java 应用
A-server.jar、沙箱辅助二进制、进程包装器、随包附带的 JDK 与内嵌构建工具源码等。每次 Bazel 版本更新导致安装清单变化时,哈希随之变化,从而自然支持多个 Bazel 版本共存。 - outputBase:目录名是工作区根目录路径的 MD5 哈希。例如在工作区根目录
/home/user/src/my-project(或其符号链接指向的同一目录)运行 Bazel 时,生成的 outputBase 为/home/user/.cache/bazel/_bazel_user/7ffd56a6e4cb724ea575aba15733d113。你可以在工作区根目录执行echo -n $(pwd) | md5sum复现这个哈希值。
outputBase承载该工作区的全部状态:动作缓存(action cache)、命令日志、外部仓库、服务端文件与execroot。它的哈希命名机制保证了:
- 同一个工作区路径永远映射到同一个 outputBase(缓存可复用);
- 同一台机器上不同路径的工作区拥有互不相同的 outputBase(互不干扰);
- 即使用户经由符号链接进入工作区,只要解析后的真实路径一致,outputBase 依然唯一——这正对应设计需求中的"无歧义"条款。
启动选项覆盖
用户可以通过两个 startup option 显式覆盖默认行为(startup option 必须位于子命令左侧,且选项名与值之间不能有空格,参见 startup_options.txt 的说明):
# 覆盖 outputBase:所有构建输出写入 /tmp/bazel/output bazel --output_base=/tmp/bazel/output build x/y:z # 覆盖 outputUserRoot(同时覆盖其下的 installBase 与 outputBase) bazel --output_user_root=/tmp/bazel build x/y:z从源码看,这两个选项在 BlazeServerStartupOptions.java 中被定义为BAZEL_CLIENT_OPTIONS:
--output_base的 help 文本明确警告:如果多次调用间该值不一致,很可能会启动一个新的、额外的 Bazel server。因为 Bazel 为每个 output base 恰好启动一个 server,典型情况下每个工作区一个 output base;而使用该选项后,同一个工作区可以拥有多个 output base,从而在同一台机器上并发运行同一客户端的多个构建。--output_user_root则通过固定用户目录,让协作用户之间可以共享构建输出——默认的_bazel_$USER按用户名隔离,而显式指定后便打破了这一隔离。
与之配套的还有--install_base(仅测试用途)、--server_jvm_out(服务端 JVM 输出,默认落在 output base 内)等隐藏/辅助选项。
完整目录布局图
原文档给出了如下权威的目录树。为便于阅读,此处保留其全部细节:
<workspace-name>/ <== 工作区根目录 bazel-my-project => <..._main> <== 指向 execRoot 的符号链接 bazel-out => <...bazel-out> <== 指向 outputPath 的便捷符号链接 bazel-bin => <...bin> <== 指向最近写入的 bin 目录 $(BINDIR) 的便捷符号链接 bazel-testlogs => <...testlogs> <== 指向测试日志目录的便捷符号链接 /home/user/.cache/bazel/ <== 机器上所有 Bazel 输出的根:outputRoot _bazel_$USER/ <== 某用户专属的顶层目录,取决于用户名: outputUserRoot install/ fba9a2c87ee9589d72889caf082f1029/ <== Bazel 安装清单的哈希:installBase A-server.jar <== 主 Bazel server Java 应用,首次运行时 从 bazel 可执行文件的数据段解包而来 linux-sandbox <== 沙箱辅助二进制(平台相关) process-wrapper <== 动作执行用的进程包装器 embedded_tools/ <== 内含随包 JDK、构建工具源码及 server 所需的其他资源 7ffd56a6e4cb724ea575aba15733d113/ <== 客户端工作区根目录的哈希(如 /home/user/src/my-project):outputBase action_cache/ <== 动作缓存目录层级 持久化记录文件元数据(时间戳, 未来或许还包括 MD5 校验和), 供 FilesystemValueChecker 使用 command.log <== 最近一次 bazel 命令的 stdout/stderr 副本 external/ <== 远程仓库被下载/符号链接进来的目录 server/ <== Bazel server 的所有相关文件 (server PID、TCP 命令端口、 请求/响应 cookie、JVM 日志) jvm.out <== server 的调试输出 execroot/ <== 所有动作的工作目录。对于沙箱、 远程执行等特殊情况,动作运行在 模拟 execroot 的目录中。实现细节 (如目录创建位置)刻意对动作隐藏。 每个动作都能相对 execroot 访问其 输入与输出 _main/ <== Bazel 构建的工作树与符号链接林根:execRoot _bin/ <== 辅助工具被链接或复制到这里 bazel-out/ <== 构建的全部实际输出都在其下:outputPath _tmp/actions/ <== 动作输出目录。其中包含最近一次 产生输出的 bazel 运行中每个动作的 stdout/stderr 文件 k8-fastbuild/ <== 每个唯一的目标 BuildConfiguration 实例 一个子目录;以编码 CPU 与编译模式的 助记符命名(如 k8-fastbuild、k8-opt、 k8-dbg)。带 Starlark transition 的 配置会追加 ST-hash 后缀 (如 k8-fastbuild-ST-abc123) bin/ <== Bazel 将目标配置的二进制输出到此处:$(BINDIR) foo/bar/_objs/baz/ <== 名为 //foo/bar:baz 的 cc_* 规则的对象文件 foo/bar/baz1.o <== 来自源文件 //foo/bar:baz1.cc 的对象文件 other_package/other.o <== 来自源文件 //other_package:other.cc 的对象文件 foo/bar/baz <== foo/bar/baz 可能是名为 //foo/bar:baz 的 cc_binary 生成的产物 foo/bar/baz.runfiles/ <== //foo/bar:baz 可执行文件的 runfiles 符号链接林 MANIFEST _main/ ... testlogs/ <== Bazel 内部测试运行器把测试日志放到这里 foo/bartest.log 如 foo/bar.log 可能是 //foo:bartest 测试的 foo/bartest.status 输出,foo/bartest.status 存放测试退出状态 (如 PASSED、FAILED (Exit 1) 等) k8-opt-exec/ <== exec 平台对应的 BuildConfiguration, 用于构建后续阶段所需的先决工具 (如 Protocol Compiler) <packages>/ <== 构建中引用的包,如同位于普通 工作区之下图中几个层级值得单独说明:
execRoot(<outputBase>/execroot/_main):所有 action 的工作目录,同时是符号链接林的根。Bazel 在此把工作区源码以符号链接的形式"种"出来,让被调用的工具认为自己工作在一个普通的工作区里。outputPath(<execRoot>/bazel-out):所有实际构建输出的总目录。其下先按目标配置(configuration)划分出k8-fastbuild、k8-opt、k8-dbg等子目录,再在每个配置目录内按bin、testlogs等功能目录组织。配置助记符编码了 CPU 与编译模式;一旦涉及 Starlark transition,还会追加-ST-<hash>后缀以区分不同配置实例。_tmp/actions/:位于bazel-out之下,保存最近一次运行中每个动作的 stdout/stderr,是排查动作失败的利器。
*.runfiles目录的具体布局在代码中以RunfilesSupport为入口有更详细的文档说明(见原文档末尾的指引)。
工作区内的便捷符号链接
在工作区目录(而不是 outputBase)中,Bazel 会创建bazel-<workspace-name>、bazel-out、bazel-testlogs、bazel-bin四个符号链接,它们分别指向 outputBase 内目标配置专属目录下的对应位置。要点如下:
- 这些符号链接仅供用户方便使用,Bazel 自身并不依赖它们——真正的路径解析走的是 outputBase 内部的规范路径;
- 只有当工作区根目录可写时才会创建它们;
bazel-bin指向的是"最近写入的"$(BINDIR),因为一次构建可能涉及多个目标配置,bazel-bin总是指向最新产出的那一个。
在源码层面,这些符号链接由 SymlinkForest.java 负责"种植"(plantSymlinkForest),它同时负责把主仓库的源码以符号链接林的形式映射到execRoot下,使所有包在 execroot 中"如同位于普通工作区之下"。设计上刻意把 execroot 内部的实现细节对 action 隐藏,保证动作的可移植性与沙箱/远程执行的一致性。
从源码看输出目录的封装
本仓库的 Java 实现把上述概念收敛在几个核心类中:
- BlazeServerStartupOptions.java:定义
install_base、output_base、output_user_root等启动选项及其帮助文本,并明确"每个 output base 恰好一个 server"的对应关系。 - BlazeDirectories.java:注释中直接给出了四组核心路径的口语化定义——
workspace:含WORKSPACE/MODULE.bazel文件、标记并配置工作区的根目录;outputBase:所有工作区输出(构建产物 + 动作缓存、日志、外部仓库映射等内部文件)的所在地,形如_bazel_$USER/$SOME_HASH/,可通过bazel info | grep output_base查询;execRoot:所有被调用工具的工作目录,形如$OUTPUT_BASE/execroot/$WORKSPACE_IDENTIFIER,可通过bazel info | grep execution_root查询;outputPath:构建输出写入的根路径,形如$OUTPUT_BASE/execroot/$WORKSPACE_IDENTIFIER/bazel-out,可通过bazel info | grep output_path查询。- 该类同时强调:必须避免多个 Bazel 实例写入同一输出树,这一约束正是通过"运行中的 Bazel 实例与 output base 一一对应"来强制实现的。
- OutputBaseInfoItem.java:
bazel info output_base命令背后的实现,直接打印当前环境的 outputBase 路径,是日常确认输出位置的最快手段。
也就是说,你可以用如下命令快速核对本文介绍的每个层级:
bazel info output_base bazel info execution_root bazel info output_path bazel info install_basebazel clean与--expunge:清理语义与层级
清理命令的语义与目录分层严格对应,理解层级即可预测清理范围:
bazel clean:先清空磁盘上的动作缓存(action cache),然后删除整个execroot目录(其中包含符号链接林与全部构建输出),最后移除工作区目录中的便捷符号链接(bazel-bin等)。执行之后,outputBase 中仍保留command.log、server/、external/等状态与缓存。bazel clean --expunge:在普通 clean 的基础上进一步清空整个 outputBase,包括外部仓库、server 状态与命令日志等一切内容。这等价于把该工作区从这台机器的 Bazel 状态中彻底抹去,下次构建将重新初始化一切。- 若要清理到用户级甚至机器级(例如彻底释放磁盘空间),则需直接操作
outputUserRoot乃至outputRoot——这正是"所有构建状态按用户收敛在一个目录下"这一设计需求带来的便利:删除_bazel_$USER即可一次性清掉该用户所有客户端的构建缓存。
需要注意:bazel clean不会删除installBase。因为 installBase 由安装清单哈希唯一标识,且可能被同一用户的不同 server 实例共享,它的生命周期由 Bazel 自身的安装管理机制(如--lock_install_base防并发回收)负责,而不是由工作区级清理命令管辖。
实战要点小结
- 定位:日常使用
bazel info系列命令(output_base、execution_root、output_path、install_base)即可精确获得各层路径,无需手动推算 MD5。 - 覆盖:CI 或需要共享产物时用
--output_base/--output_user_root指定固定位置;但务必牢记--output_base变化会派生新的 Bazel server,频繁切换会白白增加 server 数量。 - 清理:增量产物残留用
bazel clean;需要连外部仓库与 server 状态一起重来(例如排查非封闭行为)时用bazel clean --expunge;要清空某用户全部状态则删除整个_bazel_$USER目录。 - 不要依赖符号链接:
bazel-bin、bazel-out仅是便捷入口且指向"最近写入"的配置目录,脚本中对产物路径的引用应以bazel info的规范路径为准。 - 版本差异:macOS 上 Bazel 8.x 与 9.x 的 outputRoot 不同(
/private/var/tmpvs~/Library/Caches/bazel),升级后旧缓存不会自动迁移。
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考