डेपट्स

डिपसेट खास तरह का डेटा स्ट्रक्चर है. इसकी मदद से, किसी टारगेट की ट्रांज़िटिव डिपेंडेंसी के डेटा को कुशलता से इकट्ठा किया जा सकता है. ये नियम की प्रोसेसिंग का अहम हिस्सा हैं.

Depset की खास बात यह है कि इसकी मदद से, डेटा को तेज़ी से और कम जगह में मर्ज किया जा सकता है. Depset कंस्ट्रक्टर, एलिमेंट ("डायरेक्ट") की सूची और अन्य depsets ("ट्रांज़िटिव") की सूची स्वीकार करता है. इसके बाद, यह एक depset दिखाता है. इसमें सभी डायरेक्ट एलिमेंट और सभी ट्रांज़िटिव सेट का यूनियन शामिल होता है. कंस्ट्रक्टर, एक नया ग्राफ़ नोड बनाता है. इसमें डायरेक्ट और ट्रांज़िटिव नोड, सक्सेसर के तौर पर शामिल होते हैं. Depsets में, ऑर्डरिंग सिमैंटिक्स तय किया जाता है. यह सिमैंटिक्स, इस ग्राफ़ के ट्रैवर्सल पर आधारित होता है.

Depsets के इस्तेमाल के कुछ उदाहरण:

  • किसी प्रोग्राम की लाइब्रेरी के सभी ऑब्जेक्ट फ़ाइलों के पाथ सेव करना. इसके बाद, इन्हें किसी प्रोवाइडर की मदद से, लिंकर ऐक्शन को पास किया जा सकता है.

  • इंटरप्रेट की गई भाषा के लिए, ट्रांज़िटिव सोर्स फ़ाइलें सेव करना. ये फ़ाइलें, किसी एक्ज़ीक्यूटेबल की रनफ़ाइल में शामिल होती हैं.

जानकारी और कार्रवाइयां

Depset, डायरेक्टेड एसाइक्लिक ग्राफ़ (डीएजी) होता है. यह आम तौर पर, टारगेट ग्राफ़ जैसा दिखता है. इसे लीफ़ से रूट तक बनाया जाता है. डिपेंडेंसी चेन में मौजूद हर टारगेट, अपने कॉन्टेंट को पिछले कॉन्टेंट में जोड़ सकता है. इसके लिए, उसे पिछले कॉन्टेंट को पढ़ने या कॉपी करने की ज़रूरत नहीं होती.

डीएजी में मौजूद हर नोड में, डायरेक्ट एलिमेंट की सूची और चाइल्ड नोड की सूची होती है. 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() तरीके का इस्तेमाल करें. यह डुप्लीकेट एलिमेंट को छोड़कर, सभी ट्रांज़िटिव एलिमेंट की सूची दिखाता है. डीएजी के सटीक स्ट्रक्चर की सीधे तौर पर जांच नहीं की जा सकती. हालांकि, इस स्ट्रक्चर से यह तय होता है कि एलिमेंट किस क्रम में दिखाए जाएंगे.

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

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

किसी depset में शामिल किए जा सकने वाले आइटम की संख्या सीमित होती है. ठीक उसी तरह, डिक्शनरी में शामिल की जा सकने वाली कुंजियों की संख्या सीमित होती है. खास तौर पर, 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

किसी 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 कार्रवाई, डीएजी पर ट्रैवर्सल करती है. ट्रैवर्सल का टाइप, उस ऑर्डर पर निर्भर करता है जिसे depset बनाते समय तय किया गया था. Bazel के लिए, कई ऑर्डर को सपोर्ट करना ज़रूरी है, क्योंकि कभी-कभी टूल को अपने इनपुट के ऑर्डर के बारे में जानकारी चाहिए होती है. उदाहरण के लिए, लिंकर ऐक्शन को यह पक्का करना पड़ सकता है कि अगर B A पर निर्भर है, तो लिंकर की कमांड लाइन पर A.o B.o से पहले दिखे. अन्य टूल के लिए, इसके उलट ज़रूरत हो सकती है.

तीन ट्रैवर्सल ऑर्डर काम करते हैं: postorder, preorder, और topological. पहले दो ऑर्डर, ट्री ट्रैवर्सल की तरह ही काम करते हैं. हालांकि, ये डीएजी पर काम करते हैं और पहले से देखे जा चुके नोड को छोड़ देते हैं. तीसरा ऑर्डर, रूट से लीफ़ तक टोपोलॉजिकल सॉर्ट के तौर पर काम करता है. यह असल में, प्रीऑर्डर जैसा ही होता है. हालांकि, शेयर किए गए चाइल्ड, अपने सभी पैरंट के बाद ही दिखते हैं. प्रीऑर्डर और पोस्टऑर्डर, बाएं से दाएं ट्रैवर्सल के तौर पर काम करते हैं. हालांकि, ध्यान दें कि हर नोड में, डायरेक्ट एलिमेंट का क्रम, चाइल्ड के क्रम से अलग होता है. टॉपोलॉजिकल ऑर्डर के लिए, बाएं से दाएं क्रम की कोई गारंटी नहीं होती. साथ ही, अगर डीएजी के अलग-अलग नोड में डुप्लीकेट एलिमेंट हैं, तो पैरंट के बाद चाइल्ड दिखने की गारंटी भी लागू नहीं होती.

# 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"]

ट्रैवर्सल को लागू करने के तरीके की वजह से, depset बनाते समय ऑर्डर तय करना ज़रूरी है. इसके लिए, कंस्ट्रक्टर के order कीवर्ड आर्ग्युमेंट का इस्तेमाल करें. अगर इस आर्ग्युमेंट को छोड़ दिया जाता है, तो 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 के ट्रांज़िटिव सोर्स, *.foo फ़ील्ड में मौजूद सभी *.foo फ़ाइलें हैं srcs के a, b, c, और d. foo_binary टारगेट को d.foo के अलावा किसी अन्य फ़ाइल के बारे में बताने के लिए, foo_library टारगेट को उन्हें किसी प्रोवाइडर में पास करना होगा. हर लाइब्रेरी को अपनी डिपेंडेंसी से प्रोवाइडर मिलते हैं. इसके बाद, वह अपने सोर्स जोड़ती है और बढ़े हुए कॉन्टेंट के साथ नया प्रोवाइडर पास करती है. foo_binary नियम भी ऐसा ही करता है. हालांकि, प्रोवाइडर दिखाने के बजाय, यह किसी कार्रवाई के लिए कमांड लाइन बनाने के लिए, सोर्स की पूरी सूची का इस्तेमाल करता है.

foo_library और foo_binary नियमों को लागू करने का पूरा तरीका यहां दिया गया है.

# //depsets/foo.bzl

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

def _foo_library_impl(ctx):
  trans_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
  return [FooFiles(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[FooFiles].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[FooFiles].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() को एक बार कॉल करना ठीक है, क्योंकि कुल लागत सिर्फ़ O(n) होती है. जब कई नॉन-टर्मिनल टारगेट, to_list() को कॉल करने की कोशिश करते हैं, तब क्वाड्रेटिक बिहेवियर होता है.

Depsets का सही तरीके से इस्तेमाल करने के बारे में ज़्यादा जानने के लिए, परफ़ॉर्मेंस पेज देखें.

एपीआई का संदर्भ

ज़्यादा जानकारी के लिए, यहां देखें.