Depset は、ターゲットの推移的依存関係全体でデータを効率的に収集するための特殊なデータ構造です。これらはルール処理の重要な要素です。
depset の定義機能は、時間と空間効率の高い結合演算です。depset コンストラクタは、要素のリスト(「直接」)と他の depset のリスト(「推移的」)を受け取り、すべての直接要素とすべての推移的セットの和集合を含むセットを表す depset を返します。概念的には、コンストラクタは、直接ノードと推移的ノードを後継ノードとして持つ新しいグラフノードを作成します。Depset には、このグラフの走査に基づく明確な順序付けセマンティクスがあります。
depset の使用例は次のとおりです。
プログラムのライブラリのすべてのオブジェクト ファイルのパスを保存します。このパスは、プロバイダを介してリンカー アクションに渡すことができます。
実行可能ファイルの runfiles に含まれる推移的なソースファイルを保存する(インタープリタ言語の場合)。
説明とオペレーション
概念的には、depsset は有向非巡回グラフ(DAG)であり、通常はターゲット グラフに似ています。リーフからルートまで構築されます。依存関係チェーン内の各ターゲットは、前のターゲットのコンテンツを読み取ったりコピーしたりすることなく、その上に独自のコンテンツを追加できます。
DAG の各ノードは、直接要素のリストと子ノードのリストを保持します。deps の内容は、すべてのノードの直接要素などの推移的要素です。新しい 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"])
depsset の内容を取得するには、to_list() メソッドを使用します。重複を含まない、すべての推移的要素のリストを返します。DAG の正確な構造を直接検査する方法はありませんが、この構造は要素が返される順序に影響します。
s = depset(["a", "b", "c"])
print("c" in s.to_list()) # True
print(s.to_list() == ["a", "b", "c"]) # True
depsset で許可されるアイテムは、辞書で許可されるキーと同様に制限されます。特に、depsset の内容は変更できません。
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
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 の走査を実行します。トラバーサルの種類は、deps の構築時に指定された順序によって異なります。ツールによっては入力の順序が重要になる場合があるため、Bazel が複数の順序をサポートすることは有用です。たとえば、リンカー アクションでは、B
が A
に依存している場合、リンカーのコマンドラインで A.o
が B.o
の前に来るようにする必要があります。他のツールでは、逆の要件が求められる場合があります。
postorder
、preorder
、topological
の 3 つのトラバーサル順序がサポートされています。最初の 2 つは、DAG で動作し、すでにアクセスしたノードをスキップする点を除いて、ツリー トラバーサルとまったく同じように動作します。3 番目の順序は、ルートからリーフへのトポロジカル ソートとして機能します。これは、共有子をすべての親の後にのみリストするという点を除いて、プリオーダーと基本的に同じです。先行順と後行順は左から右へのトラバーサルとして機能しますが、各ノード内の直接要素には子に対する相対的な順序がないことに注意してください。トポロジカル順序の場合、左から右への順序は保証されません。また、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 が作成されるときに指定する必要があります。この引数が省略された場合、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
ターゲットをビルドします。
パフォーマンス
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
のソースファイルはコマンドラインに 2 回、出力ファイルの内容に 2 回表示されます。
別の方法として、一般的なセットを使用する方法があります。これは、キーが要素で、すべてのキーが 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) になります。
一般的に、推移的依存関係を通じて情報を蓄積する場合は、常に depsets を使用する必要があります。これにより、ターゲット グラフが深くなるにつれてビルドが適切にスケーリングされるようになります。
最後に、ルール実装で depset のコンテンツを不必要に取得しないことが重要です。バイナリ ルールの最後に to_list()
を 1 回呼び出すのは、全体的なコストが O(n) になるだけなので問題ありません。多くの非終端ターゲットが to_list()
を呼び出そうとすると、二次動作が発生します。
depsets を効率的に使用する方法については、パフォーマンス ページをご覧ください。
API リファレンス
詳しくは、こちらをご覧ください。