依赖项

报告问题 查看来源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Depset 是一种专门的数据结构,用于高效收集目标传递依赖项中的数据。它们是规则处理的重要元素。

deps 的一个显著特点是其高效的时间和空间并集运算。depset 构造函数接受一个元素列表(“直接”)和一个其他 depset 的列表(“传递”),并返回一个 depset,该 depset 表示一个包含所有直接元素和所有传递集的并集的集合。从概念上讲,构造函数会创建一个新图节点,该节点将直接节点和传递节点作为其后继节点。Depset 具有明确定义的排序语义,基于此图的遍历。

depset 的使用示例包括:

  • 存储程序库的所有对象文件的路径,然后可以通过提供程序将这些路径传递给链接器操作。

  • 对于解释型语言,存储包含在可执行文件的 runfiles 中的传递性源文件。

说明和操作

从概念上讲,deps 是一个有向无环图 (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"])

如需检索 deps 的内容,请使用 to_list() 方法。它会返回所有传递元素的列表,不包括重复元素。虽然无法直接检查 DAG 的精确结构,但此结构确实会影响元素的返回顺序。

s = depset(["a", "b", "c"])

print("c" in s.to_list())              # True
print(s.to_list() == ["a", "b", "c"])  # True

与字典中允许的键一样,deps 中允许的项也受到限制。特别是,deps 的内容可能不是可变内容。

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

如需按内容比较 depsets,请将其转换为已排序的列表。

s = depset(["a", "b", "c"])
t = depset(["c", "b", "a"])
print(sorted(s.to_list()) == sorted(t.to_list()))  # True

无法从 depset 中移除元素。如果需要这样做,您必须读出 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 进行遍历。遍历的类型取决于构建 depset 时指定的顺序。Bazel 支持多种顺序非常有用,因为有时工具会关注其输入的顺序。例如,链接器操作可能需要确保,如果 B 依赖于 A,则 A.o 在链接器的命令行上位于 B.o 之前。其他工具可能要求相反。

支持三种遍历顺序:postorderpreordertopological。前两种算法的工作方式与树遍历完全相同,只不过它们在 DAG 上运行并跳过已访问过的节点。第三种顺序是从根到叶的拓扑排序,与前序基本相同,只是共享子级仅在所有父级之后列出。前序遍历和后序遍历按从左到右的顺序进行,但请注意,在每个节点内,直接元素相对于子元素没有顺序。对于拓扑顺序,无法保证从左到右的顺序,即使是“所有父节点在子节点之前”的保证也不适用于 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"]

由于遍历的实现方式,必须在通过构造函数的 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 的传递性来源是 abcdsrcs 字段中的所有 *.foo 文件。为了让 foo_binary 目标了解 d.foo 之外的任何文件,foo_library 目标需要在提供程序中传递这些文件。每个库都会从自己的依赖项中接收提供程序,添加自己的直接来源,并传递具有增强内容的新提供程序。foo_binary 规则的作用相同,只不过它不是返回提供程序,而是使用完整的来源列表来构建操作的命令行。

下面是 foo_libraryfoo_binary 规则的完整实现。

# //depsets/foo.bzl

# A provider with one field, transitive_sources.
foo_files = 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[foo_files].transitive_sources for dep in deps])

def _foo_library_impl(ctx):
  trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
  return [foo_files(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[foo_files].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[foo_files].transitive_sources:
      trans_srcs[file] = True
  for file in srcs:
    trans_srcs[file] = True
  return trans_srcs

这样可以消除重复项,但会使命令行实参的顺序(以及文件的内容)变得不确定,不过仍然是确定性的。

此外,这两种方法的渐近性能都比基于 depset 的方法差。假设 Foo 库存在很长的依赖链。处理每条规则都需要将之前的所有传递性来源复制到新的数据结构中。这意味着,分析单个库或二进制目标的时间和空间成本与其在链中的高度成正比。对于长度为 n 的链 foolib_1 ← foolib_2 ← … ← foolib_n,总费用实际上为 O(n^2)。

一般来说,当您通过传递性依赖项累积信息时,应使用 depset。这有助于确保 build 随着目标图的深度增加而实现良好的扩展。

最后,请务必不要在规则实现中不必要地检索 depset 的内容。在二元规则的末尾调用一次 to_list() 即可,因为总费用仅为 O(n)。当许多非终端目标尝试调用 to_list() 时,就会出现二次方行为。

如需详细了解如何高效使用 depsets,请参阅性能页面。

API 参考文档

如需了解详情,请参阅此处