依赖项集是一种专用数据结构,用于跨目标的传递依赖项高效收集数据。它们是规则处理的重要元素
depset 的特点是可节省时间和空间的并集操作。depset 构造函数接受一个元素列表(“direct”)和其他 Depset (“transitive”)列表,并返回表示包含所有直接元素和所有传递集并集的 Depset。从概念上讲,构造函数会创建一个新的图节点,将直接节点和传递节点作为其后继节点。Depset 具有基于此图的遍历而明确定义的排序语义。
Depset 的用法示例包括:
存储程序库的所有对象文件的路径,然后通过提供程序将其传递给链接器操作。
对于解释型语言,存储可执行文件的 runfile 中包含的传递源文件。
说明和操作
从概念上讲,不寻常是一个有向无环图 (DAG),通常看起来与目标图类似。它从叶片构造而成,直至根部。依赖项链中的每个目标都可以在上一个目标的基础上添加自己的内容,而无需读取或复制这些内容。
DAG 中的每个节点都包含一个直接元素列表和一个子节点列表。Depset 的内容是传递元素,例如所有节点的直接元素。您可以使用 depset 构造函数创建新的 depset:它接受一个直接元素列表和另一个子节点列表。
s = depset(["a", "b", "c"])
t = depset(["d", "e"], transitive = [s])
print(s) # depset(["a", "b", "c"])
print(t) # depset(["d", "e", "a", "b", "c"])
如需检索 Depset 的内容,请使用 to_list() 方法。它会返回所有传递元素(不包括重复项)的列表。您无法直接检查 DAG 的精确结构,但此结构确实会影响元素返回的顺序。
s = depset(["a", "b", "c"])
print("c" in s.to_list()) # True
print(s.to_list() == ["a", "b", "c"]) # True
废弃集中允许的项受到限制,就像字典中允许的键受到限制一样。具体而言,未设置的内容可能无法更改。
Depset 使用引用相等:depset 与自身相等,但不等于任何其他 Depset,即使它们具有相同的内容和相同的内部结构。
s = depset(["a", "b", "c"])
t = s
print(s == t) # True
t = depset(["a", "b", "c"])
print(s == t) # False
d = {}
d[s] = None
d[t] = None
print(len(d)) # 2
如需按内容比较依赖项,请将其转换为有序列表。
s = depset(["a", "b", "c"])
t = depset(["c", "b", "a"])
print(sorted(s.to_list()) == sorted(t.to_list())) # True
您无法从设置中移除元素。如果需要,您必须读出 Depset 的全部内容,过滤要移除的元素,并重建新的 Depset。这种方法效率不高。
s = depset(["a", "b", "c"])
t = depset(["b", "c"])
# Compute set difference s - t. Precompute t.to_list() so it's not done
# in a loop, and convert it to a dictionary for fast membership tests.
t_items = {e: None for e in t.to_list()}
diff_items = [x for x in s.to_list() if x not in t_items]
# Convert back to depset if it's still going to be used for union operations.
s = depset(diff_items)
print(s) # depset(["a"])
订单
to_list
操作会对 DAG 执行遍历。遍历的类型取决于构建出发集时指定的顺序。Bazel 支持多个订单是很有用的,因为有时工具会关注输入的顺序。例如,链接器操作可能需要确保:在链接器命令行中,如果 B
依赖于 A
,则 A.o
位于 B.o
之前。其他工具可能有相反的要求。
支持三种遍历顺序:postorder
、preorder
和 topological
。前两种方法的工作方式与树遍历完全相同,只不过它们对 DAG 进行操作并跳过已经访问的节点。第三步执行从根到叶的拓扑排序,与预排序基本相同,只不过共享的子项仅在其所有父项之后列出。preorder 和 postorder 从左到右遍历,但请注意,在每个节点中,直接元素没有相对于子元素的顺序。对于拓扑顺序,没有从左到右的保证;如果 DAG 的不同节点中存在重复元素,则甚至“all-parents-before-child”保证也不适用。
# This demonstrates different traversal orders.
def create(order):
cd = depset(["c", "d"], order = order)
gh = depset(["g", "h"], order = order)
return depset(["a", "b", "e", "f"], transitive = [cd, gh], order = order)
print(create("postorder").to_list()) # ["c", "d", "g", "h", "a", "b", "e", "f"]
print(create("preorder").to_list()) # ["a", "b", "e", "f", "c", "d", "g", "h"]
# This demonstrates different orders on a diamond graph.
def create(order):
a = depset(["a"], order=order)
b = depset(["b"], transitive = [a], order = order)
c = depset(["c"], transitive = [a], order = order)
d = depset(["d"], transitive = [b, c], order = order)
return d
print(create("postorder").to_list()) # ["a", "b", "c", "d"]
print(create("preorder").to_list()) # ["d", "b", "a", "c"]
print(create("topological").to_list()) # ["d", "b", "c", "a"]
由于遍历的实现方式,必须使用构造函数的 order
关键字参数在创建 Depset 时指定顺序。如果省略此参数,则 depset 具有特殊的 default
顺序,在这种情况下,其任何元素的顺序都无法保证(除非它是确定性的)。
完整示例
如需查看此示例,请访问 https://github.com/bazelbuild/examples/tree/main/rules/depsets。
假设有一种假设的解释语言 Foo。为了构建每个 foo_binary
,您需要知道它直接或间接依赖的所有 *.foo
文件。
# //depsets:BUILD
load(":foo.bzl", "foo_library", "foo_binary")
# Our hypothetical Foo compiler.
py_binary(
name = "foocc",
srcs = ["foocc.py"],
)
foo_library(
name = "a",
srcs = ["a.foo", "a_impl.foo"],
)
foo_library(
name = "b",
srcs = ["b.foo", "b_impl.foo"],
deps = [":a"],
)
foo_library(
name = "c",
srcs = ["c.foo", "c_impl.foo"],
deps = [":a"],
)
foo_binary(
name = "d",
srcs = ["d.foo"],
deps = [":b", ":c"],
)
# //depsets:foocc.py
# "Foo compiler" that just concatenates its inputs to form its output.
import sys
if __name__ == "__main__":
assert len(sys.argv) >= 1
output = open(sys.argv[1], "wt")
for path in sys.argv[2:]:
input = open(path, "rt")
output.write(input.read())
在这里,二进制 d
的传递源是 a
、b
、c
和 d
的 srcs
字段中的所有 *.foo
文件。为了让 foo_binary
目标知道 d.foo
之外的任何文件,foo_library
目标需要将它们传入提供程序。每个库都从自己的依赖项接收提供程序,添加自己的直接来源,并通过增强的内容传递新的提供程序。foo_binary
规则的作用相同,只是它不会返回提供程序,而是使用完整的来源列表为操作构建命令行。
下面是 foo_library
和 foo_binary
规则的完整实现。
# //depsets/foo.bzl
# A provider with one field, transitive_sources.
FooFiles = provider(fields = ["transitive_sources"])
def get_transitive_srcs(srcs, deps):
"""Obtain the source files for a target and its transitive dependencies.
Args:
srcs: a list of source files
deps: a list of targets that are direct dependencies
Returns:
a collection of the transitive sources
"""
return depset(
srcs,
transitive = [dep[FooFiles].transitive_sources for dep in deps])
def _foo_library_impl(ctx):
trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
return [FooFiles(transitive_sources=trans_srcs)]
foo_library = rule(
implementation = _foo_library_impl,
attrs = {
"srcs": attr.label_list(allow_files=True),
"deps": attr.label_list(),
},
)
def _foo_binary_impl(ctx):
foocc = ctx.executable._foocc
out = ctx.outputs.out
trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
srcs_list = trans_srcs.to_list()
ctx.actions.run(executable = foocc,
arguments = [out.path] + [src.path for src in srcs_list],
inputs = srcs_list + [foocc],
outputs = [out])
foo_binary = rule(
implementation = _foo_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files=True),
"deps": attr.label_list(),
"_foocc": attr.label(default=Label("//depsets:foocc"),
allow_files=True, executable=True, cfg="host")
},
outputs = {"out": "%{name}.out"},
)
如需进行测试,您可以将这些文件复制到新的软件包中,相应地重命名标签,创建包含虚拟内容的源 *.foo
文件,然后构建 d
目标。
性能
如需了解使用 Depset 的动机,请思考如果 get_transitive_srcs()
将其来源收集在列表中,会发生什么情况。
def get_transitive_srcs(srcs, deps):
trans_srcs = []
for dep in deps:
trans_srcs += dep[FooFiles].transitive_sources
trans_srcs += srcs
return trans_srcs
这不考虑重复项,因此 a
的源文件将在命令行中出现两次,并在输出文件的内容中出现两次。
另一种方法是使用常规集合,该集合可以通过字典来模拟,其中键是元素,所有键都会映射到 True
。
def get_transitive_srcs(srcs, deps):
trans_srcs = {}
for dep in deps:
for file in dep[FooFiles].transitive_sources:
trans_srcs[file] = True
for file in srcs:
trans_srcs[file] = True
return trans_srcs
这样会消除重复项,但会使命令行参数(以及文件内容)的顺序变为未指定,但仍然具有确定性。
此外,与基于情绪的方法相比,这两种方法的效果逐渐变差。考虑 Foo 库存在长依赖项链的情况。处理每条规则都需要将在其前面的所有传递源复制到新的数据结构中。这意味着,分析单个库或二进制文件目标所需的时间和空间成本与其在链中的高度成正比。对于长度为 n 的链,foolib_1 ← foolib_2 ← ... ← foolib_n,总成本实际上为 O(n^2)。
一般来说,每当您通过传递依赖项累积信息时,都应使用 Depset。这有助于确保您的 build 能够随着目标图越来越深入而扩容。
最后,切勿在规则实现中不必要地检索取消设置的内容。在二元规则中对 to_list()
进行一次最后调用是可以的,因为总费用仅为 O(n)。每当许多非终端目标都尝试调用 to_list()
时,就会出现二次行为。
如需详细了解如何高效地使用弃用设置,请参阅性能页面。
API 参考文档
如需了解详情,请参阅此处。