首选 DAMP BUILD 文件而非 DRY
DRY 原则(“Don't Repeat Yourself”,不要重复自己)通过引入变量和函数等抽象概念来避免代码冗余,从而鼓励唯一性。
相比之下,DAMP 原则(“Descriptive and Meaningful Phrases”,描述性且有意义的短语)鼓励可读性而非唯一性,以便文件更易于理解和维护。
BUILD 文件不是代码,而是配置。它们不像代码那样经过测试,但需要人员和工具进行维护。这使得 DAMP 比 DRY 更适合它们。
BUILD.bazel 文件格式
BUILD 文件格式遵循与 Go 相同的方法,即使用标准化工具来处理大多数格式问题。
Buildifier 是一种以标准样式解析和
发出源代码的工具。因此,每个 BUILD 文件都以相同的自动化方式进行格式化,这使得格式化在代码审核期间不是问题。它还使工具更容易理解、修改和生成 BUILD 文件。
BUILD 文件格式必须与 buildifier 的输出匹配。
格式示例
# Test code implementing the Foo controller.
package(default_testonly = True)
py_test(
name = "foo_test",
srcs = glob(["*.py"]),
data = [
"//data/production/foo:startfoo",
"//foo",
"//third_party/java/jdk:jdk-k8",
],
flaky = True,
deps = [
":check_bar_lib",
":foo_data_check",
":pick_foo_port",
"//pyglib",
"//testing/pybase",
],
)
文件结构
建议:使用以下顺序(每个元素都是可选的):
软件包说明(注释)
所有
load()语句package()函数。对规则和宏的调用
Buildifier 会区分独立注释和附加到元素的注释。如果注释未附加到特定元素,请在其后使用空行。在进行自动化更改时(例如,在删除规则时保留或移除注释),这种区分非常重要。
# Standalone comment (such as to make a section in a file)
# Comment for the cc_library below
cc_library(name = "cc")
对当前软件包中目标的引用
应通过相对于软件包目录的路径来引用文件(切勿使用向上引用,例如 ..)。生成的文件应以“:”为前缀,以表明它们不是来源。源文件不应以 : 为前缀。规则应以 : 为前缀。例如,假设 x.cc 是一个源文件:
cc_library(
name = "lib",
srcs = ["x.cc"],
hdrs = [":gen_header"],
)
genrule(
name = "gen_header",
srcs = [],
outs = ["x.h"],
cmd = "echo 'int x();' > $@",
)
目标命名
目标名称应具有描述性。如果目标包含一个源文件,
则目标通常应具有从该来源派生的名称(例如,
cc_library 的 chat.cc 可以命名为 chat,或者 java_library 的
DirectMessage.java 可以命名为 direct_message)。
软件包的同名目标(与包含目录同名的目标)应提供目录名称所描述的功能。如果没有此类目标,请勿创建同名目标。
引用同名目标时,请首选使用短名称(//x
而不是 //x:x)。如果您位于同一软件包中,请首选本地
引用(:x 而不是 //x)。
避免使用具有特殊含义的“保留”目标名称。这包括 all、__pkg__ 和 __subpackages__,这些名称具有特殊的语义,使用时可能会导致混淆和意外行为。
在没有普遍的团队惯例的情况下,以下是一些在 Google 广泛使用的非约束性建议:
- 一般来说,使用“snake_case”
- 对于具有一个
src的java_library,这意味着使用与不带扩展名的文件名不同的名称 - 对于 Java
*_binary和*_test规则,请使用 “Upper CamelCase”。 这样,目标名称就可以与其中一个src匹配。对于java_test,这样就可以从目标的名称推断出test_class属性。
- 对于具有一个
- 如果特定目标有多个变体,则添加后缀以消除歧义(例如,
:foo_dev、:foo_prod或:bar_x86、:bar_x64) - 为
_test目标添加后缀_test、_unittest、Test或Tests - 避免使用无意义的后缀,例如
_lib或_library(除非有必要 避免_library目标与其对应的_binary之间发生冲突) - 对于 proto 相关目标:
proto_library目标的名称应以_proto结尾- 特定于语言的
*_proto_library规则应与底层 proto 匹配,但将_proto替换为特定于语言的后缀,例如:cc_proto_library:_cc_protojava_proto_library:_java_protojava_lite_proto_library:_java_proto_lite
公开范围
公开范围应尽可能缩小,同时仍允许测试和反向依赖项进行访问。根据需要使用 __pkg__ 和 __subpackages__。
避免将软件包 default_visibility 设置为 //visibility:public。
//visibility:public 仅应针对项目公共 API 中的目标单独设置。这些目标可能是旨在供外部项目依赖的库,也可能是可供外部项目的构建流程使用的二进制文件。
依赖项
依赖项应仅限于直接依赖项(规则中列出的来源所需的依赖项)。请勿列出传递依赖项。
软件包本地依赖项应先列出,并以与 兼容的方式引用 上述对当前软件包中目标的引用 部分(而不是通过其绝对软件包名称)。
最好将依赖项直接列为单个列表。将多个目标的“常见”依赖项放入变量会降低可维护性,使工具无法更改目标的依赖项,并可能导致未使用的依赖项。
Glob
使用 [] 表示“无目标”。请勿使用与任何内容都不匹配的 glob:它比空列表更容易出错,也不太明显。
递归
请勿使用递归 glob 来匹配源文件(例如,
glob(["**/*.java"]))。
递归 glob 会跳过包含 BUILD 文件的子目录,因此很难推断出 BUILD 文件。
递归 glob 通常不如每个目录都有一个 BUILD 文件(且它们之间定义了依赖项图)的效率高,因为后者可以实现更好的远程缓存和并行性。
最好在每个目录中编写一个 BUILD 文件,并在它们之间定义依赖项图。
非递归
非递归 glob 通常是可以接受的。
避免使用列表推导式
避免在 BUILD.bazel 文件的顶层使用列表推导式。
通过使用单独的顶层规则或宏调用创建每个命名目标,实现重复调用的自动化。为每个目标提供一个简短的 name 参数,以提高清晰度。
列表推导式会降低以下方面:
- 可维护性。人工维护人员和大规模自动化更改很难或无法正确更新列表推导式。
- 曝光度。由于该模式没有
name参数,因此很难按名称查找规则。
列表推导式模式的常见应用是生成测试。例如:
[[java_test(
name = "test_%s_%s" % (backend, count),
srcs = [ ... ],
deps = [ ... ],
...
) for backend in [
"fake",
"mock",
]] for count in [
1,
10,
]]
我们建议使用更简单的替代方案。例如,定义一个生成一项测试的宏,并针对每个顶层 name 调用该宏:
my_java_test(name = "test_fake_1",
...)
my_java_test(name = "test_fake_10",
...)
...
请勿使用 deps 变量
请勿使用列表变量封装常见依赖项:
COMMON_DEPS = [
"//d:e",
"//x/y:z",
]
cc_library(name = "a",
srcs = ["a.cc"],
deps = COMMON_DEPS + [ ... ],
)
cc_library(name = "b",
srcs = ["b.cc"],
deps = COMMON_DEPS + [ ... ],
)
同样,请勿使用带有
exports 的库目标对依赖项进行分组。
而是为每个目标单独列出依赖项:
cc_library(name = "a",
srcs = ["a.cc"],
deps = [
"//a:b",
"//x/y:z",
...
],
)
cc_library(name = "b",
srcs = ["b.cc"],
deps = [
"//a:b",
"//x/y:z",
...
],
)
让 Gazelle 和其他工具 来维护它们。虽然会有重复,但您无需考虑如何管理依赖项。
首选字面量字符串
虽然 Starlark 提供了用于串联 (+) 和格式化 (%) 的字符串运算符,但请谨慎使用它们。您可能会想提取出常见的字符串部分,以使表达式更简洁或拆分长行。但是,
很难一目了然地读取拆分的字符串值。
当值被拆分时, buildozer 和 代码搜索 等自动化工具很难找到值并正确更新它们。
在
BUILD文件中,可读性比避免重复更重要 (请参阅 DAMP 与 DRY)。本样式指南 警告不要拆分标签值字符串 并 明确允许长行。
Buildifier 会在检测到连接的字符串是标签时自动融合它们。
因此,请首选显式字面量字符串,而不是连接或格式化的字符串,尤其是在标签类型属性(例如 name 和 deps)中。例如,以下 BUILD 代码段:
NAME = "foo"
PACKAGE = "//a/b"
proto_library(
name = "%s_proto" % NAME,
deps = [PACKAGE + ":other_proto"],
alt_dep = "//surprisingly/long/chain/of/package/names:" +
"extravagantly_long_target_name",
)
最好重写为
proto_library(
name = "foo_proto",
deps = ["//a/b:other_proto"],
alt_dep = "//surprisingly/long/chain/of/package/names:extravagantly_long_target_name",
)
限制每个 .bzl 文件导出的符号
尽量减少每个公共 .bzl (Starlark) 文件导出的符号(规则、宏、常量、函数)数量。我们建议,只有当文件中的多个符号确定要一起使用时,才应导出它们。否则,请将其拆分为多个 .bzl 文件,每个文件都有自己的 bzl_library。
过多的符号可能会导致 .bzl 文件扩展为广泛的符号“库”,从而导致对单个文件的更改迫使 Bazel 重建许多目标。
其他惯例
使用大写字母和下划线声明常量(例如
GLOBAL_CONSTANT),使用小写字母和下划线声明变量(例如my_variable)。标签绝不应拆分,即使它们超过 79 个字符也是如此。 标签应尽可能使用字符串字面量。原因:这样可以轻松 查找和替换。它还可以提高可读性。
name 属性的值应为字面量常量字符串(宏除外)。原因:外部工具使用 name 属性来引用 规则。它们需要查找规则,而无需解释代码。
设置布尔值类型属性时,请使用布尔值,而不是整数值。 出于历史原因,规则仍会根据需要将整数转换为布尔值,但不建议这样做。原因:
flaky = 1可能会被误读为 “通过重新运行一次来取消此目标的 flaky 状态”。flaky = True明确表示“此测试是 flaky 的”。
与 Python 样式指南的区别
虽然与 Python 样式指南 兼容是我们的目标,但两者之间存在一些区别:
没有严格的行长度限制。长注释和长字符串通常会拆分为 79 列,但这不是必需的。不应在代码审核或预提交脚本中强制执行此操作。原因:标签可能很长,超出此 限制。
BUILD文件通常由工具生成或编辑,这与行长度限制不符。不支持隐式字符串连接。请使用
+运算符。 原因:BUILD文件包含许多字符串列表。很容易忘记逗号,这会导致完全不同的结果。过去曾因此出现许多 bug。另请参阅此讨论。在规则中使用空格将
=符号括起来,用于关键字实参。原因: 命名实参比 Python 中更常见,并且始终位于单独的行中。空格可以提高可读性。此惯例已存在很长时间,修改所有现有BUILD文件并不值得。默认情况下,对字符串使用双引号。原因:Python 样式指南中未 指定此项,但建议保持一致性。因此,我们决定仅使用双引号字符串。许多语言都对字符串字面量使用双引号。
在两个顶层定义之间使用一个空行。原因: 文件的结构不像典型的 Python 文件。
BUILD它只有顶层语句。使用单空行可以缩短BUILD文件。