我们之前的文章里经常使用常规规则(regular rules)函数 rule()
来创建自定义规则,但是这些规则都有一个问题:他们依赖于主机系统上安装的各种工具。这样就会出现一个问题,即构建是不可复制的,如果同一项目上的两个开发人员安装了不同版本的 Go SDK,则他们将构建不同的二进制文件。它还会中断远程执行,即主机的工具链可能在执行平台上不可用。而 repository_rule()
就可以解决这个问题。
首先整体比较下 repository_rule()
和 rule
的区别:
repository_rule | rule |
---|---|
仅可在 WORKSPACE 中使用 | 只能在 BUILD 中使用 |
在构建的最开始(获取阶段)运行 | 在分析阶段 |
会新建一个工作区(WORKSPACE) | 在本 WORKSPACE 中 |
注意:
fetch
(获取), load
(加载), analysis
(分析) 和 execute
(执行) 四个阶段。从构建阶段来看,rule()
规则可以依赖 repository_rule()
生成的 BUILD
文件中的目标或者 bzl
文件等。$(bazel info output_base)/external/{工作区名称}
可以看到新建的工作区。repository rules
中使用第三方规则库,则需要在 WORKSPACE
调用自定义规则前加载第三方规则库。repository_ctx
APIs 提供的规则可直接访问主机系统而无需沙箱,因此为了构建在不同环境下的可复制性,需要注意不要引入系统相关的信息,比如时间戳或者特定目录名或者环境变量等。因此从构建的阶段来看,repository_rule
可以做的事情很多,比如包括:
BUILD
文件Bazle 内置工具中 repository rules
相关规则分为两类:
git
相关的规则:@bazel_tools//tools/build_defs/repo:git.bzl
git_repository
:克隆一个外部 git 仓库new_git_repository
:克隆一个外部 git 仓库http
相关的规则:@bazel_tools//tools/build_defs/repo:http.bzl
http_archive
:将 Bazel 相关的压缩的存档文件远程仓库下载下来,对其进行解压缩,然后可以使用其中相关规则http_file
:从 URL 下载文件,并使其可用作文件组(file group)http_jar
:从 URL 下载一个 .jar
扩展名包,并以 java_import
的形式提供和内置的 repository rules
一样,可以使用 `repository_rule`[1] 函数自定义 repository rules
。
创建通用规则时,我们得到的 ctx
对象作为实现函数的参数。同样,创建 reposiroty
规则时,将得到一个 repository_ctx
对象作为实现函数的参数。
repository_ctx.attr
:可以获取用户在规则中定义的相关属性的属性值bool repository_ctx.delete(path)
:删除一个文件或者目录repository_ctx.download
:下载并可以通过 sha256 校验一个 url 文件到输出目录(output path)repository_ctx.download_and_extract
:同上,但包含了解压功能,支持 "zip", "jar", "war", "tar.gz", "tgz", "tar.bz2", 或 "tar.xz" 包类型。exec_result repository_ctx.execute(arguments, ...
:执行参数列表给出的命令,执行命令具有超时限制,默认为600秒。repository_ctx.extract
:解压压缩包到指定目录repository_ctx.file
:创建一个可指定可执行属性的文件,并可写入内容string repository_ctx.read(path)
:读取一个文件内容repository_ctx.symlink(from, to)
:创建符号链接repository_ctx.template
:使用模板生成一个文件,没有代入值的话,则功能等同于拷贝文件,类似的还有 ctx.actions.expand_template
函数path repository_ctx.which(program)
:返回一个程序的路径更多参见:https://docs.bazel.build/versions/master/skylark/lib/repository_ctx.html
conf/test.tpl
:
NAME = "%{name}"
PASSWORD = "%{passwd}"
bazels/my_test_repo.bzl
:
def _test_repo_impl(repository_ctx):
# 读取文件内容
content = repository_ctx.read(repository_ctx.path(repository_ctx.attr.conf))
# 根据模板文件创建文件
repository_ctx.template(
"hello.bzl",
repository_ctx.attr._test_tpl_path,
substitutions = {
"%{name}": "biedamingming",
"%{passwd}" : "null",
},
executable = False, # not executable
)
# 创建一个空的 BUILD 文件
# Tip: 如果需要引用新工作空间内 bzl 文件,需要创建 BUILD 文件,即创建包
repository_ctx.file("BUILD.bazel", "")
my_test_repo = repository_rule(
implementation = _test_repo_impl,
attrs = {
"conf" : attr.label(allow_single_file = True),
"_test_tpl_path": attr.label(default = "//:conf/test.tpl"),
},
)
然后到 WORKSPACE 中加载规则并创建规则实例。新工作区名称即规则实例名称。
load("//bazels:my_test_repo.bzl", "my_test_repo")
my_test_repo(
name = "my_test_repo",
conf = "@autosdk//:conf/example.txt",
)
调试:bazel query @{工作区名称}//:*
我们可以将 Bazel 配置为使用本地工具链,但是为了实现构建环境的可复制性,我们可以将工具链统一远端管理,当然不只是工具链可以,我们的依赖也可以。使用 repository_rule
实现工具链的下载,可以整个依赖环境统一到沙箱中,从而保证了可复制性。
这里我们用 `rules_go_simple`[2] 来举例,首先声明定义一个规则:
go_download = repository_rule(
implementation = _go_download_impl,
attrs = {
"urls": attr.string_list(
mandatory = True,
doc = "List of mirror URLs where a Go distribution archive can be downloaded",
),
"sha256": attr.string(
mandatory = True,
doc = "Expected SHA-256 sum of the downloaded archive",
),
"goos": attr.string(
mandatory = True,
values = ["darwin", "linux", "windows"],
doc = "Host operating system for the Go distribution",
),
"goarch": attr.string(
mandatory = True,
values = ["amd64"],
doc = "Host architecture for the Go distribution",
),
"_build_tpl": attr.label(
default = "@rules_go_simple//internal:BUILD.dist.bazel.tpl",
),
},
doc = "Downloads a standard Go distribution and installs a build file",
)
urls
和 sha256
用于下载归档文件。提供 SHA-256
校验和,以确保下载文件不会被破坏或篡改。这也使得下载缓存能够跨本地工作区进行。os
和 arch
用于生成 BUILD
文件时使用_build_tpl
是用于生成构建文件的模板的标签。这是一个隐藏属性(它的名字以_开头),这意味着它必须有一个默认值。具体参见 rules_go_simple[3] 。_go_download_impl
的实现如下:
def _go_download_impl(ctx):
# 开始下载 Go 发行版
ctx.report_progress("downloading")
ctx.download_and_extract(
ctx.attr.urls,
sha256 = ctx.attr.sha256,
stripPrefix = "go",
)
# Add a build file to the repository root directory.
# We need to fill in some template parameters, based on the platform.
ctx.report_progress("generating build file")
if ctx.attr.goos == "darwin":
os_constraint = "@platforms//os:osx"
elif ctx.attr.goos == "linux":
os_constraint = "@platforms//os:linux"
elif ctx.attr.goos == "windows":
os_constraint = "@platforms//os:windows"
else:
fail("unsupported goos: " + ctx.attr.goos)
if ctx.attr.goarch == "amd64":
arch_constraint = "@platforms//cpu:x86_64"
else:
fail("unsupported arch: " + ctx.attr.goarch)
constraints = [os_constraint, arch_constraint]
constraint_str = ",\n ".join(['"%s"' % c for c in constraints])
substitutions = {
"{goos}": ctx.attr.goos,
"{goarch}": ctx.attr.goarch,
"{exe}": ".exe" if ctx.attr.goos == "windows" else "",
"{exec_constraints}": constraint_str,
"{target_constraints}": constraint_str,
}
ctx.template(
"BUILD.bazel",
ctx.attr._build_tpl,
substitutions = substitutions,
)
这里的 ctx
实为 repository_ctx
上下文。通过 repository_ctx.report_progress(status)
可以更新正下载包的进度状态。repository_ctx.download_and_extract()
,下载一个文件,并校验其文件,解压到其工作空间的指定文件夹中。最后 ctx.template()
同上一节所述,实现将参数传入模板文件,然后复制生成 BUILD.bazel
文件。
这里简单实现了文件下载、校验和解压,进一步的我们还可以实现对私有服务器进行身份验证或者通过自定义协议进行通信,当然这个实现就更复杂了。
下载并解压工具链后,如果去使用这些工具链呢?其实就相当于我们要实现一套语言相关的规则了,比如 go_binary
,怎么去实现下载指定 Go 发行版 SDK,并编译出该 SDK 对应的可执行文件呢?我们则需要去定义工具链以及定义工具链的动作,比如编译动作(Action)。最后实现 go_binary
,将输入(源文件)传入规则,并调用具体的动作实现最后的可执行文件生成。
repository rules
作用强大是显而易见的,在构建 fetch
(获取), load
(加载), analysis
(分析) 和 execute
(执行) 四个阶段中作为第一阶段(fetch),能够做到文件下载并校验、动态生成文件、可以提前执行命令等,可操作的空间很大。利用好它,我们可以实现一个可复制的构建。
[1]
repository_rule
: https://docs.bazel.build/versions/master/skylark/lib/globals.html#repository_rule
[2]
rules_go_simple
: https://github.com/jayconrod/rules_go_simple
[3]
rules_go_simple: https://github.com/jayconrod/rules_go_simple/blob/v5/internal/BUILD.dist.bazel.tpl