尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

Bazel 规则编写实战指南:从空规则到模板化代码生成 Bazel 规则编写实战指南从空规则到模板化代码生成【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel本指南以 Bazel 官方 Rules Tutorial 为骨架完整演示如何用 Starlark 语言从零编写一个自定义构建规则从最简单的空规则出发逐步理解加载期与分析期的评估模型掌握ctx.actions.write、ctx.actions.expand_template等动作注册 API以及attr模块的属性声明与依赖建模最终落地为可复用的代码生成规则。读完本文你将具备独立编写.bzl规则文件、在BUILD中实例化自定义规则并参与 Bazel 目标图构建的完整能力。规则、Starlark 与构建语言Bazel 的自定义规则使用 Starlark 编写——这是一种类 Python 的配置语言最初为 Bazel 开发现已被多种工具采用。Bazel 的BUILD文件和.bzl文件使用 Starlark 的一个方言编写即构建语言Build Language当强调某功能是用构建语言表达、而非 Bazel 内置native特性时通常直接称其为Starlark。Bazel 在核心语言之上扩展了大量构建相关函数如glob、genrule、java_binary等。在 Bazel 中一条**规则rule**定义了一组 Bazel 对输入执行以产生输出的动作actions这些输出通过规则实现函数返回的provider被引用。例如一条 C 二进制规则可能读取一组.cpp源文件作为输入、对源文件执行g动作、返回携带可执行文件与运行时文件的DefaultInfoprovider、并返回携带 C 特有信息的CcInfoprovider。编写自定义规则的入口是调用rule()函数。在 Bazel 源码中rule()函数的实现位于 StarlarkRuleClassFunctions.java其核心参数包括implementation实现函数、attrs属性字典、executable与test等。值得注意的是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属性并自动支持visibility、testonly、tags等通用属性——这些通用属性由 Bazel 隐式添加到所有规则上详见 docs/extending/rules.mdx 中关于 common attributes 的说明。评估模型加载、分析与执行在继续之前必须理解 Bazel 的评估模型。Bazel 的构建分为三个阶段加载阶段评估BUILD与.bzl文件、实例化目标、分析阶段执行规则的 implementation 函数、注册动作、执行阶段实际运行动作产出文件。用print语句观察代码何时被求值。更新foo.bzldef _foo_binary_impl(ctx): print(analyzing, ctx.label) foo_binary rule( implementation _foo_binary_impl, ) print(bzl file evaluation)以及BUILDload(: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观察两个关键现象bzl file evaluation 先于 BUILD file 打印。在评估BUILD文件之前Bazel 会先评估它所 load 的所有.bzl文件。如果多个BUILD文件都加载foo.bzl你也只会看到一次 bzl file evaluation因为 Bazel 会缓存.bzl文件的评估结果。_foo_binary_impl没有被调用。bazel query只加载BUILD文件不会分析目标——分析阶段尚未开始规则实现函数自然不会被调用。要触发分析阶段使用cqueryconfigured 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]))]DefaultInfo与depset的细节可以稍后再看这里只需理解最后一行是规则选择自身输出的标准方式。现在构建并查看产物$ 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。可以看到当content是String时它创建一个FileWriteAction当content是Args对象时则创建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.label与attr.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_filesrc: attr.label(allow_single_file [.java])该文件通过ctx.file.属性名访问ctx.file.src源码视角ctx.files与ctx.file在 StarlarkRuleContext.java 中分别由getFile()与getFiles()提供。依赖解析的核心逻辑见 StarlarkRuleContext.java 的makeLabelMap会从每个依赖目标的FilesToRunProvider与FileProvider中取出待构建文件集合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.cc被cc_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-lib与hello-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),仅供参考
返回列表