Depset 是一種專用資料結構,可有效收集目標遞移依附元件的資料。這是規則處理作業的重要元素。
depsset 的定義特徵是其時間和空間效率高的聯集運算。depsset 建構函式會接受元素清單 (「直接」) 和其他 depsset 清單 (「遞移」),並傳回代表一組的 depsset,其中包含所有直接元素和所有遞移集合的聯集。從概念上來說,建構函式會建立新的圖形節點,並將直接和遞移節點做為後續節點。根據這個圖表的遍歷,depsset 具有明確定義的排序語意。
depsset 的用途範例包括:
儲存程式庫所有物件檔案的路徑,然後透過供應器傳遞至連結器動作。
對於解譯語言,儲存可執行檔的 Runfile 中包含的遞移來源檔案。
說明和作業
從概念上來說,depsset 是有向無環圖 (DAG),通常與目標圖形類似。從葉節點建構至根節點。 依附鏈中的每個目標都可以在前一個目標的內容上新增自己的內容,不必讀取或複製這些內容。
DAG 中的每個節點都包含直接元素清單和子節點清單。depsset 的內容是遞移元素,例如所有節點的直接元素。可以使用 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
depsset 中允許的項目受到限制,就像字典中允許的鍵受到限制一樣。特別是,deps 內容可能無法變更。
Depsset 使用參照等式:depsset 等於自身,但不等於任何其他 depsset,即使兩者內容和內部結構相同也一樣。
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
無法從 depsset 移除元素。如有需要,您必須讀出整個 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
之前。其他工具可能會有相反的規定。
系統支援三種遍歷順序:postorder
、preorder
和 topological
。前兩項作業與樹狀結構遍歷完全相同,差別在於這兩項作業適用於 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"]
由於遍歷的實作方式,建立 deps 集合時,必須使用建構函式的 order
關鍵字引數指定順序。如果省略這個引數,deps 會採用特殊的 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.
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
目標。
成效
如要瞭解使用 depsets 的動機,請考慮如果 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
這樣做可以移除重複項目,但會導致指令列引數的順序 (以及檔案內容) 未指定,不過仍具決定性。
此外,這兩種方法的漸近效能都比基於 deps 的方法差。假設 Foo 程式庫有很長的依附元件鏈結。處理每項規則時,都需要將之前的所有遞移來源複製到新的資料結構中。也就是說,分析個別程式庫或二進位目標的時間和空間成本,與其在鏈結中的高度成正比。對於長度為 n 的鏈結 foolib_1 ← foolib_2 ← … ← foolib_n,整體成本實際上為 O(n^2)。
一般來說,只要透過遞移依附元件累積資訊,就應使用 depsets。這有助於確保建構作業能隨著目標圖表深度增加而妥善擴展。
最後,請務必不要在規則實作中不必要地擷取 depsets 的內容。在二進位規則的結尾呼叫 to_list()
一次即可,因為整體費用僅為 O(n)。當許多非終端目標嘗試呼叫 to_list()
時,就會發生二次方行為。
如要進一步瞭解如何有效使用 depsets,請參閱「效能」頁面。
API 參考資料
詳情請參閱這篇文章。