优先使用 DAMP BUILD 文件而非 DRY
DRY 原则(“不要重复自己”)提倡引入变量和函数等抽象来避免代码冗余,从而鼓励代码的独特性。
与之相反,DAMP 原则(“描述性且有意义的短语”)提倡注重可读性而非唯一性,以便文件更易于理解和维护。
BUILD
文件不是代码,而是配置。它们不会像代码一样进行测试,但需要由人员和工具进行维护。因此,与 DRY 相比,DAMP 更适合它们。
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();' > $@",
)
目标命名
目标名称应具有描述性。如果目标包含一个源文件,则目标通常应采用从该源文件派生的名称(例如,chat.cc
的 cc_library
可以命名为 chat
,DirectMessage.java
的 java_library
可以命名为 direct_message
)。
软件包的同名目标(与包含目录同名的目标)应提供目录名称所描述的功能。如果没有此类目标,请勿创建同名目标。
引用同名目标时,请优先使用短名称(//x
而非 //x:x
)。如果您位于同一软件包中,请优先使用本地引用(:x
而非 //x
)。
避免使用具有特殊含义的“预留”目标名称。这包括 all
、__pkg__
和 __subpackages__
,这些名称具有特殊的语义,在使用时可能会造成混淆和意外行为。
如果没有普遍的团队惯例,以下是 Google 广泛使用的一些非约束性建议:
- 一般来说,请使用 "snake_case"
- 对于包含一个
src
的java_library
,这意味着使用的名称与不带扩展名的文件名不同 - 对于 Java
*_binary
和*_test
规则,请使用“大驼峰式命名法”。这样,目标名称就可以与其中一个src
匹配。对于java_test
,这使得test_class
属性可以从目标的名称推断出来。
- 对于包含一个
- 如果特定目标有多个变体,请添加后缀以消除歧义(例如
:foo_dev
、:foo_prod
或:bar_x86
、:bar_x64
) - 使用
_test
、_unittest
、Test
或Tests
的后缀_test
目标 - 避免使用
_lib
或_library
等无意义的后缀(除非必须这样做,以避免_library
目标与其对应的_binary
之间发生冲突) - 对于与 proto 相关的目标:
proto_library
目标的名称应以_proto
结尾- 特定于语言的
*_proto_library
规则应与底层 proto 匹配,但应将_proto
替换为特定于语言的后缀,例如:cc_proto_library
:_cc_proto
java_proto_library
:_java_proto
java_lite_proto_library
:_java_proto_lite
公开范围
可见性应尽可能缩小范围,同时仍允许测试和反向依赖项进行访问。请根据需要使用 __pkg__
和 __subpackages__
。
避免将软件包 default_visibility
设置为 //visibility:public
。仅应为项目公共 API 中的目标单独设置 //visibility:public
。这些可以是设计为由外部项目依赖的库,也可以是外部项目的构建流程可以使用的二进制文件。
依赖项
依赖项应仅限于直接依赖项(规则中列出的来源所需的依赖项)。请勿列出传递依赖项。
应先列出软件包本地依赖项,并以与上文对当前软件包中的目标的引用部分中所述的方式(而非通过其绝对软件包名称)引用它们。
最好直接以单个列表的形式列出依赖项。将多个目标的“常见”依赖项放入变量会降低可维护性,使工具无法更改目标的依赖项,并且可能会导致未使用的依赖项。
Glob
使用 []
指示“无目标”。请勿使用与任何内容都不匹配的正则表达式:与空列表相比,它更容易出错,并且不太明显。
递归
请勿使用递归 glob 来匹配源文件(例如 glob(["**/*.java"])
)。
递归正则表达式会导致 BUILD
文件难以推理,因为它们会跳过包含 BUILD
文件的子目录。
与为每个目录创建一个 BUILD
文件并在其之间定义依赖项图相比,递归正则表达式通常效率较低,因为这样可以实现更好的远程缓存和并行处理。
最好在每个目录中编写一个 BUILD
文件,并定义它们之间的依赖项图。
非递归
通常可以接受非递归正则表达式。
避免使用列表推理
避免在 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
可能会被误读为“通过重新运行此目标一次来 deflake 此目标”。flaky = True
明确表示“此测试不稳定”。
与 Python 样式指南的差异
虽然我们力求与 Python 样式指南保持兼容,但还是存在一些差异:
没有严格的行长度限制。长注释和长字符串通常会拆分为 79 列,但这不是必需的。不应在代码审核或提交前脚本中强制执行此规则。原因:标签可以很长,超出此限制。通常,
BUILD
文件由工具生成或修改,这与行长度限制不相符。不支持隐式字符串串联。使用
+
运算符。原因:BUILD
文件包含许多字符串列表。很容易忘记英文逗号,这会导致完全不同的结果。这在过去造成了许多 bug。另请参阅此讨论。在规则中,为关键字参数的
=
符号周围添加空格。原因:命名实参比在 Python 中更常见,并且始终位于单独的行中。空格有助于提高可读性。这种惯例已经存在很长时间了,因此不值得修改所有现有的BUILD
文件。默认情况下,请使用双引号来表示字符串。原因:Python 样式指南中未指定这一点,但建议保持一致。因此,我们决定仅使用双引号字符串。许多语言都使用双引号来表示字符串字面量。
在两个顶级定义之间使用单个空行。原因:
BUILD
文件的结构与典型的 Python 文件不同。它只有顶级语句。使用单个空白行可缩短BUILD
文件。