规则教程

报告问题 查看源代码 每夜版 · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Starlark 是一种类似于 Python 的配置语言,最初是为在 Bazel 中使用而开发的,后来被其他工具采用。Bazel 的 BUILD.bzl 文件采用 Starlark 的一种方言编写,这种方言正式称为“Build Language”,不过通常简称为“Starlark”,尤其是在强调某项功能是通过 Build Language 实现的,而不是 Bazel 的内置或“原生”部分时。Bazel 通过许多与 build 相关的函数(例如 globgenrulejava_binary 等)来增强核心语言。

如需了解详情,请参阅 BazelStarlark 文档,并使用 Rules SIG 模板作为新规则集的起点。

空规则

如需创建第一条规则,请创建文件 foo.bzl

def _foo_binary_impl(ctx):
    pass

foo_binary = rule(
    implementation = _foo_binary_impl,
)

调用 rule 函数时,您必须定义一个回调函数。逻辑将放在此处,但您现在可以留空该函数。ctx 实参提供有关目标的信息。

您可以从 BUILD 文件加载规则并使用它。

在同一目录中创建 BUILD 文件:

load(":foo.bzl", "foo_binary")

foo_binary(name = "bin")

现在,可以构建目标:

$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)

尽管该规则不执行任何操作,但其行为与其他规则类似:它具有强制性名称,并支持 visibilitytestonlytags 等常见属性。

评估模型

在继续之前,请务必了解代码的评估方式。

使用一些 print 语句更新 foo.bzl

def _foo_binary_impl(ctx):
    print("analyzing", ctx.label)

foo_binary = rule(
    implementation = _foo_binary_impl,
)

print("bzl file evaluation")

并构建:

load(":foo.bzl", "foo_binary")

print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")

ctx.label 对应于正在分析的目标的标签。ctx 对象包含许多实用的字段和方法;您可以在 API 参考文档中找到详尽的列表。

查询代码:

$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1

进行一些观察:

  • 系统会先输出“bzl file evaluation”。在评估 BUILD 文件之前,Bazel 会评估其加载的所有文件。如果有多个 BUILD 文件正在加载 foo.bzl,您只会看到一次“bzl 文件评估”,因为 Bazel 会缓存评估结果。
  • 回调函数 _foo_binary_impl 不会被调用。Bazel 查询会加载 BUILD 文件,但不会分析目标。

如需分析目标,请使用 cquery(“配置的查询”)或 build 命令:

$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...

如您所见,_foo_binary_impl 现在会被调用两次,每次针对一个目标。

请注意,由于在调用 bazel queryfoo.bzl 的评估结果会被缓存,因此“bzl file evaluation”和“BUILD file”都不会再次输出。Bazel 仅在实际执行 print 语句时才发出这些语句。

创建文件

为了使规则更有用,请更新规则以生成文件。首先,声明文件并为其命名。在此示例中,创建一个与目标同名的文件:

ctx.actions.declare_file(ctx.label.name)

如果您现在运行 bazel build :all,将会收到错误:

The following files have no generating action:
bin2

每当您声明文件时,都必须通过创建操作来告知 Bazel 如何生成该文件。使用 ctx.actions.write 创建包含指定内容的文件。

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello\n",
    )

代码有效,但不会执行任何操作:

$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)

ctx.actions.write 函数注册了一项操作,该操作告知了 Bazel 如何生成文件。不过,在实际请求该文件之前,Bazel 不会创建该文件。因此,最后要做的是告知 Bazel 该文件是规则的输出,而不是在规则实现中使用的临时文件。

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello!\n",
    )
    return [DefaultInfo(files = depset([out]))]

稍后会介绍 DefaultInfodepset 函数。目前,假设最后一行是选择规则输出的方式。

现在,运行 Bazel:

$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
  bazel-bin/bin1

$ cat bazel-bin/bin1
Hello!

您已成功生成文件!

属性

为了使规则更有用,请使用 attr 模块添加新属性,并更新规则定义。

添加一个名为 username 的字符串属性:

foo_binary = rule(
    implementation = _foo_binary_impl,
    attrs = {
        "username": attr.string(),
    },
)

接下来,在 BUILD 文件中设置该变量:

foo_binary(
    name = "bin",
    username = "Alice",
)

如需在回调函数中访问值,请使用 ctx.attr.username。例如:

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello {}!\n".format(ctx.attr.username),
    )
    return [DefaultInfo(files = depset([out]))]

请注意,您可以将属性设为必需属性或设置默认值。查看 attr.string 的文档。 您还可以使用其他类型的属性,例如 booleanlist of integers

依赖项

依赖项属性(例如 attr.labelattr.label_list)用于声明从拥有该属性的目标到标签出现在属性值中的目标的依赖项。这类属性构成了目标图的基础。

BUILD 文件中,目标标签显示为字符串对象,例如 //pkg:name。在实现函数中,目标将作为 Target 对象进行访问。例如,使用 Target.files 查看目标返回的文件。

多个文件

默认情况下,只有通过规则创建的目标才能作为依赖项(例如 foo_library() 目标)出现。如果您希望属性接受作为输入文件的目标(例如代码库中的源文件),可以使用 allow_files 并指定接受的文件扩展名列表(或使用 True 以允许任何文件扩展名):

"srcs": attr.label_list(allow_files = [".java"]),

可以使用 ctx.files.<attribute name> 访问文件列表。例如,可以通过以下方式访问 srcs 属性中的文件列表

ctx.files.srcs

单个文件

如果您只需要一个文件,请使用 allow_single_file

"src": attr.label(allow_single_file = [".java"])

然后,您可以在 ctx.file.<attribute name> 下访问此文件:

ctx.file.src

使用模板创建文件

您可以创建一条规则,用于根据模板生成 .cc 文件。此外,您可以使用 ctx.actions.write 输出在规则实现函数中构建的字符串,但这存在两个问题。首先,随着模板越来越大,将其放在单独的文件中可以更有效地利用内存,并避免在分析阶段构建大型字符串。其次,使用单独的文件对用户来说更方便。请改用 ctx.actions.expand_template,该命令会对模板文件执行替换操作。

创建 template 属性以声明对模板文件的依赖项:

def _hello_world_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name + ".cc")
    ctx.actions.expand_template(
        output = out,
        template = ctx.file.template,
        substitutions = {"{NAME}": ctx.attr.username},
    )
    return [DefaultInfo(files = depset([out]))]

hello_world = rule(
    implementation = _hello_world_impl,
    attrs = {
        "username": attr.string(default = "unknown person"),
        "template": attr.label(
            allow_single_file = [".cc.tpl"],
            mandatory = True,
        ),
    },
)

用户可以按如下方式使用该规则:

hello_world(
    name = "hello",
    username = "Alice",
    template = "file.cc.tpl",
)

cc_binary(
    name = "hello_bin",
    srcs = [":hello"],
)

如果您不想向最终用户公开模板,并且始终使用同一模板,则可以设置默认值并将属性设为私密:

    "_template": attr.label(
        allow_single_file = True,
        default = "file.cc.tpl",
    ),

以下划线开头的属性是私有属性,无法在 BUILD 文件中设置。该模板现在是一个隐式依赖项:每个 hello_world 目标都依赖于此文件。别忘了通过更新 BUILD 文件并使用 exports_files,使其他软件包能够看到此文件:

exports_files(["file.cc.tpl"])

进一步了解