使用宏创建自定义动词

报告问题 查看源代码

与 Bazel 的日常互动主要通过下面这些命令进行:buildtestrun。但有时,它们可能会让人感觉受到限制:您可能想要将软件包推送到代码库、发布最终用户文档或使用 Kubernetes 部署应用。但是 Bazel 没有 publishdeploy 命令 - 在哪里可以执行这些操作?

bazel 运行命令

Bazel 专注于封闭性、可再现性和增量,这意味着 buildtest 命令对于上述任务没有帮助。这些操作可以在沙盒中运行,网络访问受限,但不保证每个 bazel build 都会重新运行这些操作。

您应依赖于 bazel run:您希望产生附带效应的任务。Bazel 用户已习惯创建可执行文件的规则,而规则作者可以遵循一组常见的模式将其扩展到“自定义动词”。

实际应用:rules_k8s

以 Bazel 的 Kubernetes 规则 rules_k8s 为例。假设您有以下目标:

# BUILD file in //application/k8s
k8s_object(
    name = "staging",
    kind = "deployment",
    cluster = "testing",
    template = "deployment.yaml",
)

staging 目标上使用 bazel build 时,k8s_object 规则会构建标准 Kubernetes YAML 文件。不过,其他目标也是由具有 staging.apply:staging.delete 等名称的 k8s_object 宏创建的。用于执行这些操作的构建脚本,在使用 bazel run staging.apply 执行时,其行为类似于我们自己的 bazel k8s-applybazel 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,
)

最后,定义以下宏,以便同时为上述两条规则创建目标:

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 命令。