与 Bazel 的日常互动主要通过以下几个命令进行:build
、test
和 run
。不过,有时这些功能可能会显得有限:您可能希望将软件包推送到代码库、发布面向最终用户的文档,或者使用 Kubernetes 部署应用。但 Bazel 没有 publish
或 deploy
命令,这些操作应该放在哪里?
bazel run 命令
Bazel 侧重于密封性、可重现性和增量性,这意味着 build
和 test
命令对上述任务没有帮助。这些操作可能会在沙盒中运行,网络访问权限有限,并且不保证每次运行 bazel build
时都会重新运行。
请改用 bazel run
:这是您希望产生副作用的任务的得力助手。Bazel 用户习惯于创建可执行文件的规则,规则作者可以遵循一组通用模式,将此功能扩展到“自定义动词”。
在实际应用中:rules_k8s
例如,考虑 rules_k8s
,即 Bazel 的 Kubernetes 规则。假设您有以下目标:
# BUILD file in //application/k8s
k8s_object(
name = "staging",
kind = "deployment",
cluster = "testing",
template = "deployment.yaml",
)
当在 staging
目标上使用 bazel build
时,k8s_object
规则会构建标准的 Kubernetes YAML 文件。不过,其他目标也是通过 k8s_object
宏创建的,名称类似于 staging.apply
和 :staging.delete
。这些 build 脚本用于执行这些操作,并且在通过 bazel run
staging.apply
执行时,这些脚本的行为与我们自己的 bazel k8s-apply
或 bazel
k8s-delete
命令类似。
另一个示例:ts_api_guardian_test
您还可以在 Angular 项目中看到此模式。ts_api_guardian_test
宏会生成两个目标。第一种是标准 nodejs_test
目标,用于将生成的输出与“黄金”文件(即包含预期输出的文件)进行比较。可以使用常规 bazel
test
调用来构建和运行此测试。在 angular-cli
中,您可以使用 bazel test //etc/api:angular_devkit_core_api
运行其中一个目标。
随着时间的推移,出于正当理由,可能需要更新此黄金文件。
手动更新此文件既繁琐又容易出错,因此此宏还提供了一个 nodejs_binary
目标,用于更新标准文件,而不是与标准文件进行比较。实际上,可以编写相同的测试脚本,使其根据调用方式在“验证”或“接受”模式下运行。这与您已经学过的模式相同:没有原生 bazel test-accept
命令,但可以使用 bazel run //etc/api:angular_devkit_core_api.accept
实现相同的效果。
这种模式非常强大,一旦学会识别它,您会发现它非常常见。
调整您自己的规则
宏是此模式的核心。宏的使用方式与规则类似,但它们可以创建多个目标。通常,它们会创建一个具有指定名称的目标,该目标会执行主要构建操作:可能是构建普通二进制文件、Docker 映像或源代码归档。在此模式中,系统会创建额外的目标,以生成基于主要目标输出执行副作用的脚本,例如发布生成的二进制文件或更新预期测试输出。
为了说明这一点,请使用宏封装一个使用 Sphinx 生成网站的假想规则,以创建一个额外的目标,让用户在准备就绪后发布该网站。不妨考虑以下用于通过 Sphinx 生成网站的现有规则:
_sphinx_site = rule(
implementation = _sphinx_impl,
attrs = {"srcs": attr.label_list(allow_files = [".rst"])},
)
接下来,考虑如下规则,该规则会构建一个脚本,该脚本在运行时会发布生成的网页:
_sphinx_publisher = rule(
implementation = _publish_impl,
attrs = {
"site": attr.label(),
"_publisher": attr.label(
default = "//internal/sphinx:publisher",
executable = True,
),
},
executable = True,
)
最后,定义以下符号宏(可在 Bazel 8 或更高版本中使用),以同时为上述两条规则创建目标:
def _sphinx_site_impl(name, visibility, srcs, **kwargs):
# This creates the primary target, producing the Sphinx-generated HTML. We
# set `visibility = visibility` to make it visible to callers of the
# macro.
_sphinx_site(name = name, visibility = visibility, srcs = srcs, **kwargs)
# This creates the secondary target, which produces a script for publishing
# the site generated above. We don't want it to be visible to callers of
# our macro, so we omit visibility for it.
_sphinx_publisher(name = "%s.publish" % name, site = name, **kwargs)
sphinx_site = macro(
implementation = _sphinx_site_impl,
attrs = {"srcs": attr.label_list(allow_files = [".rst"])},
# Inherit common attributes like tags and testonly
inherit_attrs = "common",
)
或者,如果您需要支持低于 Bazel 8 的 Bazel 版本,则应改为定义旧版宏:
def sphinx_site(name, srcs = [], **kwargs):
# This creates the primary target, producing the Sphinx-generated HTML.
_sphinx_site(name = name, srcs = srcs, **kwargs)
# This creates the secondary target, which produces a script for publishing
# the site generated above.
_sphinx_publisher(name = "%s.publish" % name, site = name, **kwargs)
在 BUILD
文件中,使用宏时就好像它只创建主目标一样:
sphinx_site(
name = "docs",
srcs = ["index.md", "providers.md"],
)
在此示例中,创建了一个“docs”目标,就像该宏是一个标准的单个 Bazel 规则一样。构建时,该规则会生成一些配置并运行 Sphinx 以生成 HTML 网站,以便进行人工检查。不过,系统还会创建一个额外的“docs.publish”目标,用于构建发布网站的脚本。检查主目标的输出后,您可以使用 bazel run :docs.publish
将其发布以供公开使用,就像一个虚构的 bazel publish
命令一样。
_sphinx_publisher
规则的实现方式可能并不显而易见。通常,此类操作会编写 启动器 shell 脚本。此方法通常涉及使用 ctx.actions.expand_template
编写一个非常简单的 shell 脚本,在本例中,该脚本会使用主要目标的输出路径调用发布者二进制文件。这样,发布商实现可以保持通用,_sphinx_site
规则只需生成 HTML,而这个小脚本是唯一需要将两者结合在一起的工具。
在 rules_k8s
中,.apply
确实会执行以下操作:expand_template
根据 apply.sh.tpl
编写一个非常简单的 Bash 脚本,该脚本会运行 kubectl
并输出主要目标的结果。然后,可以使用 bazel run :staging.apply
构建并运行此脚本,从而为 k8s_object
目标有效提供 k8s-apply
命令。