依赖项

报告问题 查看源代码

如果在构建或执行时 A 需要 B,目标 A 依赖于目标 B。“依赖”关系会在目标上引发有向无环图 (DAG),称为“依赖关系图”。

目标的直接依赖项是指可通过依赖关系图中长度为 1 的路径访问的其他目标。目标的传递性依赖项是指通过图中任意长度的路径所依赖的目标。

实际上,在 build 上下文中,有两个依赖关系图:实际依赖项图和声明的依赖项图。大多数情况下,这两种图非常相似,因此无需进行区分,但对于以下讨论很有用。

实际声明的依赖项

目标 X 实际依赖于目标 Y,前提是必须存在、构建且最新版本的 Y 才能正确构建 X构建可能是指生成、处理、编译、链接、归档、压缩、执行或执行构建期间通常发生的任何其他种类任务。

如果 X 软件包中存在从 XY 的依赖项,则目标 X 对目标 Y 具有声明的依赖项

对于正确的 build,实际依赖项 A 的图必须是声明的依赖项 D 的子图。也就是说,A 中每对直接连接的节点 x --> y 也必须在 D 中直接连接。可以说,DA 的近似值。

BUILD 文件写入器必须向构建系统明确声明每条规则的所有实际直接依赖项,而不能再声明。

未能遵循此原则会导致未定义的行为:build 可能会失败,但更糟糕的是,build 可能依赖于某些先前的操作,或依赖于目标具有的传递声明依赖项。Bazel 会检查缺少依赖项并报告错误,但并非所有情况下都能完成此检查。

您无需(也不应)尝试列出间接导入的所有内容,即使 A 在执行时需要这样做也是如此。

在目标 X 的构建期间,构建工具将检查 X 依赖项的整个传递关闭,以确保这些目标中的任何更改都反映在最终结果中,并根据需要重新构建中间。

依赖项的传递特性会导致常见错误。有时,一个文件中的代码可能会使用由间接依赖项(即声明的依赖项关系图中传递的而非直接的边缘)提供的代码。间接依赖项不会显示在 BUILD 文件中。由于该规则不直接依赖于提供程序,因此无法跟踪更改,如以下示例所示:

1. 声明的依赖项与实际依赖项匹配

起初,一切都正常。软件包 a 中的代码使用软件包 b 中的代码。 软件包 b 中的代码使用软件包 c 中的代码,因此 a 以传递方式依赖于 c

a/BUILD b/BUILD
rule(
    name = "a",
    srcs = "a.in",
    deps = "//b:b",
)
      
rule(
    name = "b",
    srcs = "b.in",
    deps = "//c:c",
)
      
a / a.in b / b.in
import b;
b.foo();
    
import c;
function foo() {
  c.bar();
}
      
使用箭头连接 a、b 和 c 的已声明依赖项图
声明的依赖关系图
实际依赖关系图,与声明的依赖关系图以及使用 a、b 和 c 连接的箭头匹配
实际依赖项图

声明的依赖项与实际依赖项大体相同。一切正常。

2. 添加未声明的依赖项

当有人向 a 添加代码(这会在 c 上直接创建实际依赖项,但忘记在构建文件 a/BUILD 中声明它)时,就会引入潜在危害。

a / a.in  
        import b;
        import c;
        b.foo();
        c.garply();
      
 
使用箭头连接 a、b 和 c 的已声明依赖项图
声明的依赖关系图
包含连接 a、b 和 c 的箭头的实际依赖关系图。现在,箭头也会将 A 连接到 C。这与声明的依赖关系图不匹配
实际依赖项图

声明的依赖项不再过度估算实际依赖项。这可能会顺利构建,因为两个图的传递闭合相等,但会掩盖一个问题:a 实际依赖于 c,但未声明。

3. 声明的图表和实际的依赖关系图之间的差异

当用户重构 b 以使它不再依赖于 c 时,这种危害都会被显现出来,从而无意中通过没有自己的过错破坏 a

  b/BUILD
 
rule(
    name = "b",
    srcs = "b.in",
    deps = "//d:d",
)
      
  b / b.in
 
      import d;
      function foo() {
        d.baz();
      }
      
已声明的依赖关系图(带有将 a 和 b 连成的箭头)。
                  b 不再连接到 c,这会断开 a 与 c 的连接
声明的依赖关系图
显示连接到 b 和 c,但 b 不再连接到 c 的实际依赖关系图
实际依赖项图

声明的依赖项图现在只是对实际依赖项的近似估算,即使传递关闭,构建也可能会失败。

通过在 BUILD 文件中正确声明第 2 步中引入的从 ac 的实际依赖项,可以避免此问题。

依赖项类型

大多数构建规则有三个属性,用于指定不同类型的通用依赖项:srcsdepsdata。详细说明如下。如需了解详情,请参阅所有规则通用的属性

许多规则还针对特定于规则的种类种类(例如 compilerresources)还有其他属性。如需了解详情,请参阅构建百科全书

srcs 依赖项

直接用于输出源文件的规则的文件。

deps 依赖项

指向单独编译的模块的规则,该模块提供头文件、符号、库、数据等。

data 依赖项

构建目标可能需要一些数据文件才能正常运行。这些数据文件不是源代码:它们不会影响目标的构建方式。例如,单元测试可能会将函数的输出与文件的内容进行比较。构建单元测试时,您无需获取该文件,但运行测试时将需要该文件。这同样适用于在执行期间启动的工具。

构建系统在一个隔离的目录中运行测试,其中仅列出列为 data 的文件。因此,如果二进制文件/库/测试需要运行某些文件,请在 data 中指定这些文件(或包含它们的构建规则)。例如:

# I need a config file from a directory named env:
java_binary(
    name = "setenv",
    ...
    data = [":env/default_env.txt"],
)

# I need test data from another directory
sh_test(
    name = "regtest",
    srcs = ["regtest.sh"],
    data = [
        "//data:file1.txt",
        "//data:file2.txt",
        ...
    ],
)

这些文件是使用相对路径 path/to/data/file 提供的。在测试中,您可以合并测试源目录的路径和工作区相对路径来引用这些文件,例如 ${TEST_SRCDIR}/workspace/path/to/data/file

使用标签引用目录

在查看 BUILD 文件时,您可能会发现一些 data 标签引用了目录。这些标签以 /./ 结尾,如下例所示,您不应使用:

不建议 - data = ["//data/regression:unittest/."]

不建议 - data = ["testdata/."]

不建议 - data = ["testdata/"]

这似乎很方便,尤其是对于测试,因为它允许测试使用目录中的所有数据文件。

但是,请不要这样做。为了确保更改后的增量重新构建(和重新执行测试)正确,构建系统必须知道作为构建(或测试)输入的完整文件集。如果您指定了某个目录,构建系统仅在该目录本身发生更改(由于添加或删除了文件)时才执行重建,但无法检测到对各个文件的修改,因为这些更改不会影响所属目录。 您不应明确指定目录作为构建系统的输入,而应明确或使用 glob() 函数枚举其中包含的文件集。(使用 ** 强制 glob() 进行递归。)

建议 - data = glob(["testdata/**"])

遗憾的是,在某些情况下,必须使用目录标签。例如,如果 testdata 目录包含名称不符合标签语法的文件,则文件的显式枚举或使用 glob() 函数会生成无效标签错误。在这种情况下,您必须使用目录标签,但要注意上述错误重建的风险。

如果您必须使用目录标签,请注意,您不能使用相对 ../ 路径引用父软件包,而是应使用绝对路径,例如 //data/regression:unittest/.

任何需要使用多个文件的外部规则(例如测试)必须明确声明对所有文件的依赖性。您可以使用 filegroup()BUILD 文件中的文件组合在一起:

filegroup(
        name = 'my_data',
        srcs = glob(['my_unittest_data/*'])
)

然后,您就可以在测试中引用标签 my_data 作为数据依赖项。

构建文件 公开范围