构建样式指南

优先选择 DAMP build 文件,而不是 DRY build 文件

DRY 原则(“不要重复自己”)通过引入变量和函数等抽象概念来避免代码中的冗余,从而鼓励唯一性。

相比之下,DAMP 原则(“描述性且有意义的短语”)鼓励使用易读性高的名称,而不是追求唯一性,以便更轻松地理解和维护文件。

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();' > $@",
)

目标命名

目标名称应具有描述性。如果目标包含一个源文件,则目标名称通常应从该源派生而来(例如,chat.cccc_library 可以命名为 chatDirectMessage.javajava_library 可以命名为 direct_message)。

软件包的同名目标(与包含目录同名的目标)应提供目录名称所描述的功能。如果不存在此类目标,则不创建同名目标。

在引用同名目标时,最好使用短名称(//x 而不是 //x:x)。如果您位于同一软件包中,最好使用本地引用(:x 而不是 //x)。

避免使用具有特殊含义的“预留”目标名称。这包括 all__pkg____subpackages__,这些名称具有特殊语义,使用时可能会造成混淆和意外行为。

在没有普遍适用的团队惯例的情况下,以下是一些在 Google 广泛使用的非约束性建议:

  • 一般来说,请使用 “snake_case”
    • 对于包含一个 srcjava_library,这意味着使用与不含扩展名的文件名不同的名称
    • 对于 Java *_binary*_test 规则,请使用“Upper CamelCase”。 这样一来,目标名称就可以与某个 src 相匹配。对于 java_test,这使得可以从目标的名称推断出 test_class 属性。
  • 如果某个特定目标有多个变体,请添加后缀以消除歧义(例如。:foo_dev:foo_prod:bar_x86:bar_x64
  • _test_unittestTestTests 为后缀的 _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//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 检测到串联的字符串是标签时,会自动融合这些字符串。

因此,最好使用显式字面量字符串,而不是串联或格式化字符串,尤其是在 namedeps 等标签类型属性中。例如,以下 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 = True 明确表示“此测试不稳定”。

与 Python 样式指南的差异

虽然我们的目标是与 Python 样式指南保持兼容,但仍存在一些差异:

  • 没有严格的行长度限制。长注释和长字符串通常会拆分为 79 列,但这不是必需的。不应在代码审核或预提交脚本中强制执行此规则。理由:标签可能会很长,超出此限制。BUILD 文件通常由工具生成或修改,这与行长度限制不太相符。

  • 不支持隐式字符串串联。使用 + 运算符。 理由BUILD 文件包含许多字符串列表。很容易忘记添加逗号,这会导致完全不同的结果。过去,这导致了许多 bug。另请参阅此讨论。

  • 在规则中,关键字实参的 = 符号前后要使用空格。理由:命名实参比 Python 中更常见,并且始终位于单独一行。空格有助于提高可读性。此惯例已存在很长时间,不值得修改所有现有的 BUILD 文件。

  • 默认情况下,使用双引号表示字符串。理由:Python 样式指南中未指定此项,但建议保持一致。因此,我们决定仅使用双引号字符串。许多语言都使用双引号来表示字符串字面量。

  • 在两个顶级定义之间使用一个空行。理由BUILD 文件的结构与典型的 Python 文件不同。它只包含顶级语句。使用单行空白行可缩短 BUILD 文件。