使用宏创建自定义动词

报告问题 查看源代码 每夜 build · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

与 Bazel 的日常互动主要通过以下几个命令完成:buildtestrun。不过,有时这些功能会显得有些局限:您可能需要将软件包推送到代码库、为最终用户发布文档,或使用 Kubernetes 部署应用。但是,Bazel 没有 publishdeploy 命令,这些操作该放在哪里?

bazel run 命令

Bazel 专注于密封性、可重现性和增量性,这意味着 buildtest 命令对上述任务没有帮助。这些操作可能会在沙盒中运行,并且网络访问权限受限,并且无法保证会在每次 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-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,
)

最后,定义以下符号宏(在 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 命令。