配置

本页介绍了 Starlark 配置的优势和基本用法,以及 Bazel 的 API,用于自定义项目的构建方式。其中介绍了如何定义 build 设置,并提供了示例。

这样一来,您便可以:

  • 为项目定义自定义标志,从而无需使用 --define
  • 编写 转换,以在 不同配置中配置依赖项,而不是其父项 (例如 --compilation_mode=opt--cpu=arm
  • 将更好的默认值植入规则(例如使用指定的 SDK 自动构建 //my:android_app

等等,所有这些都完全来自 .bzl 文件(无需 Bazel 版本)。如需查看 示例,请参阅 bazelbuild/examples代码库。

用户定义的 build 设置

build 设置是单个 配置 信息。您可以将配置视为键值对映射。设置 --cpu=ppc--copt="-DFoo" 会生成类似于 {cpu: ppc, copt: "-DFoo"} 的配置。每个条目都是一个 build 设置。

cpucopt 这样的传统标志是原生设置,其键是在原生 Bazel Java 代码中定义,其值是在原生 Bazel Java 代码中设置。 Bazel 用户只能通过命令行和其他原生维护的 API 读取和写入这些标志。更改原生标志以及公开这些标志的 API 需要 Bazel 版本。用户定义的 build 设置在 .bzl 文件中定义(因此,无需 Bazel 版本即可注册更改)。它们也可以通过命令行设置 (如果它们被指定为 flags,请参阅下文了解详情),但也可以通过用户定义的转换设置。

定义 build 设置

端到端示例

build_setting rule() 参数

build 设置与其他任何规则一样,都是规则,并且使用 Starlark rule() 函数的 build_setting 属性进行区分。

# example/buildsettings/build_settings.bzl
string_flag = rule(
    implementation = _impl,
    build_setting = config.string(flag = True)
)

build_setting 属性接受一个函数,该函数用于指定 build 设置的类型。该类型仅限于一组基本的 Starlark 类型,例如 boolstring。如需了解详情,请参阅 config 模块 文档。更复杂的类型可以在规则的实现函数中完成。下文对此进行了详细介绍。

config 模块的函数接受一个可选的布尔值参数 flag, 该参数默认设置为 false。如果 flag 设置为 true,则用户可以在命令行中设置 build 设置,规则编写者也可以通过默认值和转换在内部设置 build 设置。并非所有设置都应由用户设置。例如,如果您作为规则编写者有一些想要在测试规则中启用的调试模式,您不希望让用户能够在其他非测试规则中随意启用该功能。

使用 ctx.build_setting_value

与所有规则一样,build 设置规则也有实现函数。 build 设置的基本 Starlark 类型值可以通过 ctx.build_setting_value 方法访问。此方法仅适用于 ctx build 设置规则的对象。这些实现方法可以直接转发 build 设置值,也可以对其执行其他工作,例如类型检查或创建更复杂的结构体。以下是如何实现 enum 类型的 build 设置:

# example/buildsettings/build_settings.bzl
TemperatureProvider = provider(fields = ['type'])

temperatures = ["HOT", "LUKEWARM", "ICED"]

def _impl(ctx):
    raw_temperature = ctx.build_setting_value
    if raw_temperature not in temperatures:
        fail(str(ctx.label) + " build setting allowed to take values {"
             + ", ".join(temperatures) + "} but was set to unallowed value "
             + raw_temperature)
    return TemperatureProvider(type = raw_temperature)

temperature = rule(
    implementation = _impl,
    build_setting = config.string(flag = True)
)

定义多集字符串标志

字符串设置有一个额外的 allow_multiple 参数,该参数允许在命令行或 bazelrc 中多次设置标志。它们的默认值仍然使用字符串类型的属性设置:

# example/buildsettings/build_settings.bzl
allow_multiple_flag = rule(
    implementation = _impl,
    build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "allow_multiple_flag")
allow_multiple_flag(
    name = "roasts",
    build_setting_default = "medium"
)

标志的每个设置都被视为单个值:

$ bazel build //my/target --//example:roasts=blonde \
    --//example:roasts=medium,dark

上述内容解析为 {"//example:roasts": ["blonde", "medium,dark"]}ctx.build_setting_value 返回列表 ["blonde", "medium,dark"]

实例化 build 设置

使用 build_setting 参数定义的规则具有隐式强制性 build_setting_default 属性。此属性采用与 build_setting 参数声明的类型相同的类型。

# example/buildsettings/build_settings.bzl
FlavorProvider = provider(fields = ['type'])

def _impl(ctx):
    return FlavorProvider(type = ctx.build_setting_value)

flavor = rule(
    implementation = _impl,
    build_setting = config.string(flag = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
    name = "favorite_flavor",
    build_setting_default = "APPLE"
)

预定义设置

端到端示例

Skylib 库包含一组预定义设置,您可以实例化这些设置,而无需 编写自定义 Starlark。

例如,如需定义接受一组有限的字符串值的设置,请执行以下操作:

# example/BUILD
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
string_flag(
    name = "myflag",
    values = ["a", "b", "c"],
    build_setting_default = "a",
)

如需查看完整列表,请参阅 常见 build 设置规则

使用 build 设置

依赖于 build 设置

如果目标想要读取一段配置信息,它可以直接依赖于 build 设置,方法是使用常规属性依赖项。

# example/rules.bzl
load("//example/buildsettings:build_settings.bzl", "FlavorProvider")
def _rule_impl(ctx):
    if ctx.attr.flavor[FlavorProvider].type == "ORANGE":
        ...

drink_rule = rule(
    implementation = _rule_impl,
    attrs = {
        "flavor": attr.label()
    }
)
# example/BUILD
load("//example:rules.bzl", "drink_rule")
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
    name = "favorite_flavor",
    build_setting_default = "APPLE"
)
drink_rule(
    name = "my_drink",
    flavor = ":favorite_flavor",
)

语言可能希望创建一组规范的 build 设置,该语言的所有规则都依赖于这些设置。虽然 fragments 的原生概念在 Starlark 配置世界中不再作为硬编码对象存在,但翻译此概念的一种方法是使用一组常见的隐式属性。例如:

# kotlin/rules.bzl
_KOTLIN_CONFIG = {
    "_compiler": attr.label(default = "//kotlin/config:compiler-flag"),
    "_mode": attr.label(default = "//kotlin/config:mode-flag"),
    ...
}

...

kotlin_library = rule(
    implementation = _rule_impl,
    attrs = dicts.add({
        "library-attr": attr.string()
    }, _KOTLIN_CONFIG)
)

kotlin_binary = rule(
    implementation = _binary_impl,
    attrs = dicts.add({
        "binary-attr": attr.label()
    }, _KOTLIN_CONFIG)

在命令行中使用 build 设置

与大多数原生标志类似,您可以使用命令行设置标记为标志的 build 设置 。build 设置的名称是其完整的目标路径,使用 name=value 语法:

$ bazel build //my/target --//example:string_flag=some-value # allowed
$ bazel build //my/target --//example:string_flag some-value # not allowed

支持特殊的布尔值语法:

$ bazel build //my/target --//example:boolean_flag
$ bazel build //my/target --no//example:boolean_flag

使用 build 设置别名

您可以为 build 设置目标路径设置别名,以便在命令行中更轻松地读取该路径。别名的功能与原生标志类似,并且也使用双连字符选项语法。

如需设置别名,请将 --flag_alias=ALIAS_NAME=TARGET_PATH 添加到 .bazelrc。例如,如需将别名设置为 coffee,请执行以下操作:

# .bazelrc
build --flag_alias=coffee=//experimental/user/starlark_configurations/basic_build_setting:coffee-temp

最佳实践:多次设置别名会导致最近设置的别名优先。请使用唯一的别名,以避免出现意外的解析结果。

如需使用别名,请将其替换为 build 设置目标路径。 在用户的 .bazelrc 中设置上述 coffee 示例后:

$ bazel build //my/target --coffee=ICED

而不是

$ bazel build //my/target --//experimental/user/starlark_configurations/basic_build_setting:coffee-temp=ICED

最佳实践:虽然可以在命令行中设置别名,但将其保留在 .bazelrc 中可以减少命令行混乱。

标签类型的 build 设置

端到端示例

与其他 build 设置不同,标签类型的设置无法使用 build_setting 规则参数定义。相反,Bazel 有两个内置规则:label_flaglabel_setting。这些规则会将 build 设置所设置的实际目标的提供程序转发给这些规则。转换可以读取/写入 label_flaglabel_setting,用户可以像其他 build_setting 规则一样设置 label_flag。它们的唯一区别在于,它们无法自定义定义。

标签类型的设置最终将取代延迟绑定默认值的功能。延迟绑定默认属性是标签类型的属性,其最终值可能会受到配置的影响。在 Starlark 中,这将取代 configuration_field API。

# example/rules.bzl
MyProvider = provider(fields = ["my_field"])

def _dep_impl(ctx):
    return MyProvider(my_field = "yeehaw")

dep_rule = rule(
    implementation = _dep_impl
)

def _parent_impl(ctx):
    if ctx.attr.my_field_provider[MyProvider].my_field == "cowabunga":
        ...

parent_rule = rule(
    implementation = _parent_impl,
    attrs = { "my_field_provider": attr.label() }
)

# example/BUILD
load("//example:rules.bzl", "dep_rule", "parent_rule")

dep_rule(name = "dep")

parent_rule(name = "parent", my_field_provider = ":my_field_provider")

label_flag(
    name = "my_field_provider",
    build_setting_default = ":dep"
)

build 设置和 select()

端到端示例

用户可以使用 select() 配置 build 设置的属性。build 设置目标可以传递给 config_settingflag_values 属性。要与配置匹配的值将作为 String 传递,然后解析为 build 设置的类型以进行匹配。

config_setting(
    name = "my_config",
    flag_values = {
        "//example:favorite_flavor": "MANGO"
    }
)

用户定义的转换

配置转换会将 build 图中的一个配置目标到另一个配置目标的转换进行映射。

定义

转换定义了规则之间的配置更改。例如,转换会处理“为与父项不同的 CPU 编译我的依赖项”之类的请求。

正式来说,转换是从输入配置到一个或多个输出配置的函数。大多数转换都是 1:1,例如“使用 --cpu=ppc 替换输入配置”。1:2+ 转换也可能存在,但存在特殊限制。

在 Starlark 中,转换的定义方式与规则非常相似,包括定义 transition() 函数 和实现函数。

# example/transitions/transitions.bzl
def _impl(settings, attr):
    _ignore = (settings, attr)
    return {"//example:favorite_flavor" : "MINT"}

hot_chocolate_transition = transition(
    implementation = _impl,
    inputs = [],
    outputs = ["//example:favorite_flavor"]
)

transition() 函数接受实现函数、要读取的一组 build 设置(inputs) 和要写入的一组 build 设置 (outputs)。实现函数有两个形参:settingsattrsettingstransition()inputs 参数中声明的所有设置的字典 {String:Object}。

attr 是与转换所附加到的规则的属性和值的字典。当作为一 个出站边缘转换附加时,这些 属性的值都是在 select() 解析后配置的。当作为 入站边缘转换附加时,attr不 包含任何使用选择器来解析其值的属性。如果 --foo 上的入站边缘转换读取属性 bar,然后还选择 --foo 来设置属性 bar,则入站边缘转换可能会在转换中读取错误的 bar 值。

实现函数必须返回要应用的新 build 设置值的字典(或字典列表,如果转换具有多个输出配置)。返回的字典键集必须完全包含传递给转换函数的 outputs 形参的 build 设置集。即使在转换过程中 build 设置实际上没有更改,也是如此 - 其原始值必须在返回的字典中显式传递。

定义 1:2+ 转换

端到端示例

出站边缘转换可以将单个输入配置映射到两个或多个输出配置。这对于定义捆绑多架构代码的规则非常有用。

1:2+ 转换通过在转换实现函数中返回字典列表来定义。

# example/transitions/transitions.bzl
def _impl(settings, attr):
    _ignore = (settings, attr)
    return [
        {"//example:favorite_flavor" : "LATTE"},
        {"//example:favorite_flavor" : "MOCHA"},
    ]

coffee_transition = transition(
    implementation = _impl,
    inputs = [],
    outputs = ["//example:favorite_flavor"]
)

它们还可以设置自定义键,规则实现函数可以使用这些键来读取各个依赖项:

# example/transitions/transitions.bzl
def _impl(settings, attr):
    _ignore = (settings, attr)
    return {
        "Apple deps": {"//command_line_option:cpu": "ppc"},
        "Linux deps": {"//command_line_option:cpu": "x86"},
    }

multi_arch_transition = transition(
    implementation = _impl,
    inputs = [],
    outputs = ["//command_line_option:cpu"]
)

附加转换

端到端示例

转换可以附加到两个位置:入站边缘和出站边缘。 实际上,这意味着规则可以转换自己的配置(入站边缘转换)并转换其依赖项的配置(出站边缘转换)。

注意:目前无法将 Starlark 转换附加到原生规则。 如果您需要这样做,请与 bazel-discuss@googlegroups.com 联系,以获取有关解决方法的帮助。

入站边缘转换

入站边缘转换通过将 transition 对象 (由 transition() 创建)附加到 rule()'s cfg 参数来激活:

# example/rules.bzl
load("example/transitions:transitions.bzl", "hot_chocolate_transition")
drink_rule = rule(
    implementation = _impl,
    cfg = hot_chocolate_transition,
    ...

入站边缘转换必须是 1:1 转换。

出站边缘转换

出站边缘转换通过将 transition 对象 (由 transition() 创建)附加到属性的 cfg 参数来激活:

# example/rules.bzl
load("example/transitions:transitions.bzl", "coffee_transition")
drink_rule = rule(
    implementation = _impl,
    attrs = { "dep": attr.label(cfg = coffee_transition)}
    ...

出站边缘转换可以是 1:1 或 1:2+。

如需了解如何读取这些键,请参阅使用转换访问属性

原生选项的转换

端到端示例

Starlark 转换还可以通过选项名称的特殊前缀声明对原生 build 配置选项的读取和写入。

# example/transitions/transitions.bzl
def _impl(settings, attr):
    _ignore = (settings, attr)
    return {"//command_line_option:cpu": "k8"}

cpu_transition = transition(
    implementation = _impl,
    inputs = [],
    outputs = ["//command_line_option:cpu"]

不受支持的原生选项

Bazel 不支持使用 "//command_line_option:define"--define 进行转换。请改用自定义 build 设置。一般来说,不建议使用 --define 的新用法,而建议使用 build 设置。

Bazel 不支持对 --config 进行转换。这是因为 --config 是一个“扩展”标志,它会扩展到其他标志。

至关重要的是,--config 可能包含不影响 build 配置的标志, 例如 --spawn_strategy 。Bazel 在设计上无法将此类标志绑定到各个目标。这意味着无法在转换中以一致的方式应用它们。

作为一种解决方法,您可以在转换中显式列出属于配置的标志。 这需要在两个位置维护 --config 的扩展,这是一个已知的界面缺陷。

允许使用多个 build 设置的转换

设置允许使用多个值的 build 设置时,必须使用列表设置该设置的值。

# example/buildsettings/build_settings.bzl
string_flag = rule(
    implementation = _impl,
    build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "string_flag")
string_flag(name = "roasts", build_setting_default = "medium")
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
    # Using a value of just "dark" here will throw an error
    return {"//example:roasts" : ["dark"]},

coffee_transition = transition(
    implementation = _transition_impl,
    inputs = [],
    outputs = ["//example:roasts"]
)

空操作转换

如果转换返回 {}[]None,则表示将所有设置保留为其原始值的简写形式。这可能比将每个输出显式设置为自身更方便。

# example/transitions/transitions.bzl
def _impl(settings, attr):
    _ignore = (attr)
    if settings["//example:already_chosen"] is True:
      return {}
    return {
      "//example:favorite_flavor": "dark chocolate",
      "//example:include_marshmallows": "yes",
      "//example:desired_temperature": "38C",
    }

hot_chocolate_transition = transition(
    implementation = _impl,
    inputs = ["//example:already_chosen"],
    outputs = [
        "//example:favorite_flavor",
        "//example:include_marshmallows",
        "//example:desired_temperature",
    ]
)

使用转换访问属性

端到端示例

转换附加到出站边 时(无论转换是 1:1 转换还是 1:2+ 转换),如果 ctx.attr 尚不是列表,则强制将其设为列表。此列表中元素的顺序未指定。

# example/transitions/rules.bzl
def _transition_impl(settings, attr):
    return {"//example:favorite_flavor" : "LATTE"},

coffee_transition = transition(
    implementation = _transition_impl,
    inputs = [],
    outputs = ["//example:favorite_flavor"]
)

def _rule_impl(ctx):
    # Note: List access even though "dep" is not declared as list
    transitioned_dep = ctx.attr.dep[0]

    # Note: Access doesn't change, other_deps was already a list
    for other_dep in ctx.attr.other_deps:
      # ...


coffee_rule = rule(
    implementation = _rule_impl,
    attrs = {
        "dep": attr.label(cfg = coffee_transition)
        "other_deps": attr.label_list(cfg = coffee_transition)
    })

如果转换是 1:2+ 并且设置了自定义键,则可以使用 ctx.split_attr 为每个键读取各个依赖项:

# example/transitions/rules.bzl
def _impl(settings, attr):
    _ignore = (settings, attr)
    return {
        "Apple deps": {"//command_line_option:cpu": "ppc"},
        "Linux deps": {"//command_line_option:cpu": "x86"},
    }

multi_arch_transition = transition(
    implementation = _impl,
    inputs = [],
    outputs = ["//command_line_option:cpu"]
)

def _rule_impl(ctx):
    apple_dep = ctx.split_attr.dep["Apple deps"]
    linux_dep = ctx.split_attr.dep["Linux deps"]
    # ctx.attr has a list of all deps for all keys. Order is not guaranteed.
    all_deps = ctx.attr.dep

multi_arch_rule = rule(
    implementation = _rule_impl,
    attrs = {
        "dep": attr.label(cfg = multi_arch_transition)
    })

请点击此处查看完整示例

与平台和工具链集成

如今,许多原生标志(例如 --cpu--crosstool_top)都与工具链解析相关。将来,对这些类型的 标志的显式转换可能会被对 目标平台的转换所取代。

内存和性能注意事项

向 build 添加转换(以及因此添加新配置)会带来一些成本:build 图更大、build 图更难理解,以及 build 速度更慢。在考虑在 build 规则中使用转换时,值得考虑这些成本。以下示例说明了转换如何可能导致 build 图呈指数级增长。

行为不端的 build:案例研究

可伸缩性图表

图 1. 可伸缩性图表,显示了顶级目标及其依赖项。

此图显示了一个顶级目标 //pkg:app,它依赖于两个目标://pkg:1_0//pkg:1_1。这两个目标都依赖于两个目标://pkg:2_0//pkg:2_1。这两个目标都依赖于两个目标://pkg:3_0//pkg:3_1。 依此类推,直到 //pkg:n_0//pkg:n_1,这两个目标都依赖于单个目标 //pkg:dep

构建 //pkg:app 需要以下目标: \(2n+2\)

  • //pkg:app
  • //pkg:dep
  • //pkg:i_0//pkg:i_1(针对 \(i\) ) \([1..n]\)

假设您实现了一个标志 --//foo:owner=<STRING>,并且//pkg:i_b适用

depConfig = myConfig + depConfig.owner="$(myConfig.owner)$(b)"

换句话说,//pkg:i_b 会将其所有依赖项的 --owner 的旧值附加到 b

这会生成以下 配置的目标

//pkg:app                              //foo:owner=""
//pkg:1_0                              //foo:owner=""
//pkg:1_1                              //foo:owner=""
//pkg:2_0 (via //pkg:1_0)              //foo:owner="0"
//pkg:2_0 (via //pkg:1_1)              //foo:owner="1"
//pkg:2_1 (via //pkg:1_0)              //foo:owner="0"
//pkg:2_1 (via //pkg:1_1)              //foo:owner="1"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_0)  //foo:owner="00"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_1)  //foo:owner="01"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_0)  //foo:owner="10"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_1)  //foo:owner="11"
...

//pkg:dep 生成 \(2^n\) 配置的目标:config.owner= "\(b_0b_1...b_n\)"(针对所有 \(b_i\) )。 \(\{0,1\}\)

这使得 build 图比目标图呈指数级增长,并带来相应的内存和性能后果。

待办事项:添加用于衡量和缓解这些问题的策略。

深入阅读

如需详细了解如何修改 build 配置,请参阅: