Bazel 规则编写实战指南:从空规则到模板化代码生成
2026/9/12 17:55:35 网站建设 项目流程

Bazel 规则编写实战指南:从空规则到模板化代码生成

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

本指南以 Bazel 官方 Rules Tutorial 为骨架,完整演示如何用 Starlark 语言从零编写一个自定义构建规则:从最简单的"空规则"出发,逐步理解加载期与分析期的评估模型,掌握ctx.actions.writectx.actions.expand_template等动作注册 API,以及attr模块的属性声明与依赖建模,最终落地为可复用的代码生成规则。读完本文,你将具备独立编写.bzl规则文件、在BUILD中实例化自定义规则并参与 Bazel 目标图构建的完整能力。

规则、Starlark 与构建语言

Bazel 的自定义规则使用 Starlark 编写——这是一种类 Python 的配置语言,最初为 Bazel 开发,现已被多种工具采用。Bazel 的BUILD文件和.bzl文件使用 Starlark 的一个方言编写,即"构建语言"(Build Language);当强调某功能是用构建语言表达、而非 Bazel 内置(native)特性时,通常直接称其为"Starlark"。Bazel 在核心语言之上扩展了大量构建相关函数,如globgenrulejava_binary等。

在 Bazel 中,一条**规则(rule)**定义了一组 Bazel 对输入执行以产生输出的动作(actions),这些输出通过规则实现函数返回的provider被引用。例如一条 C++ 二进制规则可能:读取一组.cpp源文件作为输入、对源文件执行g++动作、返回携带可执行文件与运行时文件的DefaultInfoprovider、并返回携带 C++ 特有信息的CcInfoprovider。

编写自定义规则的入口是调用rule()函数。在 Bazel 源码中,rule()函数的实现位于 StarlarkRuleClassFunctions.java,其核心参数包括implementation(实现函数)、attrs(属性字典)、executabletest等。值得注意的是,rule()只能在.bzl文件的初始化上下文中调用(源码中通过BzlInitThreadContext.fromOrFail(thread, "rule()")强制校验),这也解释了为什么规则定义必须放在.bzl文件中。

第一个规则:空规则

创建一个foo.bzl文件,定义你的第一条规则:

def _foo_binary_impl(ctx): pass foo_binary = rule( implementation = _foo_binary_impl, )

调用rule()时,必须提供一个回调函数作为implementation。规则的全部逻辑将写在这个函数里,但当前可以先让它空着。ctx参数(ctx对象在源码中对应 StarlarkRuleContext.java)提供了关于当前被分析目标的信息,例如目标的 label、属性值、声明的输出文件等。

接下来在同一目录创建BUILD文件,加载并使用该规则:

load(":foo.bzl", "foo_binary") foo_binary(name = "bin")

现在可以构建这个目标:

$ bazel build bin INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured). INFO: Found 1 target... Target //:bin up-to-date (nothing to build)

尽管这条规则什么都不做,它已经具备普通规则的行为:拥有必填的name属性,并自动支持visibilitytestonlytags等通用属性——这些通用属性由 Bazel 隐式添加到所有规则上(详见 docs/extending/rules.mdx 中关于 common attributes 的说明)。

评估模型:加载、分析与执行

在继续之前,必须理解 Bazel 的评估模型。Bazel 的构建分为三个阶段:加载阶段(评估BUILD.bzl文件、实例化目标)、分析阶段(执行规则的 implementation 函数、注册动作)、执行阶段(实际运行动作产出文件)。

print语句观察代码何时被求值。更新foo.bzl

def _foo_binary_impl(ctx): print("analyzing", ctx.label) foo_binary = rule( implementation = _foo_binary_impl, ) print("bzl file evaluation")

以及BUILD

load(":foo.bzl", "foo_binary") print("BUILD file") foo_binary(name = "bin1") foo_binary(name = "bin2")

ctx.label对应正在被分析的目标的 label(源码中该字段的取值逻辑见 StarlarkRuleContext.java)。ctx对象包含大量有用的字段与方法。先执行查询:

$ bazel query :all DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file //:bin2 //:bin1

观察两个关键现象:

  1. "bzl file evaluation" 先于 "BUILD file" 打印。在评估BUILD文件之前,Bazel 会先评估它所 load 的所有.bzl文件。如果多个BUILD文件都加载foo.bzl,你也只会看到一次 "bzl file evaluation",因为 Bazel 会缓存.bzl文件的评估结果。
  2. _foo_binary_impl没有被调用bazel query只加载BUILD文件,不会分析目标——分析阶段尚未开始,规则实现函数自然不会被调用。

要触发分析阶段,使用cquery(configured query,详见 docs/query/cquery.mdx)或build命令:

$ bazel build :all DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1 DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2 INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured). INFO: Found 2 targets...

可以看到,_foo_binary_impl现在被调用了两次——每个目标各一次。同时注意,"bzl file evaluation" 和 "BUILD file" 都没有再次打印,因为foo.bzl的评估结果在上一次bazel query时已被缓存。Bazel 只在实际执行到print语句时才输出其内容。

生成文件:declare_file 与 ctx.actions.write

让规则真正产生价值:生成一个文件。首先声明文件并命名。本例创建一个与目标同名的文件:

ctx.actions.declare_file(ctx.label.name)

如果此时运行bazel build :all,会得到错误:

The following files have no generating action: bin2

这是因为每当你声明一个文件,都必须通过创建动作(action)告诉 Bazel 如何生成它。使用ctx.actions.write创建指定内容的文件:

def _foo_binary_impl(ctx): out = ctx.actions.declare_file(ctx.label.name) ctx.actions.write( output = out, content = "Hello\n", )

这段代码合法,但构建时仍不会产生任何文件:

$ bazel build bin1 Target //:bin1 up-to-date (nothing to build)

ctx.actions.write只是注册了一个动作,教会 Bazel"如何"生成文件;但 Bazel 只有在文件被真正请求时才会执行该动作。因此最后一步是告诉 Bazel:该文件是规则的输出,而非规则实现内部的临时文件——通过DefaultInfoprovider 暴露它:

def _foo_binary_impl(ctx): out = ctx.actions.declare_file(ctx.label.name) ctx.actions.write( output = out, content = "Hello!\n", ) return [DefaultInfo(files = depset([out]))]

DefaultInfodepset的细节可以稍后再看,这里只需理解:最后一行是规则选择自身输出的标准方式。现在构建并查看产物:

$ bazel build bin1 INFO: Found 1 target... Target //:bin1 up-to-date: bazel-bin/bin1 $ cat bazel-bin/bin1 Hello!

文件成功生成!

源码视角ctx.actions.write的底层实现在 StarlarkActionFactory.java。可以看到,当contentString时,它创建一个FileWriteAction;当contentArgs对象时,则创建ParameterFileWriteAction(用于生成参数文件)。动作注册后由执行阶段真正落地为磁盘上的bazel-bin/bin1。这也印证了分析阶段"只注册、不执行"的设计:implementation 函数绝不直接运行外部命令。

为规则添加属性

使用attr模块 为规则添加新属性。添加一个名为username的字符串属性:

foo_binary = rule( implementation = _foo_binary_impl, attrs = { "username": attr.string(), }, )

BUILD文件中设置它:

foo_binary( name = "bin", username = "Alice", )

在回调函数中通过ctx.attr.username访问属性值。例如:

def _foo_binary_impl(ctx): out = ctx.actions.declare_file(ctx.label.name) ctx.actions.write( output = out, content = "Hello {}!\n".format(ctx.attr.username), ) return [DefaultInfo(files = depset([out]))]

attr.string支持设置属性为必填或提供默认值。在 Bazel 源码中,属性构建器的mandatory()方法(见 Attribute.java)用于将属性标记为必填,未设置必填属性会在分析阶段报错。除字符串外,还可以使用其他属性类型,例如布尔型attr.bool()、整数列表attr.int_list()等。属性类型决定了两件事:BUILD文件中允许传入什么值,以及实现函数中ctx.attr.<name>的取值类型。

依赖属性:构建目标图

依赖属性(dependency attribute),例如attr.labelattr.label_list,声明了"拥有该属性的目标"到"属性值中 label 所指目标"之间的依赖关系。这类属性是目标图(target graph)的基础

BUILD文件中,目标 label 以字符串形式出现,如//pkg:name;在实现函数中,该目标以Target对象的形式被访问。例如通过Target.files查看目标返回的文件。

多文件:allow_files 与 ctx.files

默认情况下,只有规则创建的目标(如某个foo_library()目标)才能作为依赖出现。如果希望属性接受作为输入文件的目标(如仓库中的源文件),需要使用allow_files并指定接受的文件扩展名列表(或传True允许任意扩展名):

"srcs": attr.label_list(allow_files = [".java"]),

文件列表可以通过ctx.files.<属性名>访问。例如srcs属性中的文件列表:

ctx.files.srcs

单文件:allow_single_file 与 ctx.file

如果只需要一个文件,使用allow_single_file

"src": attr.label(allow_single_file = [".java"])

该文件通过ctx.file.<属性名>访问:

ctx.file.src

源码视角ctx.filesctx.file在 StarlarkRuleContext.java 中分别由getFile()getFiles()提供。依赖解析的核心逻辑(见 StarlarkRuleContext.java 的makeLabelMap)会从每个依赖目标的FilesToRunProviderFileProvider中取出"待构建文件集合"(files to build),并将其展开为可供规则实现读取的文件列表——这也是ctx.files.srcs返回可迭代文件列表的底层来源。

基于模板生成文件:expand_template

可以创建一条基于模板生成.cc文件的规则。虽然ctx.actions.write也能输出在实现函数中拼接的字符串,但有两个问题:其一,模板越大,在分析阶段构造大字符串的内存开销越高,不如把模板放到独立文件中;其二,独立文件对用户更友好。因此改用ctx.actions.expand_template——它对模板文件执行字符串替换。

创建template属性以声明对模板文件的依赖:

def _hello_world_impl(ctx): out = ctx.actions.declare_file(ctx.label.name + ".cc") ctx.actions.expand_template( output = out, template = ctx.file.template, substitutions = {"{NAME}": ctx.attr.username}, ) return [DefaultInfo(files = depset([out]))] hello_world = rule( implementation = _hello_world_impl, attrs = { "username": attr.string(default = "unknown person"), "template": attr.label( allow_single_file = [".cc.tpl"], mandatory = True, ), }, )

这里演示了几个要点:

  • substitutions是"占位符 → 替换值"的映射:模板文件中所有{NAME}都会被替换为username属性的值;
  • username设置了默认值"unknown person",用户不传时使用默认;
  • template通过mandatory = True设为必填,且只接受.cc.tpl扩展名文件。

用户这样使用该规则:

hello_world( name = "hello", username = "Alice", template = "file.cc.tpl", ) cc_binary( name = "hello_bin", srcs = [":hello"], )

hello_world生成的hello.cccc_binary作为源文件消费,形成了一条完整的自定义规则 → native 规则的依赖链。

私有属性与隐式依赖

如果不想让最终用户指定模板、始终使用同一个模板文件,可以设置默认值并将属性设为私有

"_template": attr.label( allow_single_file = True, default = "file.cc.tpl", ),

下划线开头的属性是私有的,不能在BUILD文件中设置。此时模板成为一条隐式依赖(implicit dependency):每个hello_world目标都自动依赖这个文件。源码层面,属性私有化的判定见 Attribute.java 的isPrivateAttribute:以_开头(Starlark 可见名)的属性即被视为私有。私有属性在加载阶段不可由BUILD文件覆写,其默认值由规则声明时固定。

不要忘记更新BUILD文件,使用exports_files让该模板文件对其他包可见:

exports_files(["file.cc.tpl"])

否则,当hello_world规则位于其他包、而模板文件位于当前包时,Bazel 会因为文件不可见而拒绝依赖。

源码视角expand_template的实现在 StarlarkActionFactory.java。可以看到它做了三件事:将substitutions字典逐个构造为Substitution对象并去重合并、检查重复键、最后创建一个TemplateExpansionAction并注册到分析环境。这从源码层面验证了模板替换动作与write动作一样,都是分析阶段注册、执行阶段落盘的标准动作。

深入路径:从教程到生产级规则

掌握上述基础后,可以沿着以下仓库内资料继续深入:

  • 规则参考文档:系统讲解规则创建、属性、实现函数、provider、动作与执行阶段等完整模型,是规则开发的权威参考;
  • depsets 详解:理解DefaultInfo(files = depset([out]))depset的高效聚合语义——嵌套集合的去重与传递合并能力是大型规则集性能的关键;
  • 规则语言参考:Starlark 语法、.bzl文件约束与加载规则的完整说明;
  • 概念:标签://pkg:name标签语法与解析规则,是理解依赖属性的前提;
  • 规则部署:涉及exports_files、包可见性与规则对外发布的最佳实践;
  • 宏教程 与 旧式宏教程:区分宏与规则——宏在加载期展开为多个目标调用,规则实现则在分析期执行;
  • cquery 使用指南:用配置查询验证规则在不同配置下的分析行为。

仓库中的 examples 目录提供了可直接运行的示例:例如 examples/java-starlark 展示了用 Starlark 规则驱动 Java 构建的完整布局,examples/cpp 则包含hello-libhello-world的经典库依赖示例,可作为自定义规则与 native 规则协作的参照。此外,Bazel 自身的测试代码(如 StarlarkSubruleTest.java)中包含大量def _xxx_impl(ctx)DefaultInfo(files = ...)的真实用例,是学习规则编写风格的优秀素材。

至此,你已经走通了从"空规则"到"模板化代码生成"的完整路径:理解了 Starlark 规则在三阶段评估模型中的位置、掌握了动作注册与输出暴露的机制、能够声明带默认值和必填约束的属性、能够通过依赖属性接入目标图、并学会了用隐式依赖封装模板。剩下的,就是在实践中为你的语言或工具链打磨第一条生产级规则。

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

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询