本页面介绍了 Starlark 配置的优势和基本用法,即 Bazel 用于自定义项目构建方式的 API。其中包括如何定义 build 设置并提供了示例。
这样就可以:
- 为您的项目定义自定义标志,这样就无需使用
--define
- 编写转换以采用与其父级不同的配置(例如
--compilation_mode=opt
或--cpu=arm
)配置依赖项 - 将更好的默认值融入到规则中(例如使用指定的 SDK 自动构建
//my:android_app
)
等等,全部来自 .bzl 文件(无需发布 Bazel)。如需查看示例,请参阅 bazelbuild/examples
代码库。
用户定义的 build 设置
构建设置是一条配置信息。不妨将配置视为键值对映射。设置 --cpu=ppc
和 --copt="-DFoo"
会生成类似于 {cpu: ppc, copt: "-DFoo"}
的配置。每个条目都是构建设置。
cpu
和 copt
等传统标志是原生设置 - 它们的键已定义,其值在原生 bazel Java 代码内设置。Bazel 用户只能通过命令行以及以原生方式维护的其他 API 进行读取和写入。更改原生标志及其公开 API 需要 bazel 版本。用户定义的 build 设置在 .bzl
文件中定义(因此不需要 bazel 版本即可注册更改)。您也可以通过命令行进行设置(如果指定为 flags
,请参见下文),但也可以通过用户定义的过渡进行设置。
定义 build 设置
build_setting
rule()
参数
构建设置是与任何其他规则一样的规则,使用 Starlark rule()
函数的 build_setting
属性进行区分。
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
build_setting
属性接受用于指定 build 设置类型的函数。该类型仅限于一组基本的 Starlark 类型,例如 bool
和 string
。如需了解详情,请参阅 config
模块文档。您可以在规则的实现函数中执行更复杂的输入。详见下文。
config
模块的函数采用可选的布尔值参数 flag
,该参数默认设置为 false。如果 flag
设置为 true,那么用户可以在命令行中设置构建设置;还可以通过规则编写人员通过默认值和转换在内部进行构建设置。并非所有设置都应该可由用户设定。例如,作为规则编写者,如果您在测试规则中想要启用某个调试模式,您肯定不希望用户在其他非测试规则中随意开启该功能。
使用 ctx.build_setting_value
与所有规则一样,build 设置规则也具有实现函数。构建设置的基本 Starlark 类型值可通过 ctx.build_setting_value
方法访问。此方法仅适用于 build 设置规则的 ctx
对象。这些实现方法可以直接转发构建设置值,也可以执行其他工作,例如类型检查或更复杂的结构体创建。您将通过如下方式实现 enum
类型的构建设置:
# 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
参数,允许通过命令行或 bazelrcs 多次设置该标志。其默认值仍然使用字符串类型的属性进行设置:
# 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"
)
该 flag 的每个设置都被视为单个值:
$ 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 设置。
# 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 设置的名称为其完整目标路径,使用 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 设置别名
您可以为构建设置目标路径设置别名,以便通过命令行更轻松地阅读。别名的功能类似于原生标志,并且也使用双短划线选项语法。
通过将 --flag_alias=ALIAS_NAME=TARGET_PATH
添加到 .bazelrc
来设置别名。例如,如需将别名设置为 coffee
,请运行以下命令:
# .bazelrc
build --flag_alias=coffee=//experimental/user/starlark_configurations/basic_build_setting:coffee-temp
最佳做法:多次设置别名会导致最新的别名优先。使用唯一的别名,以避免意外解析结果。
如需使用别名,请输入它来代替构建设置目标路径。在用户的 .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_flag
和 label_setting
。这些规则会将设置 build 设置的实际目标提供程序转发给。label_flag
和 label_setting
可通过转换读取/写入,而 label_flag
可由用户设置,就像其他 build_setting
规则一样。它们唯一的区别是它们无法自定义。
标签类型设置最终将取代后期默认值的功能。延迟绑定的默认特性是标签类型特性,其最终值可能会受到配置的影响。在 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_setting
的 flag_values
属性。与配置匹配的值会作为 String
进行传递,然后解析为匹配的 build 设置类型。
config_setting(
name = "my_config",
flag_values = {
"//example:favorite_flavor": "MANGO"
}
)
用户定义的过渡
配置转换会将转换图中已配置的目标之间的转换映射到构建图中的另一个。
设置它们的规则必须包含一个特殊属性:
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist"
)
通过添加过渡效果,您可以轻松构建 build 图的大小。此操作会在可供您创建此规则目标的软件包上设置许可名单。上述代码块中的默认值会将所有内容列入许可名单。但是,如果您希望限制谁使用您的规则,可以将该属性设置为指向您自己的自定义许可名单。如果您希望了解转换会对 build 性能产生怎样的影响或提供帮助,请联系 bazel-discuss@googlegroups.com。
定义
过渡定义了规则之间的配置更改。例如,诸如“为我的依赖项与其父级不同的 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()
函数接受一个实现函数、一组要读取的构建设置(inputs
) 和一组要写入的构建设置 (outputs
)。该实现函数有两个参数:settings
和 attr
。settings
是在 transition()
的 inputs
参数中声明的所有设置的字典 {String
:Object
}。
attr
是转换连接的规则的属性和值的字典。当作为传出边缘过渡附加时,这些属性的值都是配置 select-select() 后的结果。作为传入的边缘过渡附加时,attr
不包含任何使用选择器来解析它们的值的属性。如果传入的 --foo
边缘转换读取 bar
属性,之后也会选择 --foo
来设置 bar
属性,那么传入的边缘转换可能会读取转换过程中 bar
的错误值。
实现函数必须返回要应用的新 build 设置值的字典(如果字典具有多个输出配置,则为字典列表)。返回的字典密钥集必须完全包含传递给转换函数的 outputs
参数的一组构建设置。即使 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()
的 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
上转换。请改为使用自定义构建设置。一般来说,不建议使用 --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:案例研究
图 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
- \(i\) \([1..n]\)的
//pkg:i_0
和//pkg:i_1
假设您实现了一个 --//foo:owner=<STRING>
和 //pkg:i_b
标志
depConfig = myConfig + depConfig.owner="$(myConfig.owner)$(b)"
换句话说,//pkg:i_b
会将 b
附加到 --owner
的所有依赖项的旧值。
这会生成以下配置的目标:
//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\)针对 \(\{0,1\}\)所有 \(b_i\) 配置。
这会使构建图比目标图呈指数级增加,并对内存和性能造成相应影响。
待办事项:添加衡量和缓解这些问题的策略。
补充阅读材料
如需详细了解如何修改 build 配置,请参阅:
- Starlark build 配置
- Bazel 可配置性路线图
- 完整的端到端示例集合