依赖项集是一种专用的数据结构,可高效地实现 跨目标的传递依赖项收集数据。它们是 规则处理的一个环节。
depset 的特点是可节省时间和空间的并集操作。 depset 构造函数接受元素列表(“direct”)和其他 depsets ("transitive"):会返回一个 depsets,它表示包含所有 以及所有传递集合的并集。从概念上讲, 构造函数会创建一个包含直接节点和传递节点的新图节点 作为其继任者。Depset 具有明确定义的排序语义, 遍历此图。
Depset 的用法示例包括:
存储程序库的所有对象文件的路径, 然后会通过提供程序传递给链接器操作
对于解释型语言,存储 包含在可执行文件的 runfile 中。
说明和操作
从概念上讲,不寻常是一个有向无环图 (DAG),通常看起来 与目标图表类似。它从叶片直至根部构成。 依赖项链中的每个目标都可以在 而无需阅读或复制它们
DAG 中的每个节点都包含一个直接元素列表和一个子节点列表。 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"])
要检索依赖项的内容,请使用 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 与自身相等,但不等于任何值 即使它们具有相同的内容和内部结构,也是如此。
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
您无法从设置中移除元素。如果需要 必须读出设置中的全部内容, 移除并重新构造新的废弃设置。这种方法效率不高。
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 执行遍历。遍历的类型
取决于取消设置时指定的 order
结构。它支持多个订单,这对于 Bazel 非常有用,因为有时
它们关注的是输入的顺序。例如,链接器操作
需要确保如果 B
依赖于 A
,则 A.o
在 B.o
之前
链接器命令行中。其他工具可能有相反的要求。
支持三种遍历顺序:postorder
、preorder
和
topological
。前两个名称与树状结构完全相同
遍历
只不过它们对 DAG 执行操作并跳过已经访问的节点。三阶
其原理是从根到叶的拓扑排序方式,
但共享的子项仅在其所有父项之后列出。
preorder 和 postorder 从左到右遍历,但请注意,在
每个节点的直接元素没有相对于子元素的顺序。用于拓扑
顺序,并没有从左到右保证,甚至
如果存在以下情况,则 all-parents-before-child 保证将不适用
DAG 的不同节点中的重复元素。
# 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"]
鉴于遍历的实现方式,必须在遍历时指定顺序
depset 使用构造函数的 order
关键字参数创建。如果
参数,则 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
的传递源是以下目录中的所有 *.foo
文件:
a
、b
、c
和 d
的 srcs
字段。为了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)。
一般来说,每当你累积 传递依赖项。这有助于确保 随着目标图越来越深,您的 build 也会随之扩展。
最后,千万不要检索
。向 to_list()
发起 1 次通话
因为总体成本只是 O(n)。时间是
当许多非终端目标尝试调用二次行为的 to_list()
时
。
如需详细了解如何高效地使用弃用设置,请参阅性能页面。
API 参考文档
如需了解详情,请参阅此处。