用脚本稳健地调用 Bazel:output_base、退出码与命令日志实战指南
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
在 CI 流水线、发布脚本或各类自动化工具中,常常需要把bazel build、bazel test、bazel query当作子进程来调用。Bazel 在设计上对脚本化调用相当友好,但要让脚本足够健壮,还需要掌握几个关键细节:输出基目录(output base)的锁竞争、服务端进程的生命周期、完整的退出码语义,以及如何干净地解析构建输出。本文以 Bazel 官方文档 docs/run/scripts.mdx 为主体,结合本仓库中的服务端实现与退出码源码,系统讲解在脚本中调用 Bazel 的正确姿势,读完你就能写出不会被并发锁、残留服务端进程或模糊退出码坑到的可靠脚本。
选择合适的输出基目录:--output_base
--output_base选项决定了 Bazel 进程把构建产物以及各种内部工作文件写到哪个目录。这些内部工作文件中有一个关键角色:锁文件,它用于防止多个 Bazel 进程并发地修改同一个输出基目录。
注意:
--output_base是一个 startup option(启动选项),必须写在命令最前面、紧跟bazel之后,例如bazel --output_base=/tmp/foo build //...,而不是放在子命令后面。
脚本应该选哪个输出基目录,取决于你的诉求:
- 需要把构建产物放到特定位置:此时输出基目录的选择由产物位置决定。
- 做"只读"调用(如
bazel query):锁的竞争因素更重要。因为每个 Bazel server 进程同一时刻最多只能处理一次调用(详见 docs/run/client-server.mdx 中的客户端/服务端实现说明),如果你的脚本需要并发运行多个实例,可以选择让每个实例排队等待,也可以为每个实例指定不同的--output_base,各自启动独立的 server,从而真正并行。
如果脚本使用默认的输出基目录,就会和用户交互式执行的 Bazel 命令竞争同一把锁。用户一旦发起了bazel build这样的长任务,你的脚本就必须等它跑完才能继续。从 client-server 实现 可知,输出基目录默认由工作区根目录路径和用户 ID 共同决定,因此:
- 同一台机器上构建多个工作区,会产生多个输出基目录,从而有多个 Bazel server 进程;
- 多个用户在同一工作区构建,由于 userid 不同,输出基目录不同,可以并发构建互不干扰。
从源码上看,output_base与output_user_root都在 BlazeServerStartupOptions.java 中定义(其中output_base的说明中提到它会定位到output_user_root之下的具体位置)。也就是说,输出基目录实际上是"用户根目录 + 工作区 + 用户"维度的细分,这解释了为什么它能作为 server 定位与锁隔离的依据。
服务端模式:别忘了shutdown或设置空闲超时
Bazel 默认采用一个常驻的 server 进程 来提升性能:它可以在多次构建之间缓存 BUILD 文件、依赖图和其他元数据,从而加速增量构建,并让build、query等不同命令共享已加载的包缓存。
这个优化对脚本有两个直接后果:
- 每次
bazel调用,客户端会按输出基目录查找(或启动)server,命令结束后 server 并不会立刻退出,而是继续常驻等待下一次调用; - server 默认空闲 3 小时后才自行关闭(可通过启动选项
--max_idle_secs修改)。
因此在脚本中,要么在任务收尾时显式执行bazel shutdown,要么在启动参数里加上--max_idle_secs=5,让空闲的 server 快速自行关闭。否则,自动化脚本在不同目录下反复构建,很容易在机器上堆积大量闲置的 server 进程。这一点在 client-server 文档 中被特别强调,是写批处理/CI 脚本最常见的隐患之一。
--max_idle_secs与--block_for_lock都在 BlazeServerStartupOptions.java 中实现:前者控制"空闲多久后关闭 server",后者则决定当锁被占用时是等待还是直接失败(对应下文退出码9)。
脚本最关心的:Bazel 退出码全解
Bazel 会尽力区分两类失败:源代码本身的问题(源码缺陷,重跑同一输入结果可复现)和妨碍 Bazel 正常执行的外部错误(命令行、机器或环境问题)。这两类在源码 ExitCode.java 中被明确注释区分,其中isInfrastructureFailure()方法用于标识基础设施类失败,方便基础设施用户(如 CI 编排系统)决定是否需要重试。
所有命令通用的退出码
| 退出码 | 含义 | 源码常量 |
|---|---|---|
0 | 成功 | SUCCESS |
2 | 命令行问题:非法/违规的标志或命令组合、坏的环境变量,需要修改命令行 | COMMAND_LINE_ERROR |
8 | 构建被中断,但已有序关闭 | INTERRUPTED |
9 | server 锁被占用,且传入了--noblock_for_lock | LOCK_HELD_NOBLOCK_FOR_LOCK |
32 | 外部环境失败(非本机) | REMOTE_ENVIRONMENTAL_ERROR |
33 | Bazel 内存耗尽崩溃,需要修改命令行 | OOM_ERROR |
34 | 远程执行、远程缓存或 Build Event Service 错误 | REMOTE_ERROR |
35 | 保留(Google 内部使用) | — |
36 | 本地环境问题,疑似永久性 | LOCAL_ENVIRONMENTAL_ERROR |
37 | 未处理异常 / Bazel 内部错误 | BLAZE_INTERNAL_ERROR |
38 | 向 Build Event Service 上报结果的瞬时错误 | TRANSIENT_BUILD_EVENT_SERVICE_UPLOAD_ERROR |
39 | Bazel 所需的 blob 被远程缓存驱逐 | REMOTE_CACHE_EVICTED |
41–44 | 保留(Google 内部使用) | — |
45 | 向 Build Event Service 上报结果的持续性错误 | PERSISTENT_BUILD_EVENT_SERVICE_UPLOAD_ERROR |
47 | 保留(Google 内部使用) | — |
48 | 外部依赖错误 | EXTERNAL_DEPS_ERROR |
49 | 保留(Google 内部使用) | — |
bazel build/bazel test的专属返回码
| 退出码 | 含义 |
|---|---|
1 | 构建失败(源码问题) |
3 | 构建成功,但部分测试失败或超时 |
4 | 构建成功,但请求了测试却没有找到任何测试 |
bazel run的返回码
1:构建失败;- 如果构建成功,但被执行的子进程返回了非零退出码,则该子进程的退出码就是整条命令的退出码(
RUN_FAILURE对应退出码6)。
bazel query的返回码
3:部分成功——查询在输入的 BUILD 文件集合中遇到 1 个或多个错误(常见于命令行带了--keep_going),因此结果不是 100% 可靠;7:命令失败(对应源码中的ANALYSIS_FAILURE)。
需要留意的是,未来版本可能增加新的退出码,也可能把通用的失败退出码1替换为语义更具体的非零值。但有一个不变的原则:任何非零退出码都代表出错。因此脚本的正确写法是"非零即失败",而不是硬编码枚举已知的失败码。
顺带一提,源码注释还指出:退出码应保持永久性 / 瞬时性(可重试)/ 未知属性的一致分类,因为基础设施用户正是依据退出码来决定某个请求是否值得重试——例如退出码38(BES 上报瞬时错误)通常值得重试,而36(本地环境疑似永久问题)重试意义不大。
控制.bazelrc的读取:让脚本保持 hermetic
默认情况下,Bazel 会从工作区根目录或用户主目录读取.bazelrc文件。脚本是否希望读到这些配置,取决于你的场景:
- 需要完全 hermetic的构建(例如发布构建):用
--bazelrc=/dev/null禁用.bazelrc的读取,避免用户或工作区的本地配置悄悄改变构建行为; - 希望沿用用户偏好的设置:保持默认行为即可。
.bazelrc的完整语法、优先级与常见用法,可参考 docs/run/bazelrc.mdx。
命令日志:用bazel info command_log拿到完整输出
Bazel 的全部输出(stdout 与 stderr 交错)还会写入一份命令日志文件,脚本或排查人员可以用以下命令定位它:
bazel info command_log几个重要细节:
- 命令日志文件包含最近一次Bazel 命令的交错 stdout/stderr 流;
- 每次运行
bazel info本身都会覆盖该文件内容(因为它自己成了"最近一次命令"); - 日志文件的位置不会变化,除非你修改了
--output_base或--output_user_root。
从源码看,这份日志由 CommandLogModule.java 实现:它把日志写到输出基目录下的command.log文件(getCommandLogPath返回outputBase.getRelative("command.log")),CommandLogInfoItem则负责在bazel info时报告其位置。值得注意的是,日志写入受write_command_log选项控制,并且clean命令不会产生命令日志(见getOutputListener中!Objects.equals(env.getCommandName(), "clean")的判断)。此外,实现中还提到:当 instrumentation 输出写入本地时,会先删除上一次构建留下的旧command.log,避免日志混叠。
解析输出:--noshow_progress与--show_result
Bazel 的输出在很多场景下相当容易解析,两个选项值得脚本关注:
--noshow_progress:抑制进度消息,让 stdout 更干净;--show_result n:控制是否打印"build up-to-date"之类的摘要消息。这些消息可以被解析,用来发现哪些 target 构建成功、以及它们产出的输出文件位于哪里。
如果你依赖这些摘要消息来驱动脚本逻辑,务必把n设为一个非常大的值,否则目标数量超过阈值时消息会被截断,脚本解析就会漏掉目标。典型写法:
bazel build --noshow_progress --show_result=100000 //...性能剖析参考
如果你的脚本驱动的构建出现性能问题,需要系统性地定位瓶颈(例如本地执行耗时、远程执行排队、缓存命中率),可以参考 性能剖析指南 中关于性能剖析(Performance Profiling)的部分,利用 Bazel 自带的 profiling 工具生成时序数据再进行分析,而不是靠脚本盲猜。
小结:一套健壮脚本的最小模板
综合以上要点,一段兼顾锁隔离、进程清理与错误分类的脚本骨架可以这样组织:
# 1) 若需并行运行多个实例,给每个实例指定独立的 output_base OUTPUT_BASE="${TMPDIR:-/tmp}/bazel_out_${$}" # 2) 用 max_idle_secs 保证闲置 server 快速退出 bazel --output_base="${OUTPUT_BASE}" --max_idle_secs=5 \ build --noshow_progress --show_result=100000 //... status=$? case "$status" in 0) echo "success";; 1) echo "build failed (source problem)";; 3) echo "tests failed";; 4) echo "no tests found";; 8) echo "interrupted";; 9) echo "server lock held, no blocking allowed";; 33) echo "OOM, adjust command line";; 37) echo "internal bazel error";; *) echo "other error: $status";; esac # 3) 收尾时显式关闭 server(配合 max_idle_secs 双保险) bazel --output_base="${OUTPUT_BASE}" shutdown核心原则可以浓缩为四句话:用--output_base管理锁与并行;用shutdown/--max_idle_secs防止 server 堆积;把"非零退出码"当作失败,并对 32–49 这类基础设施错误按永久/瞬时分类决定是否重试;用bazel info command_log保留可追溯的完整日志。掌握了这四点,你的 Bazel 脚本就能稳定运行在本地、CI 与发布流水线中。
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考