디셋

문제 신고 소스 보기 나이틀리 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Depset은 타겟의 전이 종속 항목에서 데이터를 효율적으로 수집하기 위한 특수 데이터 구조입니다. 이는 규칙 처리의 필수 요소입니다.

deps의 정의 기능은 시간과 공간 효율적인 결합 작업입니다. depsset 생성자는 요소 목록 ('직접')과 다른 depsset 목록 ('전이적')을 허용하고 모든 직접 요소와 모든 전이적 집합의 합집합을 포함하는 집합을 나타내는 depsset을 반환합니다. 개념적으로 생성자는 직접 및 전이 노드를 후속 노드로 갖는 새 그래프 노드를 만듭니다. deps는 이 그래프의 순회에 기반한 잘 정의된 순서 지정 시맨틱스를 갖습니다.

depsets의 사용 예는 다음과 같습니다.

  • 프로그램 라이브러리의 모든 객체 파일 경로를 저장합니다. 그러면 제공자를 통해 링커 작업에 전달될 수 있습니다.

  • 실행 파일의 runfile에 포함된 전이 소스 파일을 저장하는 해석된 언어의 경우

설명 및 작업

개념적으로 depset은 일반적으로 타겟 그래프와 유사한 방향성 비순환 그래프 (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"])

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에 허용되는 항목은 딕셔너리에 허용되는 키와 마찬가지로 제한됩니다. 특히 depset 콘텐츠는 변경할 수 없습니다.

Depsets는 참조 동등성을 사용합니다. 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

deps에서 요소를 삭제할 수는 없습니다. 이 작업이 필요한 경우 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.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"]

트래버설이 구현되는 방식 때문에 순서는 생성자의 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의 전달 종속성 소스는 a, b, c, dsrcs 필드에 있는 모든 *.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 타겟을 빌드하여 테스트할 수 있습니다.

성능

deps를 사용하는 동기를 확인하려면 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)입니다.

일반적으로 전이 종속 항목을 통해 정보를 누적하는 경우 depsets를 사용해야 합니다. 이렇게 하면 타겟 그래프가 더 깊어질 때 빌드가 잘 확장됩니다.

마지막으로 규칙 구현에서 불필요하게 depset의 콘텐츠를 가져오지 않는 것이 중요합니다. 전체 비용이 O(n)에 불과하므로 바이너리 규칙의 끝에서 to_list()를 한 번 호출해도 됩니다. 많은 비터미널 타겟이 to_list()를 호출하려고 할 때 2차 동작이 발생합니다.

depsets를 효율적으로 사용하는 방법에 관한 자세한 내용은 성능 페이지를 참고하세요.

API 참조

자세한 내용은 여기를 참고하세요.