依存関係

Depset は、ターゲットの推移的依存関係全体でデータを効率的に 収集するための特殊なデータ構造です。ルール処理に不可欠な要素です。

Depset の定義上の特徴は、時間と空間の効率に優れた結合オペレーションです。 Depset コンストラクタは、要素のリスト(「直接」)と他の depset のリスト(「推移的」)を受け取り、すべての直接要素とすべての推移的セットの結合を含むセットを表す depset を返します。概念的には、コンストラクタは、直接ノードと推移的ノードを後続ノードとして持つ新しいグラフノードを作成します。Depset には、このグラフのトラバーサルに基づく明確な順序付けセマンティクスがあります。

Depset の使用例を次に示します。

  • プログラムのライブラリのすべてのオブジェクト ファイルのパスを保存します。このパスは、プロバイダを介してリンカー アクションに渡すことができます。

  • インタープリタ言語の場合、実行可能ファイルの runfiles に含まれる推移的なソースファイルを保存します。

説明とオペレーション

概念的には、depset は有向非巡回グラフ(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 は参照の等価性を使用します。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

depset の内容を比較するには、並べ替えられたリストに変換します。

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 で複数の順序がサポートされていると便利なのは、ツールによっては入力の順序が重要になる場合があるためです。たとえば、リンカー アクションでは 、BA に依存している場合、リンカーのコマンドラインで A.oB.o より前になるようにする必要があります 。他のツールでは、逆の要件がある場合があります。

postorderpreordertopological の 3 つのトラバーサル順序がサポートされています。最初の 2 つは、DAG で動作し、すでにアクセスしたノードをスキップする点を除いて、tree traversals とまったく同じように動作します。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 が作成されたときに順序を指定する必要があります。この引数を省略すると、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_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) になります。

一般に、推移的な依存関係を通じて情報を収集する場合は、depset を使用する必要があります。これにより、ターゲット グラフが深くなるにつれて、ビルドが適切にスケーリングされます。

最後に、ルール実装で depset の内容を不必要に取得しないことが重要です。バイナリ ルールの最後に to_list() を 1 回呼び出すだけで済みます。全体的なコストは O(n) です。多くの非ターミナル ターゲットが to_list() を呼び出そうとすると、二次的な動作が発生します。

depset を効率的に使用する方法について詳しくは、パフォーマンス ページをご覧ください。

API リファレンス

詳細については、こちらをご覧ください。