用脚本稳健地调用 Bazel:output_base、退出码与命令日志实战指南
2026/9/13 18:31:03 网站建设 项目流程

用脚本稳健地调用 Bazel:output_base、退出码与命令日志实战指南

【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel

在 CI 流水线、发布脚本或各类自动化工具中,常常需要把bazel buildbazel testbazel 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_baseoutput_user_root都在 BlazeServerStartupOptions.java 中定义(其中output_base的说明中提到它会定位到output_user_root之下的具体位置)。也就是说,输出基目录实际上是"用户根目录 + 工作区 + 用户"维度的细分,这解释了为什么它能作为 server 定位与锁隔离的依据。

服务端模式:别忘了shutdown或设置空闲超时

Bazel 默认采用一个常驻的 server 进程 来提升性能:它可以在多次构建之间缓存 BUILD 文件、依赖图和其他元数据,从而加速增量构建,并让buildquery等不同命令共享已加载的包缓存。

这个优化对脚本有两个直接后果:

  1. 每次bazel调用,客户端会按输出基目录查找(或启动)server,命令结束后 server 并不会立刻退出,而是继续常驻等待下一次调用;
  2. 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
9server 锁被占用,且传入了--noblock_for_lockLOCK_HELD_NOBLOCK_FOR_LOCK
32外部环境失败(非本机)REMOTE_ENVIRONMENTAL_ERROR
33Bazel 内存耗尽崩溃,需要修改命令行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
39Bazel 所需的 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询