如果 A
在构建或执行时需要 B
,则目标 A
取决于目标 B
。依赖关系引发了对目标的有向无环图 (DAG),它称为依赖关系图。
目标的直接依赖项是指依赖关系图中长度为 1 的路径可访问的其他目标。目标的传递依赖项是指它通过图中任意长度的路径所依赖的目标。
事实上,在构建上下文中有两个依赖关系图,即实际依赖关系图和声明的依赖关系图。大多数情况下,这两个图非常相似,以至于不需要进行这种区分,但它对于下面的讨论非常有用。
实际依赖项和声明的依赖项
如果为了正确构建 X
而必须存在、构建并更新 Y
,目标 X
实际上依赖于目标 Y
。“构建”可以表示生成、处理、编译、链接、归档、压缩、执行或在构建期间经常发生的任何其他类型的任务。
如果在 X
软件包中从 X
到 Y
存在依赖项边缘,则目标 X
已声明对目标 Y
的依赖项。
为了实现正确的构建,实际依赖项图 A 必须是已声明依赖项图 D 的子图。也就是说,A 中的每对直接连接的节点 x --> y
还必须在 D 中直接连接。可以说,D 是 A 的过近似值。
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
的实际依赖项,可以避免此问题。
依赖项类型
大多数构建规则都有三个属性,用于指定不同类型的通用依赖项:srcs
、deps
和 data
。具体说明如下。如需了解详情,请参阅所有规则通用的属性。
许多规则还有一些额外的属性,用于特定于规则的依赖项种类,例如 compiler
或 resources
。如需了解详情,请参阅 Build 百科全书。
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/"]
这似乎很方便,尤其是对于测试而言,因为它允许测试使用目录中的所有数据文件。
但请尽量不要这么做。为了确保在更改后可以正确进行增量重新构建(和重新执行测试),构建系统必须知道作为 build(或测试)输入的完整文件集。在您指定目录后,构建仅在目录本身发生变化(由于文件添加或删除)时执行重新构建,但无法检测到对单个文件的修改,因为这些更改不会影响封闭的目录。您应明确地或者使用 glob()
函数枚举目录中包含的一组文件,而不是将目录指定为构建系统的输入。(使用 **
强制 glob()
为递归。)
建议 - data = glob(["testdata/**"])
遗憾的是,在某些情况下,必须使用目录标签。例如,如果 testdata
目录包含名称不符合标签语法的文件,则明确的文件枚举或使用 glob()
函数会产生“无效标签”错误。在这种情况下,您必须使用目录标签,但要注意上述错误重建的相关风险。
如果必须使用目录标签,请注意,您不能使用相对 ../
路径来引用父软件包,而应使用绝对路径(例如 //data/regression:unittest/.
)。
任何需要使用多个文件的外部规则(如测试)都必须明确声明它依赖于所有文件。您可以使用 filegroup()
在 BUILD
文件中将文件分组:
filegroup(
name = 'my_data',
srcs = glob(['my_unittest_data/*'])
)
然后,您可以在测试中将标签 my_data
引用为数据依赖项。
build 文件 | 可见性 |