Bazel 规则开发指南:全面解读 Starlark 语言(语法、可变性语义与 Python 差异)
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
Starlark 是 Bazel 用于编写构建逻辑的领域专用语言(DSL),所有BUILD文件、.bzl扩展文件与规则、宏、切面(Aspect)的定义都使用它编写。本文以 Bazel 官方文档 docs/rules/language.mdx 为主体,结合仓库内的真实源码与测试,系统讲解 Starlark 的语法、实验性类型注解、上下文隔离的可变性模型、BUILD与.bzl文件的差异,以及与 Python 的全部关键区别。读完本文,你将能够读懂并编写符合 Bazel 语义约束的.bzl扩展与BUILD文件,避开常见的运行时错误,并为深入阅读 规则编写教程、宏教程 打下语言基础。
Starlark 是什么:Bazel 的构建描述语言
Starlark(旧称 Skylark)是一种专为构建系统设计的、受 Python 启发的声明式语言,其语言层面的权威定义独立维护在 Starlark 项目仓库(含完整的 Starlark Language Specification)。在 Bazel 仓库中,Starlark 的解释器、内置类型与 Bazel 扩展 API 的实现集中在 src/main/starlark 与 src/main/java/com/google/devtools/build/lib/starlark 目录下。
在 Bazel 中,Starlark 承担两类职责:
- 编写
BUILD文件:通过调用规则(rule)注册构建目标; - 编写
.bzl扩展文件:为常量、规则、宏和函数提供定义,供BUILD文件或其他.bzl文件load。
由于 Bazel 需要在不依赖宿主系统全局状态的前提下进行大规模并行构建,Starlark 刻意牺牲了一部分 Python 的表达自由,换来了确定性、可缓存性与可并行性。Bazel 内置函数与类型的完整清单见 Bazel API 参考;语法与行为的权威定义则以 Starlark Language Specification 为准。
语法:一门受 Python3 启发的语言
Starlark 的语法深受 Python3 启发,下面的例子是完全合法的 Starlark 代码:
def fizz_buzz(n): """Print Fizz Buzz numbers from 1 to n.""" for i in range(1, n + 1): s = "" if i % 3 == 0: s += "Fizz" if i % 5 == 0: s += "Buzz" print(s if s else i) fizz_buzz(20)可以看到:缩进块、def函数定义、文档字符串、for/if、三元表达式、print等语法与 Python 高度一致。但 Starlark 的语义与 Python 存在差异,虽然这类行为差异很少见,凡是出现差异的地方,Starlark 通常会直接抛出错误,而不是静默地给出不同结果。
Starlark 支持以下 Python 类型(对应的 API 参考页面同样存在于本仓库的版本化文档中):
| 类型 | 说明 | 对应 API 参考 |
|---|---|---|
None | 空值 | globals 全局函数 |
bool | 布尔值True/False | bool |
dict | 可变字典,迭代有序 | dict |
tuple | 不可变元组 | tuple |
function | 函数对象 | function |
int | 32 位有符号整数 | int |
list | 可变列表 | list |
string | 不可变字符串,不可迭代 | string |
值得注意的一点是:与 Python 不同,Starlark 的int类型被限制为32 位有符号整数,溢出会直接抛出错误;string也不可迭代。这两点都会在后面的“与 Python 的差异”小节中进一步说明。
实验性类型注解:向静态类型迈进
实验特性声明:类型注解(Type annotations)目前是实验性功能,随时可能变更,不应在生产代码中依赖它。启用方式取决于你所使用的 Bazel 版本,见下文说明。
Bazel 正在为 Starlark 增量引入类型注解支持,其语法受到 PEP 484(Python 类型注解规范)的启发。相关进展在 Bazel issue #27370 中持续跟踪,规范文本在starlark-with-types分支的 spec 文档中增量扩展,最初的设计提案为 "SEP-001 Bootstrapping Starlark types"。
通过命令行标志启用类型注解
在本仓库源码 src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java 中,可以完整看到类型注解相关选项的真实定义,它们是理解该功能当前状态的第一手源码证据:
--experimental_starlark_type_syntax:在.bzl文件中启用类型注解及相关语法。允许使用的文件位置还受--experimental_starlark_types_allowed_paths进一步限制;该标志的 help 文本还明确说明:无论此标志如何,.scl文件中都绝不允许使用类型语法。--experimental_starlark_static_type_checking:对包含类型注解或相关语法的文件与函数启用静态类型检查。--experimental_starlark_dynamic_type_checking:对包含类型注解或相关语法的函数启用动态类型检查(检查实参与返回值)。--experimental_starlark_type_checking:一个扩展标志(expansion flag),等价于同时开启上面两个检查标志。源码注释同时提示:当两个检查标志都关闭时,Bazel 对注解中无效类型的容忍度更高。--experimental_starlark_types_allowed_paths:允许使用类型注解的规范 Label 前缀列表。--experimental_starlark_types:已废弃的 no-op 标志。源码 help 文本写明它“此前是--experimental_starlark_type_syntax与--experimental_starlark_type_checking的组合”,现在不再产生任何效果。
也就是说,文档中提到的“通过--experimental_starlark_types标志启用”,在当前仓库代码里对应的是更细粒度的一组新标志;读者在使用时应以实际 Bazel 版本的bazel help输出为准。
测试如何验证类型注解行为
仓库中的测试 src/test/java/com/google/devtools/build/lib/starlark/StarlarkTypesTest.java 直观地演示了开关语义:
- 开启
--experimental_starlark_type_syntax并配置--experimental_starlark_types_allowed_paths=//test后,一个包含def f(a: int): pass的foo.bzl能被正常加载,断言assertNoEvents()表明加载阶段无任何错误; - 反之,当使用
--noexperimental_starlark_type_syntax关闭语法开关时,同样的文件会在加载阶段报错syntax error at ':': type annotations are disallowed。
这个对照测试清楚地说明:类型注解的开关是硬性的语法级约束,而不是可被忽略的软提示。
可变性:上下文隔离与冻结机制
Starlark 偏好不可变性(immutability)。语言层面只提供两种可变数据结构:list和dict。对可变数据结构的修改——例如向列表追加一个值、从字典中删除一个条目——只对当前上下文(context)中创建的对象有效。当一个上下文结束后,其值就变成不可变的(frozen)。
为什么要有上下文隔离
因为 Bazel 的构建使用并行执行。在一次构建过程中:
- 每个
.bzl文件都有自己独立的执行上下文; - 每个
BUILD文件也有自己独立的执行上下文; - 每条规则也在自己独立的上下文中进行分析。
这种“一文件一上下文”的模型保证了并行加载与求值的确定性:没有任何一个文件能通过修改共享全局状态而干扰其他文件的求值结果,这正是 Bazel 可以安全地大规模并行解析构建文件的基础。
冻结示例:从 foo.bzl 到 bar.bzl
考虑文件foo.bzl:
# `foo.bzl` var = [] # declare a list def fct(): # declare a function var.append(5) # append a value to the list fct() # execute the fct functionBazel 在加载foo.bzl时创建var,因此var属于foo.bzl的上下文。fct()的执行同样发生在foo.bzl的上下文中。当foo.bzl的求值全部完成后,环境中的var是一个不可变的条目,其值为[5]。
现在,另一个文件bar.bzl试图从foo.bzl加载符号:
# `bar.bzl` load(":foo.bzl", "var", "fct") # loads `var`, and `fct` from `./foo.bzl` var.append(6) # runtime error, the list stored in var is frozen fct() # runtime error, fct() attempts to modify a frozen list从foo.bzl加载得到的值保持不可变,因此:
var.append(6)抛出运行时错误——var中的列表已被冻结;fct()同样抛出运行时错误——它试图修改一个已冻结的列表。
这一机制可以推广为一条重要原则:在bzl文件中定义的全局变量,不能被定义它的bzl文件之外的任何代码修改。同样的规则也适用于规则返回值:规则(rule)返回的值同样是不可变的。因此,在编写规则与宏时,如果需要“返回一个可被调用方继续构造的数据结构”,应返回新对象而不是尝试修改传入或全局的对象。
BUILD 文件与 .bzl 文件的差异
BUILD文件与.bzl文件承担不同职责,语法上也有约束差异。
职责分工
BUILD文件:通过调用规则来注册目标(target);.bzl文件:为常量、规则、宏和函数提供定义。
原生符号的可见性
原生函数(native functions,如glob、exports_files)和原生规则(native rules,如cc_library、java_binary)在BUILD文件中是全局符号,可直接使用;而在.bzl文件中,需要通过native模块 显式加载。原生函数与原生规则的完整清单分别见 reference/be/functions.mdx 与 reference/be/overview.mdx。
BUILD 文件的两条语法限制
BUILD文件存在两条硬性语法限制:
- 禁止声明函数——
BUILD文件内不能出现def; - 不允许
*args和**kwargs参数。
这意味着BUILD文件只能“使用”规则与宏,而不能定义新的抽象;任何需要逻辑封装的内容都应下沉到.bzl文件。这种刻意设计的差异保证了BUILD文件保持扁平、声明式、易于静态分析。
与 Python 的差异
Starlark 与 Python 的行为差异绝大多数会以错误形式暴露。以下是文档列出的全部差异点,按主题归纳:
作用域与语句结构
- 全局变量不可变:如上文所述,模块级绑定一旦求值完成即被冻结;
for语句不允许出现在顶层:请把它们放进函数内使用;不过,在BUILD文件中可以使用列表推导式(list comprehension);if语句不允许出现在顶层:但可以使用if表达式,例如first = data[0] if len(data) > 0 else None。
数据结构的确定性保证
- 字典迭代顺序确定:迭代字典(dict)时顺序是确定性的,这为构建结果的复现性提供了保证;
- 不允许递归:函数不能直接或间接调用自身;
int限制为 32 位有符号整数:溢出会抛出错误;- 迭代过程中修改集合是错误:遍历一个集合的同时增删其元素会报错。
比较与类型语义
- 除相等性测试外,跨值类型的比较运算未定义:
<、<=、>=、>等运算符不能用于不同类型之间的比较。简言之,5 < 'foo'会抛出错误,而5 == "5"返回False(不会做隐式类型转换); - 字符串不可迭代:无法对字符串做
for c in "abc"之类的操作。
语法细节
- 元组的尾随逗号只在括号内合法:必须写成
(1,)而不是1,; - 字典字面量不允许重复键:例如
{"a": 4, "b": 7, "a": 1}是错误; - 字符串以双引号表示:例如调用
repr时输出形如"foo"而不是 Python 的'foo'。
不支持的 Python 特性
以下 Python 特性在 Starlark 中不被支持,编写代码时请改用括号中的替代方式:
| 不支持的 Python 特性 | Starlark 中的替代方式 |
|---|---|
| 隐式字符串拼接 | 显式使用+运算符 |
链式比较(如1 < x < 5) | 拆分为独立比较并用and连接 |
class | 使用struct函数构造结构化数据 |
import | 使用load语句加载扩展 |
while、yield | 使用for与列表推导式 |
| 生成器与生成器表达式 | 使用列表推导式 |
is | 使用==代替 |
try、raise、except、finally | 使用fail抛出致命错误 |
global、nonlocal | 全局绑定本就不可变,无需这两个关键字 |
| 大部分内置函数与大部分方法 | 使用 Bazel 提供的受限内置函数集 |
其中fail是 Starlark 报告致命错误的唯一途径,它位于 Bazel 的全局函数集中(完整全局函数清单见 globals/all.mdx)。
从语言到实战:进一步阅读
掌握语言本身只是第一步。本仓库的docs/rules目录围绕规则开发提供了完整的进阶路线:
- bzl-style.mdx:
.bzl文件的风格指南,涉及命名、注释、公共接口设计等约定; - macro-tutorial.mdx:从零编写宏(macro)的实战教程,直接运用本文的
load、不可变性等概念; - rules-tutorial.mdx:编写自定义规则的入门教程;
- docs/rules/index.mdx:规则开发文档的索引页,可以按需导航;
- docs/extending/concepts.mdx:扩展机制(扩展、
load语句、仓库规则等)的概念总览。
如果需要快速核对某个类型或函数的具体 API,Bazel API 参考的入口见 docs/versions/9.1.0/rules/lib/overview.mdx;而语言语义本身的权威定义,则以 Starlark 项目维护的 Language Specification 为准。
总结
Starlark 是一门刻意“收紧”的 Python 方言:它保留了 Python 的易读语法,却通过上下文隔离与冻结保证了并行构建的确定性,通过显式报错替代了 Python 中大量隐式行为,并移除了类、异常、生成器、递归等不利于静态分析与缓存的语言特性。理解这些设计取舍,是写出既正确又高效的 Bazel 构建逻辑的前提——尤其是“全局变量不可变”“规则返回值不可变”“迭代时禁止修改集合”这三条,是.bzl开发中最容易踩中的运行时错误来源。
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考