如果目标 A 在 build 或执行时需要目标 B,则目标 A 依赖于目标 B。依赖于关系会在目标上生成有向无环图 (DAG),称为依赖关系图。
目标的直接依赖项是指在依赖关系图中通过长度为 1 的路径可到达的其他目标。目标的传递依赖项是指通过图中的任意长度的路径依赖于该目标的目标。
实际上,在 build 的上下文中,存在两个依赖关系图:实际依赖关系图和声明的依赖关系图。在大多数情况下,这两个图表非常相似,因此无需区分,但对于下面的讨论很有用。
实际依赖项和已声明的依赖项
如果目标 X 要正确构建,必须存在、构建并保持最新状态,则目标 X 实际上依赖于目标 Y。Y构建可能意味着生成、处理、编译、链接、归档、压缩、执行,或在构建期间经常发生的任何其他类型的任务。
如果目标 X 的软件包中存在从 X 到 Y 的依赖项边,则目标 X 对目标 Y 具有已声明的依赖项。
为了确保构建正确,实际依赖关系图 A 必须是声明的依赖关系图 D 的子图。也就是说,A 中每对直接连接的节点 x --> y 也必须在 D 中直接连接。可以说 D 是 A 的过近似。
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();
}
|
|
|
|
声明的依赖项会过高估计实际依赖项。一切顺利。
2. 添加未声明的依赖项
如果有人向 a 添加了会直接依赖于 c 的代码,但忘记在 build 文件 a/BUILD 中声明该依赖项,就会引入潜在危害。
a / a.in |
|
|---|---|
import b;
import c;
b.foo();
c.garply();
|
|
|
|
|
声明的依赖项不再过度近似实际依赖项。
这可能可以正常构建,因为两个图的传递闭包相等,但掩盖了一个问题: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();
}
|
|
|
|
|
即使在传递性关闭的情况下,声明的依赖关系图现在也只是实际依赖关系的近似下限;构建可能会失败。
通过确保在 BUILD 文件中正确声明第 2 步中从 a 到 c 引入的实际依赖关系,可以避免此问题。
依赖项类型
大多数 build 规则都有三个属性,用于指定不同类型的通用依赖项:srcs、deps 和 data。下面将对此进行说明。如需了解详情,请参阅所有规则共有的属性。
许多规则还具有其他属性,用于指定规则特定的依赖项,例如 compiler 或 resources。如需了解详情,请参阅build 百科全书。
srcs 依赖项
由规则直接使用的文件或输出源文件的规则。
deps 依赖项
指向单独编译的模块的规则,这些模块提供头文件、符号、库、数据等。
data 依赖项
build 目标可能需要一些数据文件才能正常运行。这些数据文件不是源代码,不会影响目标的构建方式。例如,单元测试可能会将函数的输出与文件的内容进行比较。构建单元测试时,您不需要该文件,但在运行测试时需要该文件。这同样适用于在执行期间启动的工具。
构建系统会在隔离的目录中运行测试,其中仅提供列为 data 的文件。因此,如果某个二进制文件/库/测试需要一些文件才能运行,请在 data 中指定这些文件(或包含这些文件的 build 规则)。例如:
# 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 引用为数据依赖项。
| BUILD 文件 | 曝光度 |