是什么导致 Bazel 中相同目标的重复构建?

What is causing duplicate builds of same target in Bazel?

我们在 Bazel 中看到同一目标的重复构建,想知道是什么原因造成的。

这是一个示例输出:

[52,715 / 55,135] 12 action running
    Bazel package: some-pkg - Target: a_target - Generating files at bazel-out/host/bin/some-pkg/a_target_generate [for host]; 264s remote-cache, processwrapper-sandbox
    Bazel package: some-pkg - Target: a_target - Generating files at bazel-out/k8-fastbuild/bin/some-pkg/a_target_generate; 264s remote-cache, processwrapper-sandbox
    ...

我们无法确定问题所在。看起来这只发生在 Linux 而不是在 Mac 上。

目标 a_target 是一个 custom_rule 目标。它应该与平台无关。

custom_rule = rule(
    attrs = dict(
       ...
        _custom_rule_java_binary = attr.label(
            cfg = "host",
            default = Label("//tools/bazel/build/rules/custom-rule:custom_rule_bin"),
            executable = True,
        ),
        _singlejar = attr.label(
            cfg = "host",
            default = Label("@bazel_tools//tools/jdk:singlejar"),
            executable = True,
            allow_files = True,
        ),
    ),
    implementation = ...,
)

custom_rule_bin定义如下:

java_library(
    name = "custom_rule",
    srcs = glob(["src/main/java/**/*.java"]),
    deps = [
        ...,
    ],
)

java_binary(
    name = "custom_rule_bin",
    visibility = ["//visibility:public"],
    main_class = "...",
    runtime_deps = [
        "@org_slf4j_simple",
        ":custom_rule",
        "//some-pkg:some_pkg", # same some-pkg where a_target is built twice
    ],
)

不同的是,一个说“for host”,另一个不说。有人知道额外的“for host”构建是什么吗?

我确实觉得它与自定义规则的 cfg 属性有某种关系。这可能来自一些示例代码。我们在所有生成代码的规则上使用相同的值。这个自定义规则很特殊,因为它需要来自 Bazel 正在构建的应用程序的代码 运行 并生成额外的代码。

任何见解都可以理解为什么 host 是错误的以及什么是正确的值。

任何ideas/tips如何调试这个?

首先,需要注意的是主机配置大多已被弃用,通常首选“exec”。一些相关信息在这里:https://bazel.build/rules/rules#configurations.

发生的事情是该目标在多个配置中被依赖,因此 bazel 将在每个配置中构建该目标。你可以使用 cquery 来弄清楚发生了什么

作为一个非常简单的例子:

genrule(
  name = "gen_bin",
  outs = ["bin"],
  srcs = [":gen_lib"],
  exec_tools = [":gen_tool"],
  cmd = "touch $@",
)

genrule(
  name = "gen_tool",
  outs = ["tool"],
  srcs = [":gen_lib"],
  cmd = "touch $@",
)

genrule(
  name = "gen_lib",
  outs = ["lib"],
  cmd = "touch $@; sleep 10",
)

构建 bin,bazel 运行 gen_lib genrule 两次(并行):

$ bazel build bin
INFO: Analyzed target //:bin (5 packages loaded, 16 targets configured).
INFO: Found 1 target...
[1 / 5] 2 actions running
    Executing genrule //:gen_lib; 1s linux-sandbox
    Executing genrule //:gen_lib; 1s linux-sandbox

bazel config 给出当前在 in-memory 构建图中的配置:

$ bazel config
Available configurations:
5b39bc31deb1f1d37f1f858e7eec3964394eacce5bede4456dd59d417af4a6e9 (exec)
723da02ae6d0c5577e98242c8f06ca1bd1c6d7b295c97345ac31b844bfe8f79c
8960923b9e7dc13418be101268efd8e57d80283213d18174705345598b699c6b
fd805cc1de357c04c7abac1b40bae600e3d9ee56a8d17af0c28b5031ca09bfb9 (host)

然后 cquery:

$ bazel cquery "rdeps(//..., gen_lib)"
INFO: Analyzed 3 targets (0 packages loaded, 1 target configured).
INFO: Found 3 targets...
//:gen_lib (5b39bc3)
//:gen_lib (8960923)
//:gen_tool (5b39bc3)
//:gen_bin (8960923)
//:gen_tool (8960923)
INFO: Elapsed time: 0.052s
INFO: 0 processes.
INFO: Build completed successfully, 0 total actions

(cquery 给出配置哈希的前 7 位)

--output=graph给出了一个点图,比较有用:

$ bazel cquery "rdeps(//..., gen_lib)" --output=graph > /tmp/graph
$ xdot /tmp/graph

所以gen_bin在目标配置(8960923)中,它依赖于gen_lib,所以gen_lib也会在目标配置中构建

gen_bin 还通过 exec_tools 属性依赖于 gen_tool,并且 exec_tools 在 exec 配置 (5b39bc3) 中构建所有内容。

gen_tool也依赖于gen_lib,由于gen_tool在exec配置中,所以在exec配置中内置了gen_lib的一个版本。

(在输出的目标配置中还有另一个版本的 gen_tool,这是在 rdeps() 的“universe”参数中使用 //... 的产物,因为 //... 将捕获每个目标。类似地,执行 bazel build //... 会导致 gen_tool 被构建两次。)