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"])
किसी डेपसेट का कॉन्टेंट वापस पाने के लिए, to_list() तरीके का इस्तेमाल करें. यह ट्रांसिशन एलिमेंट की सूची दिखाता है. इसमें डुप्लीकेट एलिमेंट शामिल नहीं होते. डीएजी के सटीक स्ट्रक्चर की सीधे तौर पर जांच करने का कोई तरीका नहीं है. हालांकि, इस स्ट्रक्चर से उस क्रम पर असर पड़ता है जिसमें एलिमेंट दिखाए जाते हैं.
s = depset(["a", "b", "c"])
print("c" in s.to_list()) # True
print(s.to_list() == ["a", "b", "c"]) # True
डिप्सेट में इस्तेमाल किए जा सकने वाले आइटम पर पाबंदी होती है, ठीक वैसे ही जैसे डिक्शनरी में इस्तेमाल किए जा सकने वाले कीवर्ड पर पाबंदी होती है. खास तौर पर, डेपसेट के कॉन्टेंट में बदलाव नहीं किया जा सकता.
डिप्सेट, रेफ़रंस के बराबर का इस्तेमाल करते हैं: एक डिप्सेट, खुद के बराबर होता है, लेकिन किसी भी दूसरे डिप्सेट के बराबर नहीं होता, भले ही उनका कॉन्टेंट और इंटरनल स्ट्रक्चर एक जैसा हो.
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
डेपसेट के कॉन्टेंट के हिसाब से उनकी तुलना करने के लिए, उन्हें क्रम से लगाई गई सूचियों में बदलें.
s = depset(["a", "b", "c"])
t = depset(["c", "b", "a"])
print(sorted(s.to_list()) == sorted(t.to_list())) # True
किसी डिप्सेट से एलिमेंट नहीं हटाए जा सकते. अगर ज़रूरी हो, तो आपको डिप्सेट का पूरा कॉन्टेंट पढ़ना होगा. इसके बाद, उन एलिमेंट को फ़िल्टर करना होगा जिन्हें आपको हटाना है और नया डिप्सेट बनाना होगा. यह तरीका खास तौर पर असरदार नहीं है.
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
ऑपरेशन, डीएजी पर ट्रैवर्स करता है. ट्रैवर्सल का टाइप, डेपसेट बनाने के समय तय किए गए क्रम पर निर्भर करता है. 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"]
ट्रैवर्सल लागू करने के तरीके की वजह से, कंस्ट्रक्टर के order
कीवर्ड आर्ग्युमेंट के साथ डेपसेट बनाते समय क्रम तय करना ज़रूरी है. अगर इस आर्ग्युमेंट को छोड़ दिया जाता है, तो डेपसेट में default
का खास क्रम होता है. ऐसे में, इसके किसी भी एलिमेंट के क्रम के बारे में कोई गारंटी नहीं दी जा सकती. हालांकि, यह क्रम तय होता है.
पूरा उदाहरण
यह उदाहरण https://github.com/bazelbuild/examples/tree/main/rules/depsets पर उपलब्ध है.
मान लें कि कोई काल्पनिक इंटरप्रेटेड लैंग्वेज Foo है. हर foo_binary
को बनाने के लिए, आपको उन सभी *.foo
फ़ाइलों के बारे में पता होना चाहिए जिन पर यह सीधे या indirectly निर्भर करता है.
# //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.
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
टारगेट बनाएं.
परफ़ॉर्मेंस
डिप्सेट का इस्तेमाल करने की वजह जानने के लिए, इस बात पर ध्यान दें कि अगर 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
इससे डुप्लीकेट आर्ग्युमेंट हट जाते हैं. हालांकि, इससे कमांड लाइन आर्ग्युमेंट (और इसलिए फ़ाइलों का कॉन्टेंट) का क्रम तय नहीं होता.
इसके अलावा, दोनों तरीके, डिपसेट पर आधारित तरीके की तुलना में काफ़ी खराब हैं. ऐसे मामले पर विचार करें जहां Foo लाइब्रेरी पर निर्भरता की लंबी चेन है. हर नियम को प्रोसेस करने के लिए, उससे पहले के सभी ट्रांज़िशन डेटा सोर्स को नए डेटा स्ट्रक्चर में कॉपी करना ज़रूरी है. इसका मतलब है कि किसी एक लाइब्रेरी या बाइनरी टारगेट का विश्लेषण करने में लगने वाला समय और जगह, चेन में उसकी हैसियत के हिसाब से तय होती है. n लंबाई वाली चेन के लिए, foolib_1 ← foolib_2 ← … ← foolib_n, कुल लागत O(n^2) होती है.
आम तौर पर, जब भी ट्रांज़िशन डेपेंडेंसी की मदद से जानकारी इकट्ठा की जा रही हो, तब डिप्सेट का इस्तेमाल किया जाना चाहिए. इससे यह पक्का करने में मदद मिलती है कि आपके टारगेट ग्राफ़ के गहरे होने पर, आपका बिल्ड अच्छी तरह से स्केल हो.
आखिर में, नियम लागू करने के दौरान, डिप्सेट के कॉन्टेंट को ज़रूरत से ज़्यादा बार फिर से न पाएं. बाइनरी नियम के आखिर में to_list()
पर एक कॉल करना ठीक है, क्योंकि कुल लागत सिर्फ़ O(n) है. जब कई नॉन-टर्मिनल टारगेट, to_list()
को कॉल करने की कोशिश करते हैं, तब क्वाड्रैटिक व्यवहार होता है.
डिप्सेट का बेहतर तरीके से इस्तेमाल करने के बारे में ज़्यादा जानने के लिए, परफ़ॉर्मेंस पेज पर जाएं.
एपीआई का संदर्भ
ज़्यादा जानकारी के लिए, कृपया यहां जाएं.