0%

bazel 自定义规则

之前简单了解过 bazel ,前一阵给 Envoy CI 加 Go 代码风格的检查,又折腾了一番 bazel 的扩展规则,把当时的笔记扩展记录下来,以后没准还需要折腾。

背景

背景是这样的,Envoy CI 原来使用 python 脚本来做代码风格检查,但是那一套规则,对于 Go 并不匹配,所以想改用 gofmt 来检查 Go 代码。

其实最简单的方式是,在 Envoy CI 的 base docker image 中安装 golang 的包,但是他们觉得 golang 包太大了…

那么此时,就需要通过 bazel 来安装 golang,并且把 gofmt 暴露出来。

rules_go

按照 phlax 的提示,rules_go 已经引入了 gofmt,接下来就看,怎么用上了。

经过一通翻 rules_go 的代码,大概搞明白了 bazel 扩展规则的基本套路。

我的思路是,就是通过自定义规则,来产生 gofmt 文件,类似于编译 Envoy,然后,在检查脚本里就可以用上 gofmt 了。

最终效果是,在 tools/code_format/BUILD 里调用自定义规则:

1
2
3
gen_gofmt(
name = "gofmt",
)

通过 build 来生成 gofmt

1
bazel build //tools/code_format:gofmt

rule

自定义规则,分为两部分,首先是上层的定义:rule

rule 可以理解为函数申明,可以有入参,通常是编译的源文件,如下示例中没有;还有返回值,也就是产出物。

rule 本身,比较简单,只是申明了基本信息,包括 rule implementation,还有对入参的定义。

如下示例,是基于 rules_go 的自定义的 go rule 规则 ,有几个点:

  1. 申明了 gen_gofmt 这个 rule,实现在 _gen_gofmt_impl
  2. 依赖了 rules_go 中的 go toolchain,toolchain 会提供一个执行环境,比如通常的 rule_cc 是提供的是 C 编译器的执行环境
  3. 入参处理,是在 attrs 中完成,示例并没有入参,只是将 go_context_data 放进上下文中
1
2
3
4
5
6
7
8
9
gen_gofmt = rule(
implementation = _gen_gofmt_impl,
attrs = {
"_go_context_data": attr.label(
default = "@io_bazel_rules_go//:go_context_data",
),
},
toolchains = ["@io_bazel_rules_go//go:toolchain"],
)

rule implementation

另一部分就是,rule implementation,也就是用 skylark(类 python 的语言)函数来描述具体的生产过程。

如下示例,有几个点:

  1. go_contextrules_go 提供的函数,将 toolchain 中的内容暴露出来
  2. action 是 bazel 内置的,用来申明执行什么任务,可以多个叠加,这里就只是简单的执行 cp 命令
  3. 返回值也就是产出物
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def _gen_gofmt_impl(ctx):
name = ctx.label.name
out = ctx.actions.declare_file(name)

go = go_context(ctx)
sdk = go.sdk
gofmt = sdk.root_file.dirname + "/bin/gofmt"

ctx.actions.run(
executable = "cp",
arguments = [gofmt, out.path],
outputs = [out],
inputs = sdk.tools,
)

return [DefaultInfo(
files = depset([out]),
)]

体会

bazel 确实很灵活,可以支持很复杂的扩展,但是上手门槛也高。

如果,有一个现成的示例可以抄,那么使用门槛也不高,简单 copy & paste,了解一些上层的概念也够用了。

但是,如果简单模式走不通,那就比较麻烦了,得把 bazel 的底层逻辑搞懂了,这就是门槛了。

我觉得,最主要的还是,通识性不够强,不够普世,bazel 自己撸的一套方案,用得人少,门槛就搞了。

结局

最后,phlax 发现可以这样来执行 gofmtbazel run @go_sdk//:bin/gofmt,自定义规则就可以删掉了,汗 …

还是通识性不够强,用的人少,走的弯路就多了。

ChatGPT

其实,这种笔记也挺无聊的,让我们问问 ChatGPT 能做到什么程度呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
我:请写一篇如何实现 bazel 自定义规则的介绍文章,大约 200 字

AI:Bazel 自定义规则是使用 Skylark 语言编写的,它们通过 BUILD 文件被 Bazel 调用。如果您想实现 Bazel 自定义规则,您需要执行以下步骤:

创建一个 Skylark 脚本:首先,您需要创建一个 Skylark 脚本,该脚本将包含您的自定义规则的实现。

实现自定义规则:接下来,您需要实现自定义规则。在 Skylark 脚本中,您可以使用内置函数或编写自己的函数,以实现所需的构建逻辑。

在 BUILD 文件中使用自定义规则:最后,您需要在您的 BUILD 文件中使用自定义规则。您可以通过调用 Skylark 脚本中实现的自定义规则函数来实现这一点。

请注意,如果您希望 Bazel 可以找到和使用您的自定义规则,您需要遵循 Bazel 的标准文件结构和命名规范。在您的自定义规则生效前,请确保进行了充分的测试,以确保它们按预期工作。

我:请提供一个 bazel 自定义规则的最小化示例

AI:下面是一个使用 Skylark 语言实现 Bazel 自定义规则的最简单示例:

# custom_rule.bzl
def custom_rule_impl(ctx):
output = ctx.outputs.out
ctx.actions.run(
outputs=[output],
inputs=ctx.files.srcs,
executable="echo",
arguments=["Hello, Bazel!", ">", output.path]
)

custom_rule = rule(
implementation = custom_rule_impl,
attrs = {
"srcs": attr.label_list(allow_files=True),
"out": attr.output(),
},
)

以下是如何在 BUILD 文件中使用这个自定义规则的示例:
# BUILD
load("//path/to/custom_rule.bzl", "custom_rule")

custom_rule(
name = "hello",
srcs = ["input.txt"],
out = "output.txt",
)

在此示例中,我们实现了一个简单的 custom_rule,该规则通过调用 echo 命令将消息 "Hello, Bazel!" 写入输出文件。在 BUILD 文件中,我们加载了 custom_rule.bzl,并调用了 custom_rule 以使用该规则。

这两个问题,我给 90 分,不过后面还有更细节的问题,比如,如何使用 rules_go 中的 gofmt,就回答错了。

或许,ChatGPT 还需要进化,又或者需要更多我这样的无聊笔记投喂训练,才能更深的理解?