इस दस्तावेज़ में, कोड बेस और Bazel के स्ट्रक्चर के बारे में बताया गया है. यह सुविधा उन लोगों के लिए है जो Bazel में योगदान देना चाहते हैं. यह सुविधा सामान्य उपयोगकर्ताओं के लिए नहीं है.
परिचय
Bazel का कोड बेस बहुत बड़ा है. इसमें ~350KLOC प्रोडक्शन कोड और ~260 KLOC टेस्ट कोड शामिल है. किसी को भी इसके बारे में पूरी जानकारी नहीं है. हर व्यक्ति को अपने काम से जुड़ी जानकारी तो होती है, लेकिन बहुत कम लोगों को यह पता होता है कि हर दिशा में पहाड़ियों के पीछे क्या है.
इस दस्तावेज़ में कोड बेस की खास जानकारी दी गई है, ताकि जो लोग इस यात्रा के बीच में हैं वे खुद को अंधेरे जंगल में न पाएं और उन्हें सीधा रास्ता न दिखे. इससे उन्हें इस पर काम करना शुरू करने में आसानी होगी.
Bazel के सोर्स कोड का सार्वजनिक वर्शन, GitHub पर github.com/bazelbuild/bazel पर उपलब्ध है. यह “भरोसेमंद सोर्स” नहीं है. यह Google के इंटरनल सोर्स ट्री से लिया गया है. इसमें ऐसी अतिरिक्त सुविधाएं शामिल हैं जो Google के बाहर काम की नहीं हैं. हमारा मुख्य लक्ष्य, GitHub को भरोसेमंद सोर्स बनाना है.
GitHub के पुल के अनुरोध की सामान्य प्रोसेस के ज़रिए योगदान स्वीकार किए जाते हैं. इसके बाद, Google कर्मचारी उन्हें मैन्युअल तरीके से इंटरनल सोर्स ट्री में इंपोर्ट करता है. इसके बाद, उन्हें वापस GitHub पर एक्सपोर्ट कर दिया जाता है.
क्लाइंट/सर्वर आर्किटेक्चर
Bazel का ज़्यादातर हिस्सा, सर्वर प्रोसेस में होता है. यह प्रोसेस, बिल्ड के बीच RAM में रहती है. इससे Bazel, बिल्ड के बीच की स्थिति को बनाए रख पाता है.
इसलिए, Bazel कमांड लाइन में दो तरह के विकल्प होते हैं: स्टार्टअप और कमांड. इस तरह की कमांड लाइन में:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
कुछ विकल्प (--host_jvm_args=) उस कमांड के नाम से पहले होते हैं जिसे चलाना है. वहीं, कुछ विकल्प (-c opt) बाद में होते हैं. पहले वाले विकल्प को "स्टार्टअप विकल्प" कहा जाता है. यह सर्वर प्रोसेस को पूरी तरह से प्रभावित करता है. वहीं, बाद वाले विकल्प को "कमांड विकल्प" कहा जाता है. यह सिर्फ़ एक कमांड को प्रभावित करता है.
हर सर्वर इंस्टेंस में एक ही सोर्स ट्री ("वर्कस्पेस") होता है. साथ ही, हर वर्कस्पेस में आम तौर पर एक ही ऐक्टिव सर्वर इंस्टेंस होता है. कस्टम आउटपुट बेस तय करके, इस समस्या से बचा जा सकता है. ज़्यादा जानकारी के लिए, "डायरेक्ट्री लेआउट" सेक्शन देखें.
Bazel को एक ही ELF एक्ज़ीक्यूटेबल के तौर पर डिस्ट्रिब्यूट किया जाता है. यह एक मान्य .zip फ़ाइल भी है.
bazel टाइप करने पर, C++ में लागू किया गया ऊपर दिया गया ELF एक्ज़ीक्यूटेबल (क्लाइंट) कंट्रोल में आ जाता है. यह कुकी, सर्वर प्रोसेस को सेट अप करती है. इसके लिए, यह तरीका अपनाती है:
- इस कुकी से यह पता चलता है कि क्या यह पहले ही खुद को एक्सट्रैक्ट कर चुकी है. अगर ऐसा नहीं है, तो वह ऐसा करता है. सर्वर का इस्तेमाल यहीं से शुरू होता है.
- यह कुकी यह जांच करती है कि कोई ऐसा सर्वर इंस्टेंस चालू है या नहीं जो काम करता हो: वह चल रहा हो,
उसमें स्टार्टअप के सही विकल्प हों, और वह सही वर्कस्पेस डायरेक्ट्री का इस्तेमाल करता हो. यह
$OUTPUT_BASE/serverडायरेक्ट्री में मौजूद लॉक फ़ाइल के ज़रिए, चालू सर्वर का पता लगाता है. इस फ़ाइल में वह पोर्ट होता है जिस पर सर्वर काम कर रहा होता है. - ज़रूरत पड़ने पर, पुराने सर्वर की प्रोसेस बंद कर देता है
- ज़रूरत पड़ने पर, नई सर्वर प्रोसेस शुरू करता है
जब सर्वर प्रोसेस तैयार हो जाती है, तब gRPC इंटरफ़ेस के ज़रिए उस कमांड के बारे में बताया जाता है जिसे चलाना है. इसके बाद, Bazel का आउटपुट वापस टर्मिनल पर भेज दिया जाता है. एक बार में सिर्फ़ एक निर्देश दिया जा सकता है. इसे C++ और Java में मौजूद हिस्सों के साथ, लॉकिंग के बेहतर तरीके का इस्तेमाल करके लागू किया जाता है. एक साथ कई निर्देश चलाने के लिए कुछ इन्फ़्रास्ट्रक्चर मौजूद है, क्योंकि bazel version को किसी दूसरे निर्देश के साथ एक साथ न चला पाना थोड़ा शर्मनाक है. BlazeModule का लाइफ़ साइकल और BlazeRuntime में मौजूद कुछ स्टेट, मुख्य रुकावटें हैं.
कमांड के आखिर में, Bazel सर्वर, क्लाइंट को वह एक्ज़िट कोड भेजता है जिसे क्लाइंट को वापस भेजना चाहिए. एक दिलचस्प बात यह है कि bazel run को लागू किया गया है: इस कमांड का काम, Bazel की ओर से अभी-अभी बनाए गए किसी आइटम को चलाना है. हालांकि, यह सर्वर प्रोसेस से ऐसा नहीं कर सकता, क्योंकि इसके पास टर्मिनल नहीं है. इसलिए, यह क्लाइंट को बताता है कि उसे कौनसी बाइनरी ujexec() करनी चाहिए और किन तर्कों के साथ.
जब कोई व्यक्ति Ctrl-C दबाता है, तो क्लाइंट इसे gRPC कनेक्शन पर Cancel कॉल में बदल देता है. इससे कमांड को जल्द से जल्द बंद करने की कोशिश की जाती है. तीसरे Ctrl-C के बाद, क्लाइंट सर्वर को SIGKILL भेजता है.
क्लाइंट का सोर्स कोड src/main/cpp में है. साथ ही, सर्वर से कम्यूनिकेट करने के लिए इस्तेमाल किया गया प्रोटोकॉल src/main/protobuf/command_server.proto में है.
सर्वर का मुख्य एंट्री पॉइंट BlazeRuntime.main() है. साथ ही, क्लाइंट से मिले gRPC कॉल को GrpcServerImpl.run() हैंडल करता है.
डायरेक्ट्री का लेआउट
Bazel, बिल्ड के दौरान डायरेक्ट्री का एक मुश्किल सेट बनाता है. पूरी जानकारी आउटपुट डायरेक्ट्री लेआउट में उपलब्ध है.
"वर्कस्पेस" वह सोर्स ट्री होता है जिसमें Bazel को चलाया जाता है. आम तौर पर, यह उस चीज़ से मेल खाता है जिसे आपने सोर्स कंट्रोल से चेक आउट किया है.
Bazel अपना सारा डेटा "output user root" में रखता है. आम तौर पर, यह $HOME/.cache/bazel/_bazel_${USER} होता है. हालांकि, --output_user_root स्टार्टअप विकल्प का इस्तेमाल करके इसे बदला जा सकता है.
"install base" वह जगह होती है जहां Bazel को एक्सट्रैक्ट किया जाता है. यह प्रोसेस अपने-आप होती है. साथ ही, हर Bazel वर्शन को इंस्टॉल बेस में उसके चेकसम के आधार पर एक सबडायरेक्ट्री मिलती है. यह डिफ़ॉल्ट रूप से $OUTPUT_USER_ROOT/install पर सेट होता है. इसे --install_base कमांड लाइन विकल्प का इस्तेमाल करके बदला जा सकता है.
"आउटपुट बेस" वह जगह होती है जहां किसी खास वर्कस्पेस से जुड़ा Bazel इंस्टेंस लिखता है. हर आउटपुट बेस में, किसी भी समय ज़्यादा से ज़्यादा एक Bazel सर्वर इंस्टेंस चल रहा होता है. यह आम तौर पर $OUTPUT_USER_ROOT/<checksum of the path
to the workspace> बजे होता है. इसे --output_base स्टार्टअप विकल्प का इस्तेमाल करके बदला जा सकता है. यह विकल्प, अन्य चीज़ों के साथ-साथ इस सीमा को पार करने के लिए भी काम आता है कि किसी भी समय किसी भी फ़ाइल फ़ोल्डर में सिर्फ़ एक Bazel इंस्टेंस चल सकता है.
आउटपुट डायरेक्ट्री में ये चीज़ें शामिल होती हैं:
$OUTPUT_BASE/externalपर फ़ेच की गई बाहरी रिपॉज़िटरी.- एक्ज़ेक रूट, एक डायरेक्ट्री होती है. इसमें मौजूदा बिल्ड के सभी सोर्स कोड के लिए सिंबल लिंक होते हैं. यह
$OUTPUT_BASE/execrootपर मौजूद है. बिल्ड के दौरान, वर्किंग डायरेक्ट्री$EXECROOT/<name of main repository>होती है. हम इसे$EXECROOTमें बदलने का प्लान बना रहे हैं. हालांकि, यह एक लंबी अवधि का प्लान है, क्योंकि यह एक बहुत ही मुश्किल बदलाव है. - बिल्ड के दौरान बनाई गई फ़ाइलें.
किसी निर्देश को पूरा करने की प्रोसेस
Bazel सर्वर को कंट्रोल मिलने और उसे किसी कमांड के बारे में सूचना मिलने के बाद, ये काम होते हैं:
BlazeCommandDispatcherको नए अनुरोध के बारे में सूचना दी जाती है. यह तय करता है कि कमांड को चलाने के लिए किसी वर्कस्पेस की ज़रूरत है या नहीं. सोर्स कोड से जुड़ी कमांड को छोड़कर, लगभग हर कमांड के लिए वर्कस्पेस की ज़रूरत होती है. जैसे, वर्शन या मदद. यह भी तय करता है कि कोई दूसरी कमांड चल रही है या नहीं.सही कमांड मिल गई है. हर कमांड को
BlazeCommandइंटरफ़ेस लागू करना होगा. साथ ही, इसमें@Commandएनोटेशन होना चाहिए. यह थोड़ा एंटीपैटर्न है. अगर किसी कमांड के लिए ज़रूरी सभी मेटाडेटा कोBlazeCommandके तरीकों से बताया जाए, तो यह बेहतर होगाकमांड लाइन के विकल्पों को पार्स किया जाता है. हर कमांड के लिए, कमांड लाइन के अलग-अलग विकल्प होते हैं. इनके बारे में
@Commandएनोटेशन में बताया गया है.इवेंट बस बनाई जाती है. इवेंट बस, इवेंट की एक स्ट्रीम होती है. इसमें बिल्ड के दौरान होने वाले इवेंट शामिल होते हैं. इनमें से कुछ को Build Event Protocol के तहत, Bazel से बाहर एक्सपोर्ट किया जाता है. ऐसा इसलिए किया जाता है, ताकि दुनिया को यह बताया जा सके कि बिल्ड कैसे काम करता है.
कमांड को कंट्रोल मिल जाता है. सबसे दिलचस्प कमांड वे होती हैं जो बिल्ड चलाती हैं: बिल्ड, टेस्ट, रन, कवरेज वगैरह: इस सुविधा को
BuildToolने लागू किया है.कमांड लाइन पर टारगेट पैटर्न के सेट को पार्स किया जाता है. साथ ही,
//pkg:allऔर//pkg/...जैसे वाइल्डकार्ड को हल किया जाता है. इसेAnalysisPhaseRunner.evaluateTargetPatterns()में लागू किया जाता है और Skyframe मेंTargetPatternPhaseValueके तौर पर रीफ़ाई किया जाता है.लोडिंग/विश्लेषण का चरण, ऐक्शन ग्राफ़ बनाने के लिए चलाया जाता है. यह एक डायरेक्टेड ऐसाइक्लिक ग्राफ़ होता है. इसमें उन कमांड के बारे में जानकारी होती है जिन्हें बिल्ड के लिए एक्ज़ीक्यूट करना होता है.
प्रोग्राम चलाने का चरण पूरा हो गया है. इसका मतलब है कि अनुरोध किए गए टॉप-लेवल के टारगेट बनाने के लिए, ज़रूरी हर कार्रवाई की जाती है.
कमांड लाइन के विकल्प
Bazel इनवोकेशन के लिए कमांड-लाइन विकल्पों के बारे में OptionsParsingResult ऑब्जेक्ट में बताया गया है. इसमें "optionclasses" से लेकर विकल्पों की वैल्यू तक का मैप शामिल होता है. "विकल्प क्लास", OptionsBase की सबक्लास होती है. यह कमांड लाइन के उन विकल्पों को एक साथ ग्रुप करती है जो एक-दूसरे से जुड़े होते हैं. उदाहरण के लिए:
- प्रोग्रामिंग लैंग्वेज से जुड़े विकल्प (
CppOptionsयाJavaOptions). येFragmentOptionsके सब-क्लास होने चाहिए और आखिर में इन्हेंBuildOptionsऑब्जेक्ट में रैप किया जाता है. - Bazel के कार्रवाइयां करने के तरीके से जुड़े विकल्प (
ExecutionOptions)
इन विकल्पों को विश्लेषण के चरण में इस्तेमाल करने के लिए डिज़ाइन किया गया है. इन्हें Java में RuleContext.getFragment() या Starlark में ctx.fragments के ज़रिए इस्तेमाल किया जा सकता है.
इनमें से कुछ (उदाहरण के लिए, C++ फ़ाइल को स्कैन करना है या नहीं) को एक्ज़ीक्यूशन फ़ेज़ में पढ़ा जाता है. हालांकि, इसके लिए हमेशा प्लंबिंग की ज़रूरत होती है, क्योंकि BuildConfiguration तब उपलब्ध नहीं होता है. ज़्यादा जानकारी के लिए, “कॉन्फ़िगरेशन” सेक्शन देखें.
चेतावनी: हम यह मानकर चलते हैं कि OptionsBase इंस्टेंस में बदलाव नहीं किया जा सकता और हम उनका इस्तेमाल इसी तरह करते हैं. जैसे, SkyKeys के हिस्से के तौर पर. हालांकि, ऐसा नहीं है. इनमें बदलाव करने से, Bazel में ऐसी समस्याएं आ सकती हैं जिन्हें ठीक करना मुश्किल होता है. हालांकि, इन्हें पूरी तरह से बदला नहीं जा सकता.
(FragmentOptions को बनाने के तुरंत बाद उसमें बदलाव किया जा सकता है. ऐसा तब तक किया जा सकता है, जब तक कोई और व्यक्ति उसका रेफ़रंस नहीं रखता और equals() या hashCode() को कॉल नहीं किया जाता.)
Bazel को विकल्प क्लास के बारे में इन तरीकों से पता चलता है:
- कुछ को Bazel में पहले से ही शामिल किया गया है (
CommonCommandOptions) - हर Bazel कमांड पर मौजूद @Command एनोटेशन से
ConfiguredRuleClassProviderसे (ये कमांड लाइन के विकल्प हैं, जो अलग-अलग प्रोग्रामिंग भाषाओं से जुड़े हैं)- Starlark के नियम भी अपने विकल्प तय कर सकते हैं (यहां देखें)
हर विकल्प (Starlark के तय किए गए विकल्पों को छोड़कर), FragmentOptions एनोटेशन वाली FragmentOptions सबक्लास का सदस्य वैरिएबल होता है. यह कमांड लाइन के विकल्प का नाम और टाइप तय करता है. साथ ही, कुछ मदद टेक्स्ट भी दिखाता है.@Option
कमांड लाइन के विकल्प की वैल्यू का Java टाइप आम तौर पर आसान होता है. जैसे, स्ट्रिंग, पूर्णांक, बूलियन, लेबल वगैरह. हालांकि, हम ज़्यादा जटिल टाइप के विकल्पों का भी समर्थन करते हैं. इस मामले में, कमांड लाइन स्ट्रिंग को डेटा टाइप में बदलने का काम, com.google.devtools.common.options.Converter के लागू होने पर निर्भर करता है.
Bazel को दिखने वाला सोर्स ट्री
Bazel, सॉफ़्टवेयर बनाने का काम करता है. इसके लिए, वह सोर्स कोड को पढ़ता है और उसकी व्याख्या करता है. Bazel जिस सोर्स कोड पर काम करता है उसे "वर्कस्पेस" कहा जाता है. इसे रिपॉज़िटरी, पैकेज, और नियमों में बांटा जाता है.
डेटा स्टोर करने की जगह
"रिपॉज़िटरी" एक सोर्स ट्री होता है, जिस पर डेवलपर काम करता है. यह आम तौर पर एक प्रोजेक्ट को दिखाता है. Bazel का पूर्ववर्ती, Blaze, एक मोनोरिपो पर काम करता था. इसका मतलब है कि एक ही सोर्स ट्री में, बिल्ड को चलाने के लिए इस्तेमाल किया गया सारा सोर्स कोड होता है. इसके उलट, Bazel उन प्रोजेक्ट के साथ काम करता है जिनका सोर्स कोड कई रिपॉज़िटरी में फैला होता है. जिस रिपॉज़िटरी से Bazel को कॉल किया जाता है उसे “मुख्य रिपॉज़िटरी” कहा जाता है. अन्य रिपॉज़िटरी को “बाहरी रिपॉज़िटरी” कहा जाता है.
किसी रिपॉज़िटरी को उसकी रूट डायरेक्ट्री में मौजूद WORKSPACE (या WORKSPACE.bazel) फ़ाइल से मार्क किया जाता है. इस फ़ाइल में ऐसी जानकारी होती है जो पूरे बिल्ड के लिए "ग्लोबल" होती है. उदाहरण के लिए, उपलब्ध बाहरी रिपॉज़िटरी का सेट. यह एक सामान्य Starlark फ़ाइल की तरह काम करती है. इसका मतलब है कि कोई व्यक्ति load() अन्य Starlark फ़ाइलों को कर सकता है.
इसका इस्तेमाल आम तौर पर उन रिपॉज़िटरी को पुल करने के लिए किया जाता है जिनकी ज़रूरत किसी ऐसी रिपॉज़िटरी को होती है जिसे साफ़ तौर पर रेफ़रंस किया गया है. इसे "deps.bzl पैटर्न" कहा जाता है
बाहरी रिपॉज़िटरी का कोड, $OUTPUT_BASE/external में सिंबल के तौर पर लिंक किया गया है या डाउनलोड किया गया है.
बिल्ड को चलाने के लिए, पूरे सोर्स ट्री को एक साथ जोड़ना होता है. यह
काम SymlinkForest करता है. यह मुख्य रिपॉज़िटरी में मौजूद हर पैकेज को
$EXECROOT से और हर बाहरी रिपॉज़िटरी को $EXECROOT/external या
$EXECROOT/.. से सिंबल के तौर पर लिंक करता है. हालांकि, से लिंक करने की वजह से, मुख्य रिपॉज़िटरी में external नाम का
पैकेज नहीं बनाया जा सकता. इसलिए, हम इससे
माइग्रेट कर रहे हैं
पैकेज
हर रिपॉज़िटरी में पैकेज होते हैं. ये पैकेज, मिलती-जुलती फ़ाइलों का कलेक्शन होते हैं. साथ ही, इनमें डिपेंडेंसी की जानकारी भी होती है. इन्हें BUILD या BUILD.bazel नाम की फ़ाइल से तय किया जाता है. अगर दोनों मौजूद हैं, तो Bazel BUILD.bazel को प्राथमिकता देता है. BUILD फ़ाइलों को अब भी स्वीकार किया जाता है, क्योंकि Bazel के पूर्वज, Blaze ने इस फ़ाइल के नाम का इस्तेमाल किया था. हालांकि, यह एक सामान्य पाथ सेगमेंट है. खास तौर पर, Windows पर इसका इस्तेमाल ज़्यादा होता है, जहां फ़ाइल के नाम केस-इनसेंसिटिव होते हैं.
पैकेज एक-दूसरे से अलग होते हैं: किसी पैकेज की BUILD फ़ाइल में किए गए बदलावों से, दूसरे पैकेज में बदलाव नहीं होता. BUILD फ़ाइलों को जोड़ने या हटाने से, अन्य पैकेज में बदलाव हो सकता है. ऐसा इसलिए होता है, क्योंकि रिकर्सिव ग्लोब पैकेज की सीमाओं पर रुक जाते हैं. इसलिए, BUILD फ़ाइल के मौजूद होने से रिकर्सन रुक जाता है.
BUILD फ़ाइल के आकलन को "पैकेज लोडिंग" कहा जाता है. इसे क्लास PackageFactory में लागू किया जाता है. यह Starlark इंटरप्रेटर को कॉल करके काम करता है. इसके लिए, उपलब्ध नियम क्लास के सेट के बारे में जानकारी होना ज़रूरी है. पैकेज लोड करने का नतीजा, एक Package ऑब्जेक्ट होता है. यह ज़्यादातर, स्ट्रिंग (किसी टारगेट का नाम) से टारगेट तक का मैप होता है.
पैकेज लोड करने के दौरान, ज़्यादातर काम ग्लोबिंग का होता है: Bazel को हर सोर्स फ़ाइल को साफ़ तौर पर लिस्ट करने की ज़रूरत नहीं होती. इसके बजाय, यह ग्लोब (जैसे, glob(["**/*.java"])) चला सकता है. शेल के उलट, यह रिकर्सिव ग्लोब का इस्तेमाल करता है. ये सबडायरेक्ट्री में जाते हैं, लेकिन सबपैकेज में नहीं. इसके लिए, फ़ाइल सिस्टम का ऐक्सेस ज़रूरी होता है. इसमें समय लग सकता है. इसलिए, हम इसे एक साथ और ज़्यादा से ज़्यादा असरदार तरीके से चलाने के लिए, हर तरह की तरकीबें अपनाते हैं.
ग्लोबिंग को इन क्लास में लागू किया जाता है:
LegacyGlobber, एक ऐसा ग्लोबर जो Skyframe के बारे में नहीं जानता, लेकिन तेज़ी से काम करता हैSkyframeHybridGlobber, यह Skyframe का इस्तेमाल करता है और “Skyframe restarts” (इसके बारे में नीचे बताया गया है) से बचने के लिए, पुराने ग्लोबर पर वापस आ जाता है
Package क्लास में कुछ ऐसे सदस्य होते हैं जिनका इस्तेमाल सिर्फ़ WORKSPACE फ़ाइल को पार्स करने के लिए किया जाता है. ये सदस्य, असली पैकेज के लिए काम के नहीं होते. यह डिज़ाइन से जुड़ी समस्या है, क्योंकि सामान्य पैकेज के बारे में बताने वाले ऑब्जेक्ट में ऐसे फ़ील्ड नहीं होने चाहिए जो किसी और चीज़ के बारे में बताते हों. इनमें शामिल हैं:
- रिपॉज़िटरी मैपिंग
- रजिस्टर की गई टूलचेन
- रजिस्टर किए गए एक्ज़ीक्यूशन प्लैटफ़ॉर्म
आदर्श रूप से, WORKSPACE फ़ाइल को पार्स करने और सामान्य पैकेज को पार्स करने के बीच ज़्यादा अंतर होना चाहिए, ताकि Packageको दोनों की ज़रूरतों को पूरा न करना पड़े. अफ़सोस की बात है कि ऐसा करना मुश्किल है, क्योंकि दोनों एक-दूसरे से काफ़ी जुड़े हुए हैं.
लेबल, टारगेट, और नियम
पैकेज में टारगेट होते हैं. ये टारगेट इस तरह के होते हैं:
- फ़ाइलें: ऐसी चीज़ें जो बिल्ड के लिए इनपुट या आउटपुट होती हैं. Bazel की भाषा में, हम इन्हें आर्टफ़ैक्ट कहते हैं. इनके बारे में किसी और लेख में बताया गया है. बिल्ड के दौरान बनाई गई सभी फ़ाइलें टारगेट नहीं होती हैं. ऐसा अक्सर होता है कि Bazel के आउटपुट में कोई लेबल न जुड़ा हो.
- नियम: इनमें, इनपुट से आउटपुट पाने के तरीके के बारे में बताया जाता है. ये आम तौर पर किसी प्रोग्रामिंग भाषा (जैसे,
cc_library,java_libraryयाpy_library) से जुड़े होते हैं. हालांकि, कुछ ऐसे भी होते हैं जो किसी भी प्रोग्रामिंग भाषा के साथ काम कर सकते हैं (जैसे,genruleयाfilegroup) - पैकेज ग्रुप: इनके बारे में दिखने की संभावना सेक्शन में बताया गया है.
किसी टारगेट के नाम को लेबल कहा जाता है. लेबल का सिंटैक्स @repo//pac/kage:name है. इसमें repo उस रिपॉज़िटरी का नाम है जिसमें लेबल मौजूद है, pac/kage उस डायरेक्ट्री का नाम है जिसमें लेबल की BUILD फ़ाइल मौजूद है, और name फ़ाइल का पाथ है. अगर लेबल किसी सोर्स फ़ाइल से जुड़ा है, तो यह पाथ पैकेज की डायरेक्ट्री के हिसाब से तय किया जाता है. कमांड-लाइन पर किसी टारगेट का रेफ़रंस देते समय, लेबल के कुछ हिस्सों को हटाया जा सकता है:
- अगर रिपॉज़िटरी को छोड़ दिया जाता है, तो लेबल को मुख्य रिपॉज़िटरी में माना जाता है.
- अगर पैकेज का हिस्सा शामिल नहीं किया गया है (जैसे कि
nameया:name), तो लेबल को मौजूदा वर्किंग डायरेक्ट्री के पैकेज में माना जाता है. इसमें, अपलेवल रेफ़रंस (..) वाले रिलेटिव पाथ इस्तेमाल करने की अनुमति नहीं है
किसी नियम (जैसे, "C++ लाइब्रेरी") को "नियम क्लास" कहा जाता है. नियमों की क्लास को Starlark (rule() फ़ंक्शन) या Java (जिन्हें “नेटिव नियम” कहा जाता है, टाइप RuleClass) में लागू किया जा सकता है. आने वाले समय में, भाषा के हिसाब से हर नियम को Starlark में लागू किया जाएगा. हालांकि, कुछ लेगसी नियम फ़ैमिली (जैसे कि Java या C++) फ़िलहाल Java में ही हैं.
Starlark नियम क्लास को BUILD फ़ाइलों की शुरुआत में इंपोर्ट करना होता है. इसके लिए, load() स्टेटमेंट का इस्तेमाल किया जाता है. वहीं, Java नियम क्लास को Bazel "जन्मजात" रूप से जानता है, क्योंकि वे ConfiguredRuleClassProvider के साथ रजिस्टर होती हैं.
नियम की क्लास में यह जानकारी शामिल होती है:
- इसके एट्रिब्यूट (जैसे,
srcs,deps): इनके टाइप, डिफ़ॉल्ट वैल्यू, शर्तें वगैरह. - हर एट्रिब्यूट से जुड़े कॉन्फ़िगरेशन ट्रांज़िशन और पहलू, अगर कोई हो
- नियम लागू करना
- ट्रांज़िटिव जानकारी देने वाले, ऐसे नियम बनाते हैं जो "आम तौर पर" बनाए जाते हैं
शब्दावली से जुड़ी जानकारी: कोड बेस में, हम अक्सर “नियम” शब्द का इस्तेमाल, नियम क्लास से बनाए गए टारगेट के लिए करते हैं. हालांकि, Starlark और उपयोगकर्ता के लिए उपलब्ध दस्तावेज़ में, “नियम” शब्द का इस्तेमाल सिर्फ़ नियम क्लास के लिए किया जाना चाहिए. टारगेट सिर्फ़ एक “टारगेट” होता है. यह भी ध्यान दें कि RuleClass के नाम में “क्लास” होने के बावजूद, नियम क्लास और उस टाइप के टारगेट के बीच कोई Java इनहेरिटेंस रिलेशनशिप नहीं है.
Skyframe
Bazel के लिए इस्तेमाल किए जाने वाले आकलन फ़्रेमवर्क को Skyframe कहा जाता है. इसका मॉडल यह है कि बिल्ड के दौरान बनाई जाने वाली हर चीज़ को डायरेक्टेड एसाइक्लिक ग्राफ़ में व्यवस्थित किया जाता है. इसमें किनारों को डेटा के किसी भी हिस्से से उसकी डिपेंडेंसी की ओर ले जाया जाता है. इसका मतलब है कि डेटा के अन्य हिस्सों को बनाने के लिए ज़रूरी जानकारी.
ग्राफ़ में मौजूद नोड को SkyValue कहा जाता है और उनके नाम को SkyKey कहा जाता है. ये दोनों ऑब्जेक्ट, डीपली इम्यूटेबल होते हैं. इनसे सिर्फ़ इम्यूटेबल ऑब्जेक्ट ही ऐक्सेस किए जा सकते हैं. यह इनवेरिएंट लगभग हमेशा लागू होता है. अगर यह लागू नहीं होता है, तो हम उन्हें बदलने की कोशिश नहीं करते हैं. जैसे, अलग-अलग विकल्पों वाली क्लास BuildOptions, जो BuildConfigurationValue और इसके SkyKey का सदस्य है. अगर हम उन्हें बदलते हैं, तो हम सिर्फ़ ऐसे तरीके अपनाते हैं जिन्हें बाहर से नहीं देखा जा सकता.
इससे यह पता चलता है कि Skyframe में कंप्यूट की गई हर चीज़ (जैसे कि कॉन्फ़िगर किए गए टारगेट) में भी बदलाव नहीं किया जा सकता.
Skyframe ग्राफ़ को देखने का सबसे आसान तरीका है कि आप bazel dump
--skyframe=detailed चलाएं. इससे ग्राफ़ डंप हो जाता है और हर लाइन में एक SkyValue दिखता है. इसे छोटी बिल्ड के लिए इस्तेमाल करना सबसे अच्छा होता है, क्योंकि यह काफ़ी बड़ी हो सकती है.
Skyframe, com.google.devtools.build.skyframe पैकेज में उपलब्ध है. इसी नाम वाले पैकेज com.google.devtools.build.lib.skyframe में, Skyframe के ऊपर Bazel को लागू करने का तरीका शामिल है. Skyframe के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
किसी SkyKey को SkyValue में बदलने के लिए, Skyframe उस कुंजी के टाइप से जुड़े SkyFunction को चालू करेगा. फ़ंक्शन के आकलन के दौरान, यह Skyframe से अन्य डिपेंडेंसी का अनुरोध कर सकता है. इसके लिए, यह SkyFunction.Environment.getValue() के अलग-अलग ओवरलोड को कॉल करता है. इससे उन डिपेंडेंसी को Skyframe के इंटरनल ग्राफ़ में रजिस्टर करने का साइड इफ़ेक्ट होता है. इसलिए, जब भी इसकी किसी डिपेंडेंसी में बदलाव होता है, तो Skyframe फ़ंक्शन का फिर से आकलन कर पाएगा. दूसरे शब्दों में कहें, तो Skyframe की कैश मेमोरी और इंक्रीमेंटल कंप्यूटेशन, SkyFunction और SkyValue के हिसाब से काम करते हैं.
जब भी कोई SkyFunction ऐसी डिपेंडेंसी का अनुरोध करता है जो उपलब्ध नहीं है, तो getValue() शून्य वैल्यू दिखाता है. इसके बाद, फ़ंक्शन को Skyframe को कंट्रोल वापस दे देना चाहिए. इसके लिए, उसे खुद ही null वैल्यू दिखानी होगी. कुछ समय बाद, Skyframe उस डिपेंडेंसी का आकलन करेगा जो उपलब्ध नहीं है. इसके बाद, फ़ंक्शन को शुरू से रीस्टार्ट करेगा. इस बार, getValue() कॉल को गैर-शून्य नतीजे के साथ पूरा किया जाएगा.
इसका मतलब है कि रीस्टार्ट करने से पहले, SkyFunction में किए गए किसी भी कंप्यूटेशन को दोहराना होगा. हालांकि, इसमें डिपेंडेंसी SkyValues का आकलन करने के लिए किया गया काम शामिल नहीं है, जिसे कैश मेमोरी में सेव किया जाता है. इसलिए, हम आम तौर पर इस समस्या को इन तरीकों से हल करते हैं:
- रीस्टार्ट की संख्या को सीमित करने के लिए, बैच में डिपेंडेंसी का एलान करना (
getValuesAndExceptions()का इस्तेमाल करके). SkyValueको अलग-अलग हिस्सों में बांटना. इन हिस्सों को अलग-अलगSkyFunctions से कंप्यूट किया जाता है, ताकि इन्हें अलग-अलग कंप्यूट और कैश किया जा सके. इसे रणनीति के तहत किया जाना चाहिए, क्योंकि इससे मेमोरी का इस्तेमाल बढ़ सकता है.- रीस्टार्ट के बीच की स्थिति को सेव करना. इसके लिए,
SkyFunction.Environment.getState()का इस्तेमाल किया जाता है या "Skyframe के पीछे" एक ऐड हॉक स्टैटिक कैश मेमोरी रखी जाती है.
हमें इस तरह के समाधानों की ज़रूरत इसलिए पड़ती है, क्योंकि हमारे पास आम तौर पर हज़ारों Skyframe नोड होते हैं. साथ ही, Java में लाइटवेट थ्रेड काम नहीं करती हैं.
Starlark
Starlark, डोमेन के हिसाब से बनाई गई एक भाषा है. इसका इस्तेमाल लोग Bazel को कॉन्फ़िगर करने और उसे बढ़ाने के लिए करते हैं. इसे Python के सीमित सबसेट के तौर पर बनाया गया है. इसमें बहुत कम टाइप होते हैं. साथ ही, कंट्रोल फ़्लो पर ज़्यादा पाबंदियां होती हैं. सबसे अहम बात यह है कि इसमें एक साथ कई बार पढ़ने की सुविधा चालू करने के लिए, डेटा में बदलाव न करने की गारंटी होती है. यह ट्यूरिंग-कंप्लीट नहीं है. इसलिए, कुछ (लेकिन सभी नहीं) उपयोगकर्ता इस भाषा में सामान्य प्रोग्रामिंग टास्क पूरे करने की कोशिश नहीं करते.
Starlark को net.starlark.java पैकेज में लागू किया जाता है.
इसका एक इंडिपेंडेंट Go इंप्लीमेंटेशन भी है. इसे यहां देखा जा सकता है. Bazel में इस्तेमाल किया जाने वाला Java
इंप्लीमेंटेशन, फ़िलहाल एक इंटरप्रेटर है.
Starlark का इस्तेमाल कई तरह के कामों के लिए किया जाता है. जैसे:
BUILDभाषा. यहां नए नियम तय किए जाते हैं. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास, सिर्फ़BUILDफ़ाइल के कॉन्टेंट और उससे लोड की गई.bzlफ़ाइलों का ऐक्सेस होता है.- नियमों की परिभाषाएं. नए नियमों (जैसे, नई भाषा के लिए सहायता) को इस तरह से तय किया जाता है. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास, कॉन्फ़िगरेशन और डेटा का ऐक्सेस होता है. यह कॉन्फ़िगरेशन और डेटा, इसकी डायरेक्ट डिपेंडेंसी से मिलता है. इसके बारे में ज़्यादा जानकारी बाद में दी जाएगी.
- WORKSPACE फ़ाइल. यहां बाहरी रिपॉज़िटरी (ऐसा कोड जो मुख्य सोर्स ट्री में नहीं है) तय की जाती हैं.
- रिपॉज़िटरी के नियमों की परिभाषाएं. यहां बाहरी रिपॉज़िटरी के नए टाइप तय किए जाते हैं. इस कॉन्टेक्स्ट में चल रहा Starlark कोड, उस मशीन पर कोई भी कोड चला सकता है जिस पर Bazel चल रहा है. साथ ही, यह वर्कस्पेस के बाहर भी पहुंच सकता है.
BUILD और .bzl फ़ाइलों के लिए उपलब्ध बोलियां थोड़ी अलग होती हैं, क्योंकि ये अलग-अलग चीज़ों को ज़ाहिर करती हैं. इनके बीच के अंतर की सूची यहां दी गई है.
Starlark के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
लोडिंग/विश्लेषण का चरण
लोडिंग/विश्लेषण के फ़ेज़ में, Bazel यह तय करता है कि किसी नियम को बनाने के लिए कौनसी कार्रवाइयां ज़रूरी हैं. इसकी बुनियादी यूनिट "कॉन्फ़िगर किया गया टारगेट" है. यह (टारगेट, कॉन्फ़िगरेशन) का एक पेयर होता है.
इसे "डेटा लोड होने/विश्लेषण का चरण" कहा जाता है, क्योंकि इसे दो अलग-अलग हिस्सों में बांटा जा सकता है. पहले ये हिस्से क्रम से होते थे, लेकिन अब ये एक साथ हो सकते हैं:
- पैकेज लोड किए जा रहे हैं. इसका मतलब है कि
BUILDफ़ाइलों कोPackageऑब्जेक्ट में बदला जा रहा है, ताकि उन्हें दिखाया जा सके - कॉन्फ़िगर किए गए टारगेट का विश्लेषण करना. इसका मतलब है कि ऐक्शन ग्राफ़ बनाने के लिए, नियमों को लागू करना
कमांड लाइन पर अनुरोध किए गए कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में कॉन्फ़िगर किए गए हर टारगेट का विश्लेषण, बॉटम-अप तरीके से किया जाना चाहिए. इसका मतलब है कि पहले लीफ़ नोड और फिर कमांड लाइन पर मौजूद नोड का विश्लेषण किया जाना चाहिए. कॉन्फ़िगर किए गए किसी एक टारगेट के विश्लेषण के लिए, ये इनपुट इस्तेमाल किए जाते हैं:
- कॉन्फ़िगरेशन. ("कैसे" नियम बनाना है; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म के साथ-साथ, कमांड लाइन के ऐसे विकल्प जिन्हें उपयोगकर्ता C++ कंपाइलर को पास करना चाहता है)
- सीधे तौर पर निर्भरता रखने वाले आइटम. नियम का विश्लेषण करने के लिए, ट्रांज़िटिव जानकारी देने वाली कंपनियां उपलब्ध हैं. इन्हें ऐसा इसलिए कहा जाता है, क्योंकि ये कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में मौजूद जानकारी को "रोल-अप" करते हैं. जैसे, क्लासपाथ पर मौजूद सभी .jar फ़ाइलें या वे सभी .o फ़ाइलें जिन्हें C++ बाइनरी में लिंक करने की ज़रूरत होती है)
- टारगेट. यह उस पैकेज को लोड करने का नतीजा है जिसमें टारगेट मौजूद है. नियमों के लिए, इसमें एट्रिब्यूट शामिल होते हैं. आम तौर पर, यही मायने रखता है.
- कॉन्फ़िगर किए गए टारगेट को लागू करना. नियमों के लिए, यह Starlark या Java में हो सकता है. नियम के मुताबिक कॉन्फ़िगर नहीं किए गए सभी टारगेट, Java में लागू किए जाते हैं.
कॉन्फ़िगर किए गए टारगेट का विश्लेषण करने पर, यह आउटपुट मिलता है:
- यह जानकारी, उन ट्रांज़िटिव जानकारी देने वाली कंपनियों को मिलती है जिन्होंने ऐसे टारगेट कॉन्फ़िगर किए हैं जो इस जानकारी पर निर्भर करते हैं
- यह किन आर्टफ़ैक्ट को बना सकता है और उन्हें बनाने के लिए कौनसी कार्रवाइयाँ कर सकता है.
Java के नियमों के लिए उपलब्ध एपीआई RuleContext है. यह Starlark के नियमों के ctx आर्ग्युमेंट के बराबर है. इसका एपीआई ज़्यादा बेहतर है. हालांकि, इसका इस्तेमाल करके Bad Things™ करना भी आसान है. उदाहरण के लिए, ऐसा कोड लिखना जिसकी टाइम या स्पेस कॉम्प्लेक्सिटी क्वाड्रेटिक (या इससे भी खराब) हो, Java अपवाद की वजह से Bazel सर्वर को क्रैश करना या इनवेरिएंट का उल्लंघन करना (जैसे, अनजाने में Options इंस्टेंस में बदलाव करना या कॉन्फ़िगर किए गए टारगेट को बदलने की अनुमति देना)
कॉन्फ़िगर किए गए टारगेट की डायरेक्ट डिपेंडेंसी तय करने वाला एल्गोरिदम, DependencyResolver.dependentNodeMap() में मौजूद होता है.
कॉन्फ़िगरेशन
कॉन्फ़िगरेशन, टारगेट बनाने का "तरीका" होता है: किस प्लैटफ़ॉर्म के लिए, कमांड लाइन के किन विकल्पों के साथ वगैरह.
एक ही टारगेट को एक ही बिल्ड में कई कॉन्फ़िगरेशन के लिए बनाया जा सकता है. यह उदाहरण के लिए तब काम आता है, जब एक ही कोड का इस्तेमाल ऐसे टूल के लिए किया जाता है जिसे बिल्ड के दौरान चलाया जाता है और टारगेट कोड के लिए भी. साथ ही, जब हम क्रॉस-कंपाइलिंग कर रहे हों या जब हम एक फ़ैट Android ऐप्लिकेशन (ऐसा ऐप्लिकेशन जिसमें कई सीपीयू आर्किटेक्चर के लिए नेटिव कोड होता है) बना रहे हों
कॉन्सेप्ट के हिसाब से, कॉन्फ़िगरेशन एक BuildOptions इंस्टेंस होता है. हालांकि, व्यवहार में BuildOptions को BuildConfiguration में रैप किया जाता है, जो कई तरह के अतिरिक्त फ़ंक्शन उपलब्ध कराता है. यह डिपेंडेंसी ग्राफ़ में सबसे ऊपर से सबसे नीचे तक फैलता है. अगर इसमें बदलाव होता है, तो बिल्ड का फिर से विश्लेषण करना होगा.
इस वजह से, कई तरह की समस्याएं आती हैं. जैसे, अगर टेस्ट रन के अनुरोधों की संख्या में बदलाव होता है, तो पूरे बिल्ड का फिर से विश्लेषण करना पड़ता है. भले ही, इससे सिर्फ़ टेस्ट टारगेट पर असर पड़ता हो. हम कॉन्फ़िगरेशन को "ट्रिम" करने की योजना बना रहे हैं, ताकि ऐसा न हो. हालांकि, यह सुविधा अभी उपलब्ध नहीं है.
जब किसी नियम को लागू करने के लिए कॉन्फ़िगरेशन के किसी हिस्से की ज़रूरत होती है, तो उसे RuleClass.Builder.requiresConfigurationFragments() का इस्तेमाल करके, अपनी परिभाषा में इसका एलान करना होता है. ऐसा इसलिए किया जाता है, ताकि गलतियां न हों. जैसे, Python के नियमों में Java फ़्रैगमेंट का इस्तेमाल करना. साथ ही, कॉन्फ़िगरेशन को ट्रिम करने में आसानी हो. जैसे, अगर Python के विकल्प बदलते हैं, तो C++ टारगेट का फिर से विश्लेषण करने की ज़रूरत नहीं होती.
यह ज़रूरी नहीं है कि किसी नियम का कॉन्फ़िगरेशन, उसके "पैरंट" नियम के कॉन्फ़िगरेशन जैसा हो. डिपेंडेंसी एज में कॉन्फ़िगरेशन बदलने की प्रोसेस को "कॉन्फ़िगरेशन ट्रांज़िशन" कहा जाता है. ऐसा दो जगहों पर हो सकता है:
- डिपेंडेंसी एज पर. इन ट्रांज़िशन को
Attribute.Builder.cfg()में बताया गया है. येRule(जहां ट्रांज़िशन होता है) औरBuildOptions(ओरिजनल कॉन्फ़िगरेशन) से लेकर एक या उससे ज़्यादाBuildOptions(आउटपुट कॉन्फ़िगरेशन) तक के फ़ंक्शन होते हैं. - कॉन्फ़िगर किए गए टारगेट में आने वाले किसी भी एज पर. इनके बारे में
RuleClass.Builder.cfg()में बताया गया है.
इससे जुड़ी क्लास TransitionFactory और ConfigurationTransition हैं.
कॉन्फ़िगरेशन ट्रांज़िशन का इस्तेमाल इन कामों के लिए किया जाता है:
- यह एलान करने के लिए कि किसी खास डिपेंडेंसी का इस्तेमाल बिल्ड के दौरान किया जाता है और इसलिए इसे एक्ज़ीक्यूशन आर्किटेक्चर में बनाया जाना चाहिए
- यह एलान करने के लिए कि किसी खास डिपेंडेंसी को कई आर्किटेक्चर के लिए बनाया जाना चाहिए. जैसे, फ़ैट Android APK में नेटिव कोड के लिए
अगर कॉन्फ़िगरेशन ट्रांज़िशन के बाद एक से ज़्यादा कॉन्फ़िगरेशन मिलते हैं, तो इसे स्प्लिट ट्रांज़िशन कहा जाता है.
कॉन्फ़िगरेशन ट्रांज़िशन को Starlark में भी लागू किया जा सकता है. इसके बारे में दस्तावेज़ यहां दिया गया है
ट्रांज़िट की जानकारी देने वाली कंपनियां
ट्रांज़िटिव इन्फ़ो प्रोवाइडर, कॉन्फ़िगर किए गए टारगेट के लिए एक तरीका है. यह कॉन्फ़िगर किए गए टारगेट के बारे में जानकारी देने का _एकमात्र _तरीका है. इनके नाम में "ट्रांज़िटिव" इसलिए है, क्योंकि आम तौर पर यह कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र का रोल-अप होता है.
आम तौर पर, Java के ट्रांज़िटिव इन्फ़ो प्रोवाइडर और Starlark के ट्रांज़िटिव इन्फ़ो प्रोवाइडर के बीच 1:1 का संबंध होता है. हालांकि, DefaultInfo इसका अपवाद है. यह FileProvider, FilesToRunProvider, और RunfilesProvider का कॉम्बिनेशन है. ऐसा इसलिए है, क्योंकि इस एपीआई को Java के एपीआई के सीधे तौर पर लिप्यंतरण के बजाय, ज़्यादा Starlark-ish माना गया था.
इनकी मुख्य वजह इनमें से कोई एक है:
- एक Java क्लास ऑब्जेक्ट. यह सुविधा सिर्फ़ उन प्रोवाइडर के लिए उपलब्ध है जिन्हें Starlark से ऐक्सेस नहीं किया जा सकता. ये कंपनियां,
TransitiveInfoProviderकी सबक्लास हैं. - एक स्ट्रिंग. यह लेगसी है और इसका इस्तेमाल करने से मना किया जाता है, क्योंकि इससे नाम के टकराव की समस्या हो सकती है. इस तरह की जानकारी देने वाली कंपनियां,
build.lib.packages.Infoकी सीधी सबक्लास होती हैं . - सेवा देने वाली कंपनी का सिंबल. इसे Starlark का इस्तेमाल करके,
provider()फ़ंक्शन से बनाया जा सकता है. नए प्रोवाइडर बनाने का यही तरीका सुझाया जाता है. इस सिंबल को Java मेंProvider.Keyइंस्टेंस के तौर पर दिखाया जाता है.
Java में लागू किए गए नए प्रोवाइडर को BuiltinProvider का इस्तेमाल करके लागू किया जाना चाहिए.
NativeProvider को बंद कर दिया गया है. हालांकि, हमने इसे अब तक नहीं हटाया है. साथ ही, TransitiveInfoProvider सबक्लास को Starlark से ऐक्सेस नहीं किया जा सकता.
कॉन्फ़िगर किए गए टारगेट
कॉन्फ़िगर किए गए टारगेट, RuleConfiguredTargetFactory के तौर पर लागू किए जाते हैं. Java में लागू की गई हर नियम क्लास के लिए, एक सबक्लास होती है. Starlark कॉन्फ़िगर किए गए टारगेट, StarlarkRuleConfiguredTargetUtil.buildRule() की मदद से बनाए जाते हैं .
कॉन्फ़िगर किए गए टारगेट फ़ैक्ट्री को अपनी रिटर्न वैल्यू बनाने के लिए, RuleConfiguredTargetBuilder का इस्तेमाल करना चाहिए. इसमें ये शामिल हैं:
- उनका
filesToBuild, "यह नियम जिन फ़ाइलों के सेट को दिखाता है" के बारे में धुंधली जानकारी. ये वे फ़ाइलें हैं जो कॉन्फ़िगर किए गए टारगेट के कमांड लाइन पर होने या genrule के srcs में होने पर बनती हैं. - उनकी रनफ़ाइलें, रेगुलर और डेटा.
- उनके आउटपुट ग्रुप. ये "फ़ाइलों के अन्य सेट" हैं जिन्हें नियम बना सकता है. इन्हें BUILD में filegroup नियम के output_group एट्रिब्यूट का इस्तेमाल करके ऐक्सेस किया जा सकता है. साथ ही, Java में
OutputGroupInfoprovider का इस्तेमाल करके भी इन्हें ऐक्सेस किया जा सकता है.
Runfiles
कुछ बाइनरी को चलाने के लिए, डेटा फ़ाइलों की ज़रूरत होती है. इसका एक अहम उदाहरण, ऐसे टेस्ट हैं जिनके लिए इनपुट फ़ाइलों की ज़रूरत होती है. Bazel में इसे "रनफ़ाइल" के कॉन्सेप्ट से दिखाया जाता है. "रनफ़ाइल्स ट्री" किसी बाइनरी के लिए डेटा फ़ाइलों का डायरेक्ट्री ट्री होता है. इसे फ़ाइल सिस्टम में, सिमलंक ट्री के तौर पर बनाया जाता है. इसमें अलग-अलग सिमलंक होते हैं, जो आउटपुट ट्री के सोर्स में मौजूद फ़ाइलों की ओर इशारा करते हैं.
रनफ़ाइल के सेट को Runfiles इंस्टेंस के तौर पर दिखाया जाता है. यह कॉन्सेप्ट के तौर पर, रनफ़ाइल्स ट्री में मौजूद किसी फ़ाइल के पाथ से लेकर, उसे दिखाने वाले Artifact इंस्टेंस तक का मैप होता है. यह एक Map से थोड़ा ज़्यादा जटिल है. इसकी दो वजहें हैं:
- ज़्यादातर मामलों में, किसी फ़ाइल का रनफ़ाइल पाथ, उसके execpath के जैसा ही होता है. हम इसका इस्तेमाल कुछ रैम को सेव करने के लिए करते हैं.
- रनफ़ाइल ट्री में कई तरह की लेगसी एंट्री होती हैं. इन्हें भी दिखाना ज़रूरी है.
रनफ़ाइलें, RunfilesProvider का इस्तेमाल करके इकट्ठा की जाती हैं: इस क्लास का एक इंस्टेंस, कॉन्फ़िगर किए गए टारगेट (जैसे कि लाइब्रेरी) और उसके ट्रांज़िटिव क्लोज़र के लिए ज़रूरी रनफ़ाइलों को दिखाता है. इन्हें नेस्ट किए गए सेट की तरह इकट्ठा किया जाता है. असल में, इन्हें नेस्ट किए गए सेट का इस्तेमाल करके लागू किया जाता है: हर टारगेट, अपनी डिपेंडेंसी की रनफ़ाइलों को यूनाइट करता है, उनमें से कुछ को जोड़ता है, और फिर नतीजे के तौर पर मिले सेट को डिपेंडेंसी ग्राफ़ में ऊपर की ओर भेजता है. RunfilesProvider इंस्टेंस में दो Runfiles इंस्टेंस होते हैं. एक तब होता है, जब नियम "data" एट्रिब्यूट के ज़रिए निर्भर होता है और दूसरा, हर तरह की इनकमिंग डिपेंडेंसी के लिए होता है. ऐसा इसलिए होता है, क्योंकि डेटा एट्रिब्यूट के ज़रिए किसी टारगेट पर निर्भर रहने पर, कभी-कभी अलग-अलग रनफ़ाइल दिखती हैं. यह लेगसी वर्शन का ऐसा व्यवहार है जिसे हम हटाना चाहते हैं. हालांकि, हम इसे अब तक हटा नहीं पाए हैं.
बाइनरी की रनफ़ाइल को RunfilesSupport के इंस्टेंस के तौर पर दिखाया जाता है. यह Runfiles से अलग है, क्योंकि RunfilesSupport को बनाया जा सकता है. हालांकि, Runfiles सिर्फ़ एक मैपिंग है. इसके लिए, इन अतिरिक्त कॉम्पोनेंट की ज़रूरत होती है:
- इनपुट रनफ़ाइल मेनिफ़ेस्ट. यह रनफ़ाइल ट्री का क्रम से दिया गया ब्यौरा है. इसका इस्तेमाल रनफ़ाइल ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. Bazel यह मानता है कि मेनिफ़ेस्ट के कॉन्टेंट में बदलाव होने पर ही रनफ़ाइल ट्री में बदलाव होता है.
- आउटपुट रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल रनटाइम लाइब्रेरी करती हैं. ये लाइब्रेरी, रनफ़ाइल ट्री को मैनेज करती हैं. खास तौर पर, Windows पर ऐसा होता है. Windows पर कभी-कभी सिंबॉलिक लिंक काम नहीं करते.
- रनफ़ाइल मिडलमैन. रनफ़ाइल्स ट्री बनाने के लिए, सिमलिंक ट्री और सिमलिंक जिस आर्टफ़ैक्ट की ओर इशारा करते हैं उसे बनाना ज़रूरी है. डिपेंडेंसी एज की संख्या कम करने के लिए, रनफ़ाइल मिडलमैन का इस्तेमाल किया जा सकता है. इससे इन सभी को दिखाया जा सकता है.
RunfilesSupportऑब्जेक्ट जिस बाइनरी के रनफ़ाइल को दिखाता है उसे चलाने के लिए, कमांड लाइन आर्ग्युमेंट.
आस्पेक्ट
एसपेक्ट, "डिपेंडेंसी ग्राफ़ में कंप्यूटेशन को नीचे की ओर ले जाने" का एक तरीका है. Bazel का इस्तेमाल करने वाले लोगों के लिए, इनके बारे में यहां बताया गया है. प्रोटोकॉल बफ़र, प्रेरणा देने वाला एक अच्छा उदाहरण है: proto_library नियम को किसी खास भाषा के बारे में पता नहीं होना चाहिए. हालांकि, किसी भी प्रोग्रामिंग भाषा में प्रोटोकॉल बफ़र मैसेज (प्रोटोकॉल बफ़र की “बुनियादी इकाई”) को लागू करने की प्रोसेस को proto_library नियम से जोड़ा जाना चाहिए, ताकि अगर एक ही भाषा में दो टारगेट एक ही प्रोटोकॉल बफ़र पर निर्भर हों, तो उसे सिर्फ़ एक बार बनाया जाए.
कॉन्फ़िगर किए गए टारगेट की तरह, इन्हें Skyframe में SkyValue के तौर पर दिखाया जाता है. साथ ही, इन्हें बनाने का तरीका, कॉन्फ़िगर किए गए टारगेट बनाने के तरीके से मिलता-जुलता है: इनमें ConfiguredAspectFactory नाम की फ़ैक्ट्री क्लास होती है, जिसके पास RuleContext का ऐक्सेस होता है. हालांकि, कॉन्फ़िगर किए गए टारगेट फ़ैक्ट्रियों के उलट, इसे उस कॉन्फ़िगर किए गए टारगेट के बारे में भी पता होता है जिससे यह जुड़ी होती है और इसके प्रोवाइडर के बारे में भी पता होता है.
डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजे गए पहलुओं का सेट, Attribute.Builder.aspects() फ़ंक्शन का इस्तेमाल करके हर एट्रिब्यूट के लिए तय किया जाता है. इस प्रोसेस में, कुछ ऐसी क्लास शामिल होती हैं जिनके नाम मिलते-जुलते हैं:
AspectClass, पहलू को लागू करने का तरीका है. यह Java में हो सकता है (इस मामले में, यह एक सबक्लास है) या Starlark में (इस मामले में, यहStarlarkAspectClassका एक इंस्टेंस है). यहRuleConfiguredTargetFactoryके जैसा ही है.AspectDefinitionआसपेक्ट की डेफ़िनिशन है. इसमें वे कंपनियां शामिल होती हैं जिनसे आसपेक्ट को काम करने के लिए ज़रूरी डेटा मिलता है और वे कंपनियां भी शामिल होती हैं जिन्हें आसपेक्ट डेटा उपलब्ध कराता है. साथ ही, इसमें आसपेक्ट को लागू करने का रेफ़रंस भी शामिल होता है. जैसे, सहीAspectClassइंस्टेंस. यहRuleClassके जैसा है.AspectParameters, किसी ऐसे पहलू को पैरामीटर के तौर पर सेट करने का तरीका है जिसे डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजा जाता है. फ़िलहाल, यह स्ट्रिंग से स्ट्रिंग मैप है. इसका एक अच्छा उदाहरण प्रोटोकॉल बफ़र है: अगर किसी भाषा में एक से ज़्यादा एपीआई हैं, तो प्रोटोकॉल बफ़र को किस एपीआई के लिए बनाया जाना चाहिए, इसकी जानकारी डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजी जानी चाहिए.Aspect, उस सभी डेटा को दिखाता है जिसकी ज़रूरत, किसी ऐसे पहलू का हिसाब लगाने के लिए होती है जो डिपेंडेंसी ग्राफ़ में नीचे की ओर बढ़ता है. इसमें पहलू की क्लास, उसकी परिभाषा, और उसके पैरामीटर शामिल होते हैं.RuleAspectएक ऐसा फ़ंक्शन है जो यह तय करता है कि किसी नियम के किन पहलुओं को लागू किया जाना चाहिए. यहRule->Aspectफ़ंक्शन है.
एक ऐसी समस्या भी है जिसके बारे में शायद आपको पहले से न पता हो. वह यह है कि पहलुओं को अन्य पहलुओं से जोड़ा जा सकता है. उदाहरण के लिए, Java IDE के लिए क्लासपाथ इकट्ठा करने वाला पहलू, क्लासपाथ पर मौजूद सभी .jar फ़ाइलों के बारे में जानना चाहेगा. हालांकि, इनमें से कुछ प्रोटोकॉल बफ़र हैं. ऐसे में, IDE पहलू, (proto_library नियम + Java proto पहलू) पेयर से अटैच होना चाहेगा.
क्लास AspectCollection में, पहलुओं की जटिलता को कैप्चर किया जाता है.
प्लैटफ़ॉर्म और टूलचेन
Bazel, एक से ज़्यादा प्लैटफ़ॉर्म पर काम करने वाले बिल्ड के साथ काम करता है. इसका मतलब है कि ऐसे बिल्ड में कई आर्किटेक्चर हो सकते हैं जहां बिल्ड ऐक्शन चलते हैं. साथ ही, कई आर्किटेक्चर के लिए कोड बनाया जाता है. Bazel की भाषा में, इन आर्किटेक्चर को प्लैटफ़ॉर्म कहा जाता है. पूरी जानकारी के लिए, यहां जाएं
किसी प्लैटफ़ॉर्म के बारे में, पाबंदी की सेटिंग (जैसे कि "सीपीयू आर्किटेक्चर" का कॉन्सेप्ट) से पाबंदी की वैल्यू (जैसे कि x86_64 जैसा कोई खास सीपीयू) तक की कुंजी-वैल्यू मैपिंग से पता चलता है. हमारे पास @platforms रिपॉज़िटरी में, सबसे ज़्यादा इस्तेमाल की जाने वाली कंस्ट्रेंट सेटिंग और वैल्यू की "डिक्शनरी" है.
टूलचेन का कॉन्सेप्ट इस बात पर निर्भर करता है कि बिल्ड किन प्लैटफ़ॉर्म पर चल रहा है और किन प्लैटफ़ॉर्म को टारगेट किया जा रहा है. इसके लिए, अलग-अलग कंपाइलर का इस्तेमाल करना पड़ सकता है. उदाहरण के लिए, कोई खास C++ टूलचेन किसी खास ओएस पर चल सकता है और कुछ अन्य ओएस को टारगेट कर सकता है. Bazel को सेट किए गए एक्ज़ीक्यूशन और टारगेट प्लैटफ़ॉर्म के आधार पर, इस्तेमाल किए गए C++ कंपाइलर का पता लगाना होगा (टूलचेन के बारे में दस्तावेज़ यहां उपलब्ध है).
इसके लिए, टूलचेन को एक्ज़ीक्यूशन और टारगेट प्लैटफ़ॉर्म की उन पाबंदियों के साथ एनोटेट किया जाता है जिन्हें वे सपोर्ट करते हैं. इसके लिए, टूलचेन की परिभाषा को दो हिस्सों में बांटा गया है:
toolchain()एक ऐसा नियम है जिसमें टूलचेन के एक्ज़ीक्यूशन और टारगेट की उन शर्तों के बारे में बताया जाता है जिन्हें टूलचेन सपोर्ट करता है. साथ ही, यह भी बताया जाता है कि यह किस तरह की टूलचेन है. जैसे, C++ या Java. बाद वाली जानकारी कोtoolchain_type()नियम से दिखाया जाता है- भाषा के हिसाब से नियम, जिसमें टूलचेन के बारे में जानकारी दी गई हो. जैसे,
cc_toolchain()
ऐसा इसलिए किया जाता है, क्योंकि हमें हर टूलचेन की सीमाओं के बारे में जानना होता है, ताकि टूलचेन रिज़ॉल्यूशन किया जा सके. साथ ही, भाषा के हिसाब से *_toolchain() नियमों में इससे ज़्यादा जानकारी होती है. इसलिए, इन्हें लोड होने में ज़्यादा समय लगता है.
एक्ज़ीक्यूशन प्लैटफ़ॉर्म को इनमें से किसी एक तरीके से तय किया जाता है:
- WORKSPACE फ़ाइल में
register_execution_platforms()फ़ंक्शन का इस्तेमाल करके - कमांड लाइन पर, --extra_execution_platforms कमांड लाइन विकल्प का इस्तेमाल करके
उपलब्ध एक्ज़ीक्यूशन प्लैटफ़ॉर्म का सेट, RegisteredExecutionPlatformsFunction में कैलकुलेट किया जाता है .
कॉन्फ़िगर किए गए टारगेट के लिए टारगेट प्लैटफ़ॉर्म, PlatformOptions.computeTargetPlatform() तय करता है . यह प्लैटफ़ॉर्म की सूची है, क्योंकि हम आने वाले समय में कई टारगेट प्लैटफ़ॉर्म के साथ काम करना चाहते हैं. हालांकि, इसे अभी लागू नहीं किया गया है.
कॉन्फ़िगर किए गए टारगेट के लिए इस्तेमाल की जाने वाली टूलचेन का सेट, ToolchainResolutionFunction तय करता है. यह इन पर निर्भर करता है:
- रजिस्टर की गई टूलचेन का सेट (WORKSPACE फ़ाइल और कॉन्फ़िगरेशन में)
- कॉन्फ़िगरेशन में, एक्ज़ीक्यूशन और टारगेट प्लैटफ़ॉर्म
- टूलचेन के टाइप का वह सेट जिसकी ज़रूरत कॉन्फ़िगर किए गए टारगेट को होती है (
UnloadedToolchainContextKey) - कॉन्फ़िगर किए गए टारगेट (
exec_compatible_withएट्रिब्यूट) और कॉन्फ़िगरेशन (--experimental_add_exec_constraints_to_targets) के लिए, एक्ज़ीक्यूशन प्लैटफ़ॉर्म की पाबंदियों का सेट,UnloadedToolchainContextKeyमें
इसका नतीजा UnloadedToolchainContext होता है. यह टूलचेन टाइप (ToolchainTypeInfo इंस्टेंस के तौर पर दिखाया गया है) से लेकर चुनी गई टूलचेन के लेबल तक का मैप होता है. इसे "अनलोड किया गया" कहा जाता है, क्योंकि इसमें टूलचेन शामिल नहीं होते, सिर्फ़ उनके लेबल शामिल होते हैं.
इसके बाद, टूलचेन को ResolvedToolchainContext.load() का इस्तेमाल करके लोड किया जाता है. साथ ही, कॉन्फ़िगर किए गए उस टारगेट के लिए इनका इस्तेमाल किया जाता है जिसने इनके लिए अनुरोध किया था.
हमारे पास एक लेगसी सिस्टम भी है. यह सिस्टम, एक ही "होस्ट" कॉन्फ़िगरेशन पर निर्भर करता है. साथ ही, टारगेट कॉन्फ़िगरेशन को अलग-अलग कॉन्फ़िगरेशन फ़्लैग से दिखाया जाता है. जैसे, --cpu . हम धीरे-धीरे ऊपर दिए गए सिस्टम पर स्विच कर रहे हैं. ऐसे मामलों को मैनेज करने के लिए जहां लोग लेगसी कॉन्फ़िगरेशन की वैल्यू पर भरोसा करते हैं, हमने प्लैटफ़ॉर्म मैपिंग लागू की हैं. इससे लेगसी फ़्लैग और नए स्टाइल वाले प्लैटफ़ॉर्म की पाबंदियों के बीच अनुवाद किया जा सकता है.
उनका कोड PlatformMappingFunction में है और इसमें नॉन-स्टरलार्क "छोटी
भाषा" का इस्तेमाल किया गया है.
कंस्ट्रेंट
कभी-कभी कोई व्यक्ति किसी टारगेट को सिर्फ़ कुछ प्लैटफ़ॉर्म के साथ काम करने वाला बनाना चाहता है. Bazel में, इस काम को पूरा करने के लिए कई तरीके उपलब्ध हैं:
- नियम से जुड़ी शर्तें
environment_group()/environment()- प्लैटफ़ॉर्म से जुड़ी पाबंदियां
नियम से जुड़ी पाबंदियों का इस्तेमाल ज़्यादातर Google में Java के नियमों के लिए किया जाता है. ये अब उपलब्ध नहीं हैं और Bazel में भी उपलब्ध नहीं हैं. हालांकि, सोर्स कोड में इसके रेफ़रंस हो सकते हैं. इस सुविधा को कंट्रोल करने वाली एट्रिब्यूट को constraints= कहा जाता है .
environment_group() और environment()
ये नियम, लेगसी सिस्टम के तहत काम करते हैं और इनका इस्तेमाल बड़े पैमाने पर नहीं किया जाता.
सभी बिल्ड नियम यह एलान कर सकते हैं कि उन्हें किन "एनवायरमेंट" के लिए बनाया जा सकता है. यहां "एनवायरमेंट" का मतलब environment() नियम के इंस्टेंस से है.
किसी नियम के लिए, काम करने वाले अलग-अलग एनवायरमेंट तय करने के कई तरीके हैं:
restricted_to=एट्रिब्यूट के ज़रिए. यह स्पेसिफ़िकेशन का सबसे सीधा फ़ॉर्म है. इससे यह पता चलता है कि यह नियम, इस ग्रुप के लिए किन एनवायरमेंट के साथ काम करता है.compatible_with=एट्रिब्यूट के ज़रिए. इससे उन एनवायरमेंट के बारे में पता चलता है जिनमें कोई नियम काम करता है. इनमें "स्टैंडर्ड" एनवायरमेंट के अलावा, वे एनवायरमेंट भी शामिल होते हैं जिनमें नियम डिफ़ॉल्ट रूप से काम करते हैं.- पैकेज-लेवल के एट्रिब्यूट
default_restricted_to=औरdefault_compatible_with=के ज़रिए. environment_group()नियमों में डिफ़ॉल्ट स्पेसिफ़िकेशन के ज़रिए. हर एनवायरमेंट, थीम के हिसाब से मिलते-जुलते पीयर के ग्रुप से जुड़ा होता है. जैसे, "सीपीयू आर्किटेक्चर", "JDK वर्शन" या "मोबाइल ऑपरेटिंग सिस्टम". एनवायरमेंट ग्रुप की परिभाषा में यह शामिल होता है कि अगरrestricted_to=/environment()एट्रिब्यूट से कोई और एनवायरमेंट तय नहीं किया गया है, तो इनमें से कौनसे एनवायरमेंट "डिफ़ॉल्ट" तौर पर काम करने चाहिए. ऐसे किसी भी एट्रिब्यूट के बिना बनाए गए नियम में, सभी डिफ़ॉल्ट वैल्यू शामिल होती हैं.- नियम क्लास के डिफ़ॉल्ट के ज़रिए. इससे, दी गई नियम क्लास के सभी इंस्टेंस के लिए ग्लोबल डिफ़ॉल्ट सेटिंग बदल जाती हैं. इसका इस्तेमाल, उदाहरण के लिए, सभी
*_testनियमों की जांच करने के लिए किया जा सकता है. इसके लिए, हर इंस्टेंस को इस सुविधा के बारे में साफ़ तौर पर बताने की ज़रूरत नहीं होती.
environment() को एक सामान्य नियम के तौर पर लागू किया जाता है. वहीं, environment_group(), Target का सब-क्लास है, लेकिन Rule (EnvironmentGroup) का नहीं. साथ ही, यह एक ऐसा फ़ंक्शन है जो Starlark (StarlarkLibrary.environmentGroup()) से डिफ़ॉल्ट रूप से उपलब्ध होता है. इससे एक ऐसा टारगेट बनता है जिसका नाम फ़ंक्शन के नाम पर रखा जाता है. ऐसा इसलिए किया जाता है, ताकि साइक्लिक डिपेंडेंसी से बचा जा सके. साइक्लिक डिपेंडेंसी तब होती है, जब हर एनवायरमेंट को यह बताना होता है कि वह किस एनवायरमेंट ग्रुप से जुड़ा है और हर एनवायरमेंट ग्रुप को अपने डिफ़ॉल्ट एनवायरमेंट के बारे में बताना होता है.
--target_environment कमांड-लाइन विकल्प का इस्तेमाल करके, किसी बिल्ड को किसी खास एनवायरमेंट तक सीमित किया जा सकता है.
पाबंदी की जांच करने की सुविधा, RuleContextConstraintSemantics और TopLevelConstraintSemantics में लागू की गई है.
प्लैटफ़ॉर्म से जुड़ी पाबंदियां
फ़िलहाल, यह बताने का "आधिकारिक" तरीका यह है कि कोई टारगेट किन प्लैटफ़ॉर्म के साथ काम करता है. इसके लिए, टूलचेन और प्लैटफ़ॉर्म के बारे में बताने के लिए इस्तेमाल की गई पाबंदियों का इस्तेमाल किया जाता है. इसकी समीक्षा पुल के अनुरोध #10945 में की जा रही है.
किसको दिखे
अगर आपको Google जैसे किसी बड़े संगठन में, कई डेवलपर के साथ मिलकर बड़े कोड बेस पर काम करना है, तो आपको यह पक्का करना होगा कि कोई भी डेवलपर आपके कोड पर निर्भर न रहे. ऐसा न करने पर, हायरम के नियम के मुताबिक, लोग उन व्यवहारों पर भरोसा करने लगेंगे जिन्हें आपने लागू करने से जुड़ी जानकारी माना था.
Bazel, विज़िबिलिटी नाम की सुविधा के ज़रिए इसे सपोर्ट करता है: यह एलान किया जा सकता है कि किसी खास टारगेट पर सिर्फ़ विज़िबिलिटी एट्रिब्यूट का इस्तेमाल करके भरोसा किया जा सकता है. यह एट्रिब्यूट थोड़ा खास है, क्योंकि इसमें लेबल की सूची होती है. हालांकि, ये लेबल किसी खास टारगेट का पॉइंटर होने के बजाय, पैकेज के नामों पर पैटर्न को कोड कर सकते हैं. (हाँ, यह डिज़ाइन से जुड़ी समस्या है.)
इसे इन जगहों पर लागू किया गया है:
RuleVisibilityइंटरफ़ेस, दिखने की स्थिति के बारे में जानकारी दिखाता है. यह एक कॉन्स्टेंट (पूरी तरह से सार्वजनिक या पूरी तरह से निजी) या लेबल की सूची हो सकती है.- लेबल, पैकेज ग्रुप (पैकेज की पहले से तय की गई सूची), सीधे तौर पर पैकेज (
//pkg:__pkg__) या पैकेज के सबट्री (//pkg:__subpackages__) को रेफ़र कर सकते हैं. यह कमांड लाइन सिंटैक्स से अलग है, जिसमें//pkg:*या//pkg/...का इस्तेमाल किया जाता है. - पैकेज ग्रुप को उनके टारगेट (
PackageGroup) और कॉन्फ़िगर किए गए टारगेट (PackageGroupConfiguredTarget) के तौर पर लागू किया जाता है. अगर हम चाहें, तो इन्हें सामान्य नियमों से बदला जा सकता है. इनके लॉजिक को लागू करने के लिए, इनका इस्तेमाल किया जाता है:PackageSpecification, जो//pkg/...जैसे किसी एक पैटर्न से मेल खाता है;PackageGroupContents, जो किसी एकpackage_groupकेpackagesएट्रिब्यूट से मेल खाता है; औरPackageSpecificationProvider, जो किसीpackage_groupऔर उसके ट्रांज़िटिवincludesको एग्रीगेट करता है. - विज़िबिलिटी लेबल की सूचियों से डिपेंडेंसी में कन्वर्ज़न,
DependencyResolver.visitTargetVisibilityऔर कुछ अन्य जगहों पर किया जाता है. - असल जांच
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()में की जाती है
नेस्ट किए गए सेट
अक्सर, कॉन्फ़िगर किया गया टारगेट, अपनी डिपेंडेंसी से फ़ाइलों का एक सेट इकट्ठा करता है. साथ ही, इसमें अपनी फ़ाइलें जोड़ता है. इसके बाद, यह एग्रीगेट किए गए सेट को ट्रांज़िटिव इन्फ़ो प्रोवाइडर में रैप करता है, ताकि इस पर निर्भर कॉन्फ़िगर किए गए टारगेट भी ऐसा कर सकें. उदाहरण:
- बिल्ड के लिए इस्तेमाल की गई C++ हेडर फ़ाइलें
cc_libraryके ट्रांज़िटिव क्लोज़र को दिखाने वाली ऑब्जेक्ट फ़ाइलें- .jar फ़ाइलों का वह सेट जिसे Java के नियम को कंपाइल या चलाने के लिए, क्लासपाथ पर होना चाहिए
- Python के नियम के ट्रांज़िटिव क्लोज़र में Python फ़ाइलों का सेट
अगर हम इस काम को सामान्य तरीके से करते हैं, जैसे कि List या Set का इस्तेमाल करके, तो मेमोरी का इस्तेमाल काफ़ी ज़्यादा होगा: अगर नियमों की कोई चेन है और हर नियम एक फ़ाइल जोड़ता है, तो हमारे पास 1+2+...+N कलेक्शन मेंबर होंगे.
इस समस्या को हल करने के लिए, हमने NestedSet का कॉन्सेप्ट तैयार किया है. यह एक ऐसा डेटा स्ट्रक्चर है जो NestedSet के अन्य इंस्टेंस और इसके कुछ सदस्यों से मिलकर बना होता है. इस तरह, यह सेट का डायरेक्टेड एसाइक्लिक ग्राफ़ बनाता है. इनमें बदलाव नहीं किया जा सकता और इनके सदस्यों को दोहराया जा सकता है. हम कई इटरेटिव ऑर्डर (NestedSet.Order) तय करते हैं: प्रीऑर्डर, पोस्टऑर्डर, टोपोलॉजिकल (नोड हमेशा अपने पूर्वजों के बाद आता है) और "कोई फ़र्क़ नहीं पड़ता, लेकिन यह हर बार एक जैसा होना चाहिए".
इसी डेटा स्ट्रक्चर को Starlark में depset कहा जाता है.
आर्टफ़ैक्ट और कार्रवाइयां
असल बिल्ड में, कमांड का एक सेट होता है. इन कमांड को चलाने पर, उपयोगकर्ता को मनमुताबिक आउटपुट मिलता है. कमांड को Action क्लास के इंस्टेंस के तौर पर दिखाया जाता है. साथ ही, फ़ाइलों को Artifact क्लास के इंस्टेंस के तौर पर दिखाया जाता है. इन्हें "ऐक्शन ग्राफ़" नाम के एक बाइपार्टाइट, डायरेक्टेड, असाइकलिक ग्राफ़ में व्यवस्थित किया जाता है.
आर्टफ़ैक्ट दो तरह के होते हैं: सोर्स आर्टफ़ैक्ट (ऐसे आर्टफ़ैक्ट जो Bazel के एक्ज़ीक्यूट होने से पहले उपलब्ध होते हैं) और डिराइव किए गए आर्टफ़ैक्ट (ऐसे आर्टफ़ैक्ट जिन्हें बनाने की ज़रूरत होती है). डेराइव किए गए आर्टफ़ैक्ट कई तरह के हो सकते हैं:
- **नियमित आर्टफ़ैक्ट. **इनकी जांच यह देखने के लिए की जाती है कि ये अप-टू-डेट हैं या नहीं. इसके लिए, इनके चेकसम का हिसाब लगाया जाता है. mtime को शॉर्टकट के तौर पर इस्तेमाल किया जाता है. अगर फ़ाइल का ctime नहीं बदला है, तो हम उसके चेकसम का हिसाब नहीं लगाते.
- ऐसे सिंबॉलिक लिंक आर्टफ़ैक्ट जिनका समाधान नहीं हुआ है. इनकी जांच readlink() को कॉल करके की जाती है, ताकि यह पता चल सके कि ये अप-टू-डेट हैं या नहीं. सामान्य आर्टफ़ैक्ट के उलट, ये डैंगलिंग सिंबल लिंक हो सकते हैं. आम तौर पर, इसका इस्तेमाल उन मामलों में किया जाता है जहां कुछ फ़ाइलों को किसी तरह के संग्रह में पैक किया जाता है.
- ट्री आर्टफ़ैक्ट. ये सिर्फ़ फ़ाइलें नहीं हैं, बल्कि डायरेक्ट्री ट्री हैं. इनकी जांच यह देखने के लिए की जाती है कि ये अप-टू-डेट हैं या नहीं. इसके लिए, इनमें मौजूद फ़ाइलों और उनके कॉन्टेंट की जांच की जाती है. इन्हें
TreeArtifactके तौर पर दिखाया जाता है. - मेटाडेटा के आर्टफ़ैक्ट में बदलाव नहीं किया गया है. इन आर्टफ़ैक्ट में किए गए बदलावों से, फिर से बनाने की प्रोसेस ट्रिगर नहीं होती. इसका इस्तेमाल सिर्फ़ बिल्ड स्टैंप की जानकारी के लिए किया जाता है: हम सिर्फ़ इसलिए फिर से बिल्ड नहीं करना चाहते, क्योंकि मौजूदा समय बदल गया है.
सोर्स आर्टफ़ैक्ट, ट्री आर्टफ़ैक्ट या अनरिज़ॉल्व्ड सिमलंक आर्टफ़ैक्ट क्यों नहीं हो सकते, इसकी कोई बुनियादी वजह नहीं है. फ़िलहाल, हमने इसे लागू नहीं किया है. हालांकि, हमें इसे लागू करना चाहिए. BUILD फ़ाइल में सोर्स डायरेक्ट्री को रेफ़रंस करना, Bazel से जुड़ी कुछ ऐसी समस्याओं में से एक है जो लंबे समय से ठीक नहीं हुई हैं. हमारे पास एक ऐसा तरीका है जो कुछ हद तक काम करता है. इसे BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM प्रॉपर्टी की मदद से चालू किया जाता है
Artifact का एक अहम उदाहरण बिचौलिए हैं. इन्हें Artifact
ऐसे इंस्टेंस के तौर पर दिखाया जाता है जो MiddlemanAction के आउटपुट होते हैं. इनका इस्तेमाल कुछ खास मामलों में किया जाता है:
- आर्टफ़ैक्ट को एक साथ ग्रुप करने के लिए, एग्रीगेटिंग मिडलमैन का इस्तेमाल किया जाता है. ऐसा इसलिए किया जाता है, ताकि अगर कई कार्रवाइयों में एक ही बड़े इनपुट सेट का इस्तेमाल किया जाता है, तो हमारे पास N*M डिपेंडेंसी एज न हों, सिर्फ़ N+M (इन्हें नेस्ट किए गए सेट से बदला जा रहा है)
- शेड्यूलिंग डिपेंडेंसी मिडलमैन यह पक्का करते हैं कि कोई कार्रवाई दूसरी कार्रवाई से पहले हो.
इनका इस्तेमाल ज़्यादातर लिंटिंग के लिए किया जाता है. हालांकि, इनका इस्तेमाल C++ कंपाइलेशन के लिए भी किया जाता है. इसके बारे में जानने के लिए,
CcCompilationContext.createMiddleman()देखें - रनफ़ाइल मिडलमैन का इस्तेमाल, रनफ़ाइल ट्री की मौजूदगी को पक्का करने के लिए किया जाता है. इससे, किसी को भी आउटपुट मेनिफ़ेस्ट और रनफ़ाइल ट्री से रेफ़र किए गए हर आर्टफ़ैक्ट पर अलग से निर्भर नहीं रहना पड़ता.
कार्रवाइयों को ऐसे कमांड के तौर पर सबसे अच्छी तरह से समझा जा सकता है जिन्हें चलाने की ज़रूरत होती है. साथ ही, यह भी समझा जा सकता है कि इसके लिए किस एनवायरमेंट की ज़रूरत होती है और यह किस तरह के आउटपुट जनरेट करता है. किसी कार्रवाई के ब्यौरे में ये मुख्य कॉम्पोनेंट शामिल होते हैं:
- वह कमांड लाइन जिसे चलाना है
- इनपुट आर्टफ़ैक्ट की ज़रूरत होती है
- सेट किए जाने वाले एनवायरमेंट वैरिएबल
- ऐसी व्याख्याएं जिनमें उस एनवायरमेंट (जैसे कि प्लैटफ़ॉर्म) के बारे में बताया गया हो जिसमें इसे चलाना है \
इसके अलावा, कुछ अन्य खास मामले भी हैं. जैसे, ऐसी फ़ाइल लिखना जिसका कॉन्टेंट Bazel को पता है. ये AbstractAction के सबक्लास हैं. ज़्यादातर कार्रवाइयां SpawnAction या StarlarkAction होती हैं. ये दोनों एक ही हैं और इन्हें अलग-अलग क्लास में नहीं रखा जाना चाहिए. हालांकि, Java और C++ में कार्रवाई के अपने टाइप होते हैं (JavaCompileAction, CppCompileAction, और CppLinkAction).
हमारा मकसद, सभी चीज़ों को SpawnAction पर ले जाना है. JavaCompileAction काफ़ी हद तक ऐसा कर सकता है. हालांकि, C++ के लिए .d फ़ाइल पार्स करने और स्कैन करने की वजह से, यह थोड़ा खास मामला है.
ऐक्शन ग्राफ़ को ज़्यादातर Skyframe ग्राफ़ में "एम्बेड" किया जाता है: कॉन्सेप्ट के तौर पर, किसी ऐक्शन के एक्ज़ीक्यूशन को ActionExecutionFunction के इनवोकेशन के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से Skyframe डिपेंडेंसी एज की मैपिंग के बारे में ActionExecutionFunction.getInputDeps() और Artifact.key() में बताया गया है. साथ ही, Skyframe एज की संख्या कम रखने के लिए, इसमें कुछ ऑप्टिमाइज़ेशन किए गए हैं:
- डिराइव किए गए आर्टफ़ैक्ट के अपने
SkyValueनहीं होते. इसके बजाय,Artifact.getGeneratingActionKey()का इस्तेमाल करके, उस कार्रवाई के लिए कुंजी का पता लगाया जाता है जो इसे जनरेट करती है - नेस्ट किए गए सेट की अपनी Skyframe कुंजी होती है.
शेयर की गई कार्रवाइयां
कुछ कार्रवाइयां, कॉन्फ़िगर किए गए कई टारगेट से जनरेट होती हैं. स्टार्लार्क के नियम ज़्यादा सीमित होते हैं, क्योंकि उन्हें सिर्फ़ अपनी डिराइव की गई कार्रवाइयों को उस डायरेक्ट्री में रखने की अनुमति होती है जिसे उनके कॉन्फ़िगरेशन और पैकेज से तय किया जाता है. हालांकि, ऐसा होने पर भी एक ही पैकेज के नियमों में टकराव हो सकता है. वहीं, Java में लागू किए गए नियम, डिराइव किए गए आर्टफ़ैक्ट को कहीं भी रख सकते हैं.
इसे एक गलत सुविधा माना जाता है. हालांकि, इसे हटाना बहुत मुश्किल है, क्योंकि इससे एक्ज़ीक्यूशन के समय में काफ़ी बचत होती है. उदाहरण के लिए, जब किसी सोर्स फ़ाइल को किसी तरह से प्रोसेस करने की ज़रूरत होती है और उस फ़ाइल को कई नियमों से रेफ़रंस किया जाता है (हैंडवेव-हैंडवेव). हालांकि, इसके लिए कुछ रैम की ज़रूरत होती है: शेयर की गई हर कार्रवाई के इंस्टेंस को मेमोरी में अलग से सेव करना होता है.
अगर दो कार्रवाइयों से एक ही आउटपुट फ़ाइल जनरेट होती है, तो वे एक जैसी होनी चाहिए:
उनमें एक जैसे इनपुट, एक जैसे आउटपुट, और एक जैसी कमांड लाइन होनी चाहिए. यह
समानता संबंध, Actions.canBeShared() में लागू किया जाता है. इसकी पुष्टि, विश्लेषण और एक्ज़ीक्यूशन फ़ेज़ के बीच की जाती है. इसके लिए, हर कार्रवाई को देखा जाता है.
इसे SkyframeActionExecutor.findAndStoreArtifactConflicts() में लागू किया गया है. साथ ही, Bazel में यह कुछ ऐसी जगहों में से एक है जहां बिल्ड का "ग्लोबल" व्यू देखना ज़रूरी होता है.
एक्ज़ीक्यूशन फ़ेज़
इस दौरान Bazel, बिल्ड ऐक्शन शुरू करता है. जैसे, आउटपुट जनरेट करने वाले कमांड.
विश्लेषण के चरण के बाद, Bazel सबसे पहले यह तय करता है कि किन आर्टफ़ैक्ट को बनाना है. इसके लिए लॉजिक, TopLevelArtifactHelper में कोड किया गया है. आसान शब्दों में कहें, तो यह कमांड लाइन पर कॉन्फ़िगर किए गए टारगेट और खास आउटपुट ग्रुप के कॉन्टेंट का filesToBuild है. इसका मकसद, "अगर यह टारगेट कमांड लाइन पर है, तो इन आर्टफ़ैक्ट को बनाएं" को साफ़ तौर पर बताना है.
अगला चरण, एक्ज़ीक्यूशन रूट बनाना है. Bazel के पास फ़ाइल सिस्टम (--package_path) में अलग-अलग जगहों से सोर्स पैकेज पढ़ने का विकल्प होता है. इसलिए, इसे स्थानीय तौर पर लागू की गई कार्रवाइयों के लिए, पूरा सोर्स ट्री उपलब्ध कराना होता है. इसे क्लास SymlinkForest मैनेज करती है. यह विश्लेषण के दौरान इस्तेमाल किए गए हर टारगेट पर ध्यान देती है. साथ ही, एक ऐसा डायरेक्ट्री ट्री बनाती है जो हर पैकेज को उसकी असल जगह से इस्तेमाल किए गए टारगेट के साथ सिमलंक करता है. इसके अलावा, कमांड को सही पाथ पास किए जा सकते हैं. इसके लिए, --package_path को ध्यान में रखें.
यह इसलिए सही नहीं है, क्योंकि:
- जब किसी पैकेज को एक पैकेज पाथ एंट्री से दूसरी एंट्री में ले जाया जाता है, तो यह ऐक्शन कमांड लाइन बदलता है (पहले ऐसा अक्सर होता था)
- अगर कोई कार्रवाई रिमोटली की जाती है, तो कमांड लाइन अलग होती है. वहीं, अगर कार्रवाई स्थानीय तौर पर की जाती है, तो कमांड लाइन अलग होती है
- इसके लिए, इस्तेमाल किए जा रहे टूल के हिसाब से कमांड लाइन ट्रांसफ़ॉर्मेशन की ज़रूरत होती है (जैसे, Java क्लासपाथ और C++ में शामिल पाथ के बीच के अंतर पर ध्यान दें)
- किसी कार्रवाई की कमांड लाइन बदलने से, उसकी ऐक्शन कैश मेमोरी की एंट्री अमान्य हो जाती है
--package_pathको धीरे-धीरे बंद किया जा रहा है
इसके बाद, Bazel ऐक्शन ग्राफ़ (द्विभागी, डायरेक्टेड ग्राफ़, जिसमें ऐक्शन और उनके इनपुट और आउटपुट आर्टफ़ैक्ट शामिल होते हैं) को ट्रैवर्स करना और ऐक्शन चलाना शुरू कर देता है.
हर कार्रवाई को SkyValue
क्लास ActionExecutionValue के इंस्टेंस के तौर पर दिखाया जाता है.
कार्रवाई करने में ज़्यादा समय लगता है. इसलिए, हमारे पास कैश मेमोरी की कुछ लेयर होती हैं. इन्हें Skyframe के पीछे से ऐक्सेस किया जा सकता है:
ActionExecutionFunction.stateMapमें Skyframe को फिर से शुरू करने का डेटा होता है, ताकिActionExecutionFunctionको कम कीमत पर फिर से शुरू किया जा सके- लोकल ऐक्शन कैश मेमोरी में, फ़ाइल सिस्टम की स्थिति के बारे में डेटा होता है
- रिमोट एक्ज़ीक्यूशन सिस्टम में आम तौर पर, अपनी कैश मेमोरी भी होती है
लोकल ऐक्शन कैश मेमोरी
यह कैश, Skyframe के पीछे मौजूद एक और लेयर है. अगर Skyframe में किसी कार्रवाई को फिर से लागू किया जाता है, तो भी यह लोकल ऐक्शन कैश में हिट हो सकता है. यह लोकल फ़ाइल सिस्टम की स्थिति को दिखाता है. इसे डिस्क पर क्रम से लगाया जाता है. इसका मतलब है कि जब कोई नया Bazel सर्वर शुरू होता है, तो Skyframe ग्राफ़ खाली होने के बावजूद, लोकल ऐक्शन कैश मेमोरी से हिट मिल सकते हैं.
इस कैश मेमोरी में हिट की जांच करने के लिए, ActionCacheChecker.getTokenIfNeedToExecute() तरीके का इस्तेमाल किया जाता है .
इसके नाम के उलट, यह मैप, किसी डिराइव किए गए आर्टफ़ैक्ट के पाथ को उस कार्रवाई से मैप करता है जिसने इसे बनाया है. इस कार्रवाई के बारे में यहां बताया गया है:
- इनपुट और आउटपुट फ़ाइलों का सेट और उनका चेकसम
- इसकी "ऐक्शन की", जो आम तौर पर वह कमांड लाइन होती है जिसे लागू किया गया था. हालांकि, आम तौर पर यह उन सभी चीज़ों को दिखाती है जिन्हें इनपुट फ़ाइलों के चेकसम से कैप्चर नहीं किया जाता. जैसे,
FileWriteActionके लिए, यह लिखे गए डेटा का चेकसम होता है
इसके अलावा, “टॉप-डाउन ऐक्शन कैश मेमोरी” नाम की एक सुविधा भी है. यह सुविधा अभी भी डेवलपमेंट के चरण में है. यह ट्रांज़िटिव हैश का इस्तेमाल करती है, ताकि कैश मेमोरी में बार-बार न जाना पड़े.
इनपुट डिस्कवरी और इनपुट प्रूनिंग
कुछ कार्रवाइयां, सिर्फ़ इनपुट का सेट होने से ज़्यादा मुश्किल होती हैं. किसी कार्रवाई के इनपुट के सेट में दो तरह से बदलाव किए जा सकते हैं:
- कार्रवाई शुरू होने से पहले, उसे नए इनपुट मिल सकते हैं या वह यह तय कर सकती है कि उसके कुछ इनपुट ज़रूरी नहीं हैं. इसका सबसे अच्छा उदाहरण C++ है. C++ में, यह अनुमान लगाना बेहतर होता है कि कोई C++ फ़ाइल, ट्रांज़िटिव क्लोज़र से किन हेडर फ़ाइलों का इस्तेमाल करती है. इससे हमें हर फ़ाइल को रिमोट एक्ज़ीक्यूटर को नहीं भेजना पड़ता. इसलिए, हमारे पास हर हेडर फ़ाइल को "इनपुट" के तौर पर रजिस्टर न करने का विकल्प होता है. इसके बजाय, हम ट्रांज़िटिव तरीके से शामिल किए गए हेडर के लिए सोर्स फ़ाइल को स्कैन करते हैं और सिर्फ़ उन हेडर फ़ाइलों को इनपुट के तौर पर मार्क करते हैं जिनका ज़िक्र
#includeस्टेटमेंट में किया गया है. हम अनुमान को ज़्यादा रखते हैं, ताकि हमें पूरे C प्रीप्रोसेसर को लागू न करना पड़े. फ़िलहाल, Bazel में यह विकल्प "false" पर सेट है और इसका इस्तेमाल सिर्फ़ Google में किया जाता है. - कार्रवाई के दौरान, यह पता चल सकता है कि कुछ फ़ाइलों का इस्तेमाल नहीं किया गया है. C++ में इसे ".d फ़ाइलें" कहा जाता है: कंपाइलर बताता है कि कौनसी हेडर फ़ाइलें इस्तेमाल की गई थीं. साथ ही, Make की तुलना में बेहतर इंक्रीमेंटैलिटी पाने के लिए, Bazel इस फ़ैक्ट का इस्तेमाल करता है. यह, स्कैनर को शामिल करने के मुकाबले बेहतर अनुमान देता है, क्योंकि यह कंपाइलर पर निर्भर करता है.
इन्हें कार्रवाई के इन तरीकों का इस्तेमाल करके लागू किया जाता है:
Action.discoverInputs()को कॉल किया जाता है. इससे, आर्टफ़ैक्ट का नेस्ट किया गया सेट दिखना चाहिए. यह सेट, ज़रूरी आर्टफ़ैक्ट का होता है. ये सोर्स आर्टफ़ैक्ट होने चाहिए, ताकि ऐक्शन ग्राफ़ में ऐसे कोई डिपेंडेंसी एज न हों जिनका कॉन्फ़िगर किए गए टारगेट ग्राफ़ में कोई मिलता-जुलता एज न हो.Action.execute()को कॉल करके कार्रवाई की जाती है.Action.execute()के आखिर में, कार्रवाईAction.updateInputs()को कॉल करके यह बता सकती है कि उसके सभी इनपुट की ज़रूरत नहीं थी. अगर इस्तेमाल किए गए इनपुट को इस्तेमाल नहीं किया गया है के तौर पर रिपोर्ट किया जाता है, तो इससे इंक्रीमेंटल बिल्ड गलत हो सकते हैं.
जब कोई ऐक्शन कैश, नए ऐक्शन इंस्टेंस (जैसे, सर्वर रीस्टार्ट होने के बाद बनाया गया) पर हिट दिखाता है, तो Bazel updateInputs() को कॉल करता है. ऐसा इसलिए किया जाता है, ताकि इनपुट का सेट, पहले किए गए इनपुट की खोज और छंटाई के नतीजे को दिखा सके.
Starlark ऐक्शन, कुछ इनपुट को इस्तेमाल नहीं किया गया के तौर पर एलान करने की सुविधा का इस्तेमाल कर सकते हैं. इसके लिए, unused_inputs_list= के ctx.actions.run() आर्ग्युमेंट का इस्तेमाल करें.
कार्रवाइयां करने के अलग-अलग तरीके: रणनीतियां/ऐक्शन कॉन्टेक्स्ट
कुछ कार्रवाइयों को अलग-अलग तरीकों से किया जा सकता है. उदाहरण के लिए, कमांड लाइन को स्थानीय तौर पर, स्थानीय तौर पर लेकिन अलग-अलग तरह के सैंडबॉक्स में या रिमोटली (दूर से) एक्ज़ीक्यूट किया जा सकता है. इस कॉन्सेप्ट को ActionContext या Strategy कहा जाता है. ऐसा इसलिए, क्योंकि हमने नाम बदलने की प्रोसेस को सिर्फ़ आधा ही पूरा किया है...
कार्रवाई के कॉन्टेक्स्ट का लाइफ़ साइकल इस तरह होता है:
- जब एक्ज़ीक्यूशन फ़ेज़ शुरू होता है, तब
BlazeModuleइंस्टेंस से पूछा जाता है कि उनके पास कौनसे ऐक्शन कॉन्टेक्स्ट हैं. ऐसाExecutionToolके कंस्ट्रक्टर में होता है. कार्रवाई के कॉन्टेक्स्ट टाइप की पहचान, JavaClassइंस्टेंस से होती है. यहActionContextके सब-इंटरफ़ेस को रेफ़र करता है. साथ ही, यह बताता है कि कार्रवाई के कॉन्टेक्स्ट को किस इंटरफ़ेस को लागू करना चाहिए. - उपलब्ध विकल्पों में से, कार्रवाई के लिए सही कॉन्टेक्स्ट चुना जाता है. इसके बाद, इसे
ActionExecutionContextऔरBlazeExecutorको भेज दिया जाता है. - कार्रवाइयां,
ActionExecutionContext.getContext()औरBlazeExecutor.getStrategy()का इस्तेमाल करके कॉन्टेक्स्ट का अनुरोध करती हैं (ऐसा करने का सिर्फ़ एक तरीका होना चाहिए…)
रणनीतियों को अपना काम करने के लिए, दूसरी रणनीतियों को कॉल करने की अनुमति होती है. उदाहरण के लिए, इसका इस्तेमाल डाइनैमिक रणनीति में किया जाता है. यह रणनीति, कार्रवाइयों को स्थानीय और रिमोट, दोनों जगहों पर शुरू करती है. इसके बाद, यह उस रणनीति का इस्तेमाल करती है जो सबसे पहले पूरी होती है.
एक अहम रणनीति, पर्सिस्टेंट वर्कर प्रोसेस (WorkerSpawnStrategy) को लागू करने वाली रणनीति है. इसका मतलब यह है कि कुछ टूल को शुरू होने में ज़्यादा समय लगता है. इसलिए, हर कार्रवाई के लिए नया टूल शुरू करने के बजाय, कार्रवाइयों के बीच उनका फिर से इस्तेमाल किया जाना चाहिए. (इससे सही होने से जुड़ी समस्या हो सकती है, क्योंकि Bazel, वर्कर प्रोसेस के इस भरोसे पर काम करता है कि वह अलग-अलग अनुरोधों के बीच, ऑब्ज़र्वेबल स्टेट को नहीं ले जाता)
अगर टूल बदलता है, तो वर्कर प्रोसेस को फिर से शुरू करना होगा. किसी वर्कर का फिर से इस्तेमाल किया जा सकता है या नहीं, यह इस बात पर निर्भर करता है कि WorkerFilesHash का इस्तेमाल करके, इस्तेमाल किए गए टूल के लिए चेकसम की गणना की गई है या नहीं. यह इस बात पर निर्भर करता है कि कार्रवाई के कौनसे इनपुट, टूल का हिस्सा हैं और कौनसे इनपुट हैं. यह कार्रवाई के क्रिएटर तय करता है: Spawn.getToolFiles() और Spawn की रनफ़ाइलें, टूल के हिस्से के तौर पर गिनी जाती हैं.
रणनीतियों (या ऐक्शन कॉन्टेक्स्ट!) के बारे में ज़्यादा जानकारी:
- कार्रवाइयां चलाने की अलग-अलग रणनीतियों के बारे में जानकारी यहां उपलब्ध है.
- डाइनैमिक रणनीति के बारे में जानकारी यहां उपलब्ध है. इस रणनीति में, हम स्थानीय और रिमोट, दोनों तरह से कार्रवाई करते हैं, ताकि यह देखा जा सके कि कौनसी कार्रवाई पहले पूरी होती है.
- लोकल ऐक्शन को लागू करने के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
लोकल रिसोर्स मैनेजर
Bazel, कई कार्रवाइयों को एक साथ चला सकता है. एक साथ की जाने वाली लोकल कार्रवाइयों की संख्या, कार्रवाई के हिसाब से अलग-अलग होती है. किसी कार्रवाई के लिए जितने ज़्यादा संसाधनों की ज़रूरत होती है, उतने ही कम इंस्टेंस एक साथ चलने चाहिए, ताकि लोकल मशीन पर ज़्यादा लोड न पड़े.
इसे ResourceManager क्लास में लागू किया जाता है: हर कार्रवाई को, ResourceSet इंस्टेंस (सीपीयू और रैम) के तौर पर ज़रूरी स्थानीय संसाधनों के अनुमान के साथ एनोटेट किया जाना चाहिए. इसके बाद, जब ऐक्शन कॉन्टेक्स्ट को कोई ऐसा काम करना होता है जिसके लिए स्थानीय संसाधनों की ज़रूरत होती है, तो वे ResourceManager.acquireResources() को कॉल करते हैं. साथ ही, ज़रूरी संसाधन उपलब्ध होने तक उन्हें ब्लॉक कर दिया जाता है.
स्थानीय संसाधन मैनेजमेंट के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
आउटपुट डायरेक्ट्री का स्ट्रक्चर
हर कार्रवाई के लिए, आउटपुट डायरेक्ट्री में एक अलग जगह की ज़रूरत होती है. यहां कार्रवाई के आउटपुट रखे जाते हैं. आम तौर पर, डिराइव किए गए आर्टफ़ैक्ट की जगह यह होती है:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
किसी कॉन्फ़िगरेशन से जुड़ी डायरेक्ट्री का नाम कैसे तय किया जाता है? यहां दो ऐसी प्रॉपर्टी हैं जिनमें टकराव हो रहा है:
- अगर एक ही बिल्ड में दो कॉन्फ़िगरेशन हो सकते हैं, तो उनके लिए अलग-अलग डायरेक्ट्री होनी चाहिए, ताकि दोनों में एक ही कार्रवाई का अपना वर्शन हो. ऐसा न होने पर, अगर दो कॉन्फ़िगरेशन एक ही आउटपुट फ़ाइल बनाने वाली कार्रवाई की कमांड लाइन से सहमत नहीं हैं, तो Bazel को यह पता नहीं चलेगा कि कौनसी कार्रवाई चुननी है. इसे "ऐक्शन कॉन्फ़्लिक्ट" कहा जाता है
- अगर दो कॉन्फ़िगरेशन "लगभग" एक ही चीज़ को दिखाते हैं, तो उनका नाम एक जैसा होना चाहिए. इससे, एक कॉन्फ़िगरेशन में की गई कार्रवाइयों को दूसरे कॉन्फ़िगरेशन के लिए फिर से इस्तेमाल किया जा सकता है. हालांकि, ऐसा तब ही किया जा सकता है, जब कमांड लाइनें मेल खाती हों. उदाहरण के लिए, Java कंपाइलर के लिए कमांड लाइन के विकल्पों में किए गए बदलावों की वजह से, C++ कंपाइल करने की कार्रवाइयों को फिर से नहीं चलाया जाना चाहिए.
अब तक, हम इस समस्या को हल करने का कोई सिद्धांत नहीं बना पाए हैं. यह समस्या, कॉन्फ़िगरेशन ट्रिमिंग की समस्या से मिलती-जुलती है. विकल्पों के बारे में ज़्यादा जानकारी यहां दी गई है. समस्या वाली मुख्य चीज़ें, Starlark के नियम और पहलू हैं. Starlark के नियम लिखने वाले लोगों को आम तौर पर Bazel के बारे में ज़्यादा जानकारी नहीं होती. पहलू, "एक जैसी" आउटपुट फ़ाइलें जनरेट करने वाली चीज़ों के स्पेस में एक और डाइमेंशन जोड़ते हैं.
मौजूदा तरीका यह है कि कॉन्फ़िगरेशन के लिए पाथ सेगमेंट <CPU>-<compilation mode> है. इसमें अलग-अलग सफ़िक्स जोड़े गए हैं, ताकि Java में लागू किए गए कॉन्फ़िगरेशन ट्रांज़िशन से कार्रवाई के टकराव न हों. इसके अलावा, Starlark कॉन्फ़िगरेशन ट्रांज़िशन के सेट का चेकसम जोड़ा जाता है, ताकि उपयोगकर्ता कार्रवाई से जुड़े टकराव न कर पाएं. यह पूरी तरह से सही नहीं है. इसे OutputDirectories.buildMnemonic() में लागू किया जाता है. साथ ही, यह इस बात पर निर्भर करता है कि हर कॉन्फ़िगरेशन फ़्रैगमेंट, आउटपुट डायरेक्ट्री के नाम में अपना हिस्सा जोड़ता है.
जांच
Bazel में, टेस्ट चलाने की कई सुविधाएं उपलब्ध हैं. यह इन पर काम करता है:
- रिमोट एक्ज़ीक्यूशन बैकएंड उपलब्ध होने पर, टेस्ट को रिमोट तरीके से चलाना
- एक साथ कई बार टेस्ट चलाना (डीफ़्लेकिंग या टाइमिंग का डेटा इकट्ठा करने के लिए)
- शार्डिंग टेस्ट (एक ही टेस्ट के टेस्ट केस को कई प्रोसेस में बांटना, ताकि टेस्ट तेज़ी से हो सके)
- फ़्लेकी टेस्ट को फिर से चलाना
- टेस्ट को टेस्ट सुइट में ग्रुप करना
टेस्ट, कॉन्फ़िगर किए गए सामान्य टारगेट होते हैं. इनमें TestProvider होता है, जो यह बताता है कि टेस्ट को कैसे चलाया जाना चाहिए:
- ऐसे आर्टफ़ैक्ट जिनकी वजह से टेस्ट चलाया जा रहा है. यह "cache
status" फ़ाइल है. इसमें क्रम से लगाया गया
TestResultDataमैसेज शामिल होता है - टेस्ट को कितनी बार चलाना है
- टेस्ट को जितने हिस्सों में बांटना है उनकी संख्या
- टेस्ट को कैसे चलाया जाना चाहिए, इस बारे में कुछ पैरामीटर (जैसे, टेस्ट टाइमआउट)
यह तय करना कि कौनसे टेस्ट चलाने हैं
यह तय करना कि कौनसे टेस्ट चलाए जाएं, एक लंबी प्रोसेस है.
सबसे पहले, टारगेट पैटर्न पार्स करने के दौरान, टेस्ट सुइट को बार-बार बढ़ाया जाता है. इस सुविधा को TestsForTargetPatternFunction में लागू किया गया है. थोड़ा हैरान करने वाला तथ्य यह है कि अगर कोई टेस्ट सुइट किसी भी टेस्ट का एलान नहीं करता है, तो इसका मतलब है कि वह अपने पैकेज में मौजूद हर टेस्ट को रेफ़र करता है. इसे Package.beforeBuild() में लागू किया जाता है. इसके लिए, टेस्ट सुइट के नियमों में $implicit_tests नाम का एक इंप्लिसिट एट्रिब्यूट जोड़ा जाता है.
इसके बाद, कमांड लाइन के विकल्पों के हिसाब से, साइज़, टैग, टाइमआउट, और भाषा के लिए टेस्ट फ़िल्टर किए जाते हैं. इसे TestFilter में लागू किया जाता है. साथ ही, टारगेट पार्सिंग के दौरान इसे TargetPatternPhaseFunction.determineTests() से कॉल किया जाता है. इसके बाद, नतीजे को TargetPatternPhaseValue.getTestsToRunLabels() में रखा जाता है. नियम के जिन एट्रिब्यूट के लिए फ़िल्टर किया जा सकता है उन्हें कॉन्फ़िगर क्यों नहीं किया जा सकता, इसकी वजह यह है कि ऐसा विश्लेषण के चरण से पहले होता है. इसलिए, कॉन्फ़िगरेशन उपलब्ध नहीं होता है.
इसके बाद, इस डेटा को BuildView.createResult() में प्रोसेस किया जाता है: जिन टारगेट का विश्लेषण नहीं किया जा सका उन्हें फ़िल्टर कर दिया जाता है. साथ ही, टेस्ट को एक्सक्लूसिव और नॉन-एक्सक्लूसिव टेस्ट में बांट दिया जाता है. इसके बाद, इसे AnalysisResult में रखा जाता है. इससे ExecutionTool को पता चलता है कि कौनसे टेस्ट चलाने हैं.
इस लंबी प्रोसेस को ज़्यादा पारदर्शी बनाने के लिए, tests()
क्वेरी ऑपरेटर (TestsFunction में लागू किया गया) उपलब्ध है. इससे यह पता चलता है कि कमांड लाइन पर किसी टारगेट को तय करने पर, कौनसे टेस्ट
चलाए जाते हैं. यह
दुर्भाग्यवश, फिर से लागू किया गया है. इसलिए, यह शायद ऊपर दिए गए तरीके से कई तरह से अलग है.
टेस्ट चल रहे हैं
टेस्ट चलाने का तरीका यह है कि कैश मेमोरी के स्टेटस आर्टफ़ैक्ट का अनुरोध किया जाए. इसके बाद
, TestRunnerAction को लागू किया जाता है. यह TestActionContext को कॉल करता है.
को --test_strategy कमांड लाइन विकल्प से चुना जाता है. यह विकल्प
, टेस्ट को अनुरोध किए गए तरीके से चलाता है.
टेस्ट, एक तय प्रोटोकॉल के हिसाब से किए जाते हैं. इस प्रोटोकॉल में एनवायरमेंट वैरिएबल का इस्तेमाल किया जाता है. इससे टेस्ट को यह पता चलता है कि उनसे क्या उम्मीद की जा रही है. Bazel, टेस्ट से क्या उम्मीद करता है और टेस्ट, Bazel से क्या उम्मीद कर सकते हैं, इस बारे में पूरी जानकारी यहां दी गई है. आसान शब्दों में कहें, तो एक्ज़िट कोड 0 का मतलब है कि प्रोसेस पूरी हो गई है. इसके अलावा, किसी भी अन्य कोड का मतलब है कि प्रोसेस पूरी नहीं हुई.
कैश मेमोरी के स्टेटस की फ़ाइल के अलावा, हर टेस्ट प्रोसेस कई अन्य फ़ाइलें जनरेट करती है. इन्हें "टेस्ट लॉग डायरेक्ट्री" में रखा जाता है. यह टारगेट कॉन्फ़िगरेशन की आउटपुट डायरेक्ट्री की testlogs नाम की सबडायरेक्ट्री होती है:
test.xml, JUnit-स्टाइल वाली एक्सएमएल फ़ाइल. इसमें टेस्ट शार्ड में मौजूद हर टेस्ट केस के बारे में जानकारी होती हैtest.log, टेस्ट का कंसोल आउटपुट. stdout और stderr अलग-अलग नहीं होते.test.outputs, "undeclared outputs directory"; इसका इस्तेमाल उन टेस्ट के लिए किया जाता है जो टर्मिनल पर प्रिंट करने के अलावा, फ़ाइलें भी आउटपुट करना चाहते हैं.
टेस्ट एक्ज़ीक्यूशन के दौरान दो चीज़ें हो सकती हैं, जो सामान्य टारगेट बनाने के दौरान नहीं हो सकतीं: एक्सक्लूसिव टेस्ट एक्ज़ीक्यूशन और आउटपुट स्ट्रीमिंग.
कुछ टेस्ट को एक्सक्लूसिव मोड में चलाना होता है. उदाहरण के लिए, इन्हें अन्य टेस्ट के साथ पैरलल में नहीं चलाया जा सकता. इसे टेस्ट के नियम में tags=["exclusive"] जोड़कर या --test_strategy=exclusive के साथ टेस्ट चलाकर ट्रिगर किया जा सकता है . हर एक्सक्लूसिव टेस्ट को Skyframe के अलग इनवोकेशन से चलाया जाता है. यह इनवोकेशन, "मुख्य" बिल्ड के बाद टेस्ट को एक्ज़ीक्यूट करने का अनुरोध करता है. इसे SkyframeExecutor.runExclusiveTest() में लागू किया गया है.
सामान्य कार्रवाइयों के पूरा होने पर, उनका फ़ाइनल आउटपुट डंप कर दिया जाता है. हालांकि, उपयोगकर्ता टेस्ट के आउटपुट को स्ट्रीम करने का अनुरोध कर सकता है, ताकि उसे लंबे समय तक चलने वाले टेस्ट की प्रोग्रेस के बारे में जानकारी मिलती रहे. इसे --test_output=streamed कमांड लाइन विकल्प से तय किया जाता है. इसका मतलब है कि टेस्ट को खास तौर पर लागू किया जाता है, ताकि अलग-अलग टेस्ट के आउटपुट एक-दूसरे में न मिलें.
इसे StreamedTestOutput क्लास में लागू किया जाता है. यह इस तरह काम करता है: यह उस टेस्ट की test.log फ़ाइल में हुए बदलावों को पोल करता है और नए बाइट को उस टर्मिनल पर डंप करता है जहां Bazel के नियम लागू होते हैं.
जांचे गए टेस्ट के नतीजे, इवेंट बस पर उपलब्ध होते हैं. इसके लिए, अलग-अलग इवेंट (जैसे कि TestAttempt, TestResult या TestingCompleteEvent) को मॉनिटर करना होता है. इन नतीजों को Build Event Protocol में डंप किया जाता है. साथ ही, AggregatingTestListener इन्हें कंसोल पर भेजता है.
कवरेज कलेक्शन
कवरेज की जानकारी, LCOV फ़ॉर्मैट में फ़ाइलों bazel-testlogs/$PACKAGE/$TARGET/coverage.dat में मौजूद टेस्ट से मिलती है .
कवरेज इकट्ठा करने के लिए, हर टेस्ट एक्ज़ीक्यूशन को collect_coverage.sh नाम की स्क्रिप्ट में रैप किया जाता है .
यह स्क्रिप्ट, टेस्ट का एनवायरमेंट सेट अप करती है, ताकि कवरेज कलेक्शन को चालू किया जा सके. साथ ही, यह तय किया जा सके कि कवरेज रनटाइम, कवरेज फ़ाइलें कहां लिखता है. इसके बाद, यह टेस्ट को चलाता है. कोई टेस्ट खुद कई सबप्रोसेस चला सकता है. साथ ही, इसमें अलग-अलग प्रोग्रामिंग भाषाओं में लिखे गए हिस्से शामिल हो सकते हैं. हालांकि, कवरेज कलेक्शन के रनटाइम अलग-अलग होंगे. रैपर स्क्रिप्ट, ज़रूरत पड़ने पर नतीजे वाली फ़ाइलों को LCOV फ़ॉर्मैट में बदलती है. साथ ही, उन्हें एक फ़ाइल में मर्ज करती है.
collect_coverage.sh को टेस्ट की रणनीतियों के हिसाब से इंटरपोज़ किया जाता है. इसके लिए, टेस्ट के इनपुट पर collect_coverage.sh का होना ज़रूरी है. ऐसा, इंप्लिसिट एट्रिब्यूट :coverage_support की मदद से किया जाता है. इसे कॉन्फ़िगरेशन फ़्लैग --coverage_support की वैल्यू के तौर पर सेट किया जाता है (TestConfiguration.TestOptions.coverageSupport देखें)
कुछ भाषाओं में ऑफ़लाइन इंस्ट्रूमेंटेशन होता है. इसका मतलब है कि कवरेज इंस्ट्रूमेंटेशन को कंपाइल करने के समय जोड़ा जाता है. जैसे, C++. वहीं, अन्य भाषाओं में ऑनलाइन इंस्ट्रूमेंटेशन होता है. इसका मतलब है कि कवरेज इंस्ट्रूमेंटेशन को लागू करने के समय जोड़ा जाता है.
एक और मुख्य सिद्धांत बेसलाइन कवरेज है. यह किसी लाइब्रेरी, बाइनरी या टेस्ट का कवरेज है. अगर इसमें कोई कोड नहीं चलाया गया है, तो यह कवरेज है. यह समस्या हल करता है. अगर आपको किसी बाइनरी के लिए टेस्ट कवरेज का हिसाब लगाना है, तो सभी टेस्ट के कवरेज को मर्ज करना काफ़ी नहीं है. ऐसा इसलिए, क्योंकि बाइनरी में ऐसा कोड हो सकता है जो किसी भी टेस्ट से लिंक न हो. इसलिए, हम हर बाइनरी के लिए एक कवरेज फ़ाइल जनरेट करते हैं. इसमें सिर्फ़ वे फ़ाइलें शामिल होती हैं जिनके लिए हम कवरेज इकट्ठा करते हैं. इसमें कोई भी कवर्ड लाइन नहीं होती है. किसी टारगेट के लिए बेसलाइन कवरेज फ़ाइल, bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat पर मौजूद है . अगर Bazel को --nobuild_tests_only फ़्लैग पास किया जाता है, तो यह टेस्ट के साथ-साथ बाइनरी और लाइब्रेरी के लिए भी जनरेट होता है.
फ़िलहाल, बेसलाइन कवरेज की सुविधा काम नहीं कर रही है.
हम हर नियम के लिए कवरेज इकट्ठा करने के लिए, दो ग्रुप की फ़ाइलों को ट्रैक करते हैं: इंस्ट्रुमेंट की गई फ़ाइलों का सेट और इंस्ट्रुमेंटेशन मेटाडेटा फ़ाइलों का सेट.
इंस्ट्रुमेंट की गई फ़ाइलों का सेट सिर्फ़ इंस्ट्रुमेंट करने के लिए फ़ाइलों का एक सेट होता है. ऑनलाइन कवरेज रनटाइम के लिए, इसका इस्तेमाल रनटाइम में यह तय करने के लिए किया जा सकता है कि किन फ़ाइलों को इंस्ट्रुमेंट करना है. इसका इस्तेमाल, बेसलाइन कवरेज को लागू करने के लिए भी किया जाता है.
इंस्ट्रुमेंटेशन मेटाडेटा फ़ाइलों का सेट, अतिरिक्त फ़ाइलों का वह सेट होता है जिसकी ज़रूरत किसी टेस्ट को LCOV फ़ाइलें जनरेट करने के लिए होती है. Bazel को इन फ़ाइलों की ज़रूरत होती है. असल में, इसमें रनटाइम से जुड़ी फ़ाइलें शामिल होती हैं. उदाहरण के लिए, gcc कंपाइल करने के दौरान .gcno फ़ाइलें बनाता है. अगर कवरेज मोड चालू है, तो इन्हें टेस्ट ऐक्शन के इनपुट के सेट में जोड़ा जाता है.
कवरेज का डेटा इकट्ठा किया जा रहा है या नहीं, यह जानकारी BuildConfiguration में सेव की जाती है. यह तरीका इसलिए काम का है, क्योंकि इसकी मदद से टेस्ट ऐक्शन और ऐक्शन ग्राफ़ को आसानी से बदला जा सकता है. हालांकि, इसका मतलब यह भी है कि अगर यह बिट फ़्लिप हो जाती है, तो सभी टारगेट का फिर से विश्लेषण करना होगा. कुछ भाषाओं, जैसे कि C++ के लिए, कवरेज इकट्ठा करने वाला कोड जनरेट करने के लिए, कंपाइलर के अलग-अलग विकल्पों की ज़रूरत होती है. इससे इस समस्या को कुछ हद तक कम किया जा सकता है, क्योंकि इसके बाद फिर से विश्लेषण करना ज़रूरी हो जाता है.
कवरेज सपोर्ट फ़ाइलें, लेबल पर निर्भर करती हैं. ये लेबल, इंप्लिसिट डिपेंडेंसी के तौर पर काम करते हैं, ताकि इन्हें इनवोकेशन नीति के तहत बदला जा सके. इससे, Bazel के अलग-अलग वर्शन के बीच अंतर किया जा सकता है. हमारा सुझाव है कि इन अंतरों को हटा दिया जाए और हम इनमें से किसी एक को स्टैंडर्ड बना दें.
हम "कवरेज रिपोर्ट" भी जनरेट करते हैं. इसमें Bazel इनवोकेशन में हर टेस्ट के लिए इकट्ठा किए गए कवरेज को मर्ज किया जाता है. इसे CoverageReportActionFactory हैंडल करता है और इसे BuildView.createResult() से कॉल किया जाता है . यह टूल, ज़रूरी टूल को ऐक्सेस करता है. इसके लिए, यह उस पहले टेस्ट के :coverage_report_generator एट्रिब्यूट को देखता है जिसे लागू किया गया है.
क्वेरी इंजन
Bazel में एक छोटी भाषा होती है. इसका इस्तेमाल अलग-अलग ग्राफ़ के बारे में अलग-अलग चीज़ें पूछने के लिए किया जाता है. क्वेरी के ये टाइप उपलब्ध हैं:
bazel queryका इस्तेमाल, टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel cqueryका इस्तेमाल, कॉन्फ़िगर किए गए टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel aqueryका इस्तेमाल, ऐक्शन ग्राफ़ की जांच करने के लिए किया जाता है
इनमें से हर एक को AbstractBlazeQueryEnvironment की सबक्लासिंग करके लागू किया जाता है.
QueryFunction को सबक्लास करके, क्वेरी के अतिरिक्त फ़ंक्शन किए जा सकते हैं
. स्ट्रीमिंग क्वेरी के नतीजों को अनुमति देने के लिए, उन्हें किसी
डेटा स्ट्रक्चर में इकट्ठा करने के बजाय, query2.engine.Callback को QueryFunction में पास किया जाता है.
, उन नतीजों के लिए को कॉल करता है जिन्हें उसे दिखाना होता है.
क्वेरी के नतीजे को अलग-अलग तरीकों से दिखाया जा सकता है: लेबल, लेबल और नियम वाली क्लास, XML, प्रोटोबफ़ वगैरह. इन्हें OutputFormatter की सबक्लास के तौर पर लागू किया जाता है.
कुछ क्वेरी आउटपुट फ़ॉर्मैट (जैसे, प्रोटो) के लिए यह ज़रूरी है कि Bazel, पैकेज लोड करने से जुड़ी _पूरी _जानकारी दे, ताकि आउटपुट की तुलना की जा सके और यह पता लगाया जा सके कि किसी टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को क्रम से लगाया जाना चाहिए. यही वजह है कि कुछ ही एट्रिब्यूट टाइप ऐसे हैं जिनमें जटिल Starlark वैल्यू वाले एट्रिब्यूट नहीं हैं. आम तौर पर, इसके लिए लेबल का इस्तेमाल किया जाता है. साथ ही, उस लेबल वाले नियम में जटिल जानकारी जोड़ी जाती है. यह समस्या हल करने का कोई संतोषजनक तरीका नहीं है. इसलिए, इस ज़रूरी शर्त को हटा दिया जाना चाहिए.
मॉड्यूल सिस्टम
Bazel में मॉड्यूल जोड़कर, इसकी सुविधाओं को बढ़ाया जा सकता है. हर मॉड्यूल को
BlazeModule का सबक्लास होना चाहिए. नाम, Bazel के इतिहास से जुड़ा है. पहले इसे
Blaze कहा जाता था. साथ ही, इसे किसी कमांड
के एक्ज़ीक्यूशन के दौरान अलग-अलग इवेंट के बारे में जानकारी मिलती है.
इनका इस्तेमाल ज़्यादातर "नॉन-कोर" फ़ंक्शन लागू करने के लिए किया जाता है. ये फ़ंक्शन, Bazel के सिर्फ़ कुछ वर्शन (जैसे कि Google में इस्तेमाल होने वाला वर्शन) के लिए ज़रूरी होते हैं:
- रिमोट एक्ज़ीक्यूशन सिस्टम के इंटरफ़ेस
- नए निर्देश
एक्सटेंशन पॉइंट BlazeModule के ऑफ़र का सेट कुछ हद तक बेतरतीब है. इसे डिज़ाइन के अच्छे सिद्धांतों के उदाहरण के तौर पर इस्तेमाल न करें.
इवेंट बस
BlazeModules, Bazel के बाकी हिस्सों के साथ इवेंट बस (EventBus) के ज़रिए कम्यूनिकेट करते हैं: हर बिल्ड के लिए एक नया इंस्टेंस बनाया जाता है. Bazel के अलग-अलग हिस्से, इस पर इवेंट पोस्ट कर सकते हैं. साथ ही, मॉड्यूल उन इवेंट के लिए लिसनर रजिस्टर कर सकते हैं जिनमें उनकी दिलचस्पी है. उदाहरण के लिए, यहां दी गई चीज़ों को इवेंट के तौर पर दिखाया जाता है:
- बनाए जाने वाले बिल्ड टारगेट की सूची तय कर ली गई है
(
TargetParsingCompleteEvent) - टॉप-लेवल के कॉन्फ़िगरेशन तय किए गए हैं
(
BuildConfigurationEvent) - टारगेट बनाया गया, चाहे वह बन पाया हो या नहीं (
TargetCompleteEvent) - टेस्ट चलाया गया (
TestAttempt,TestSummary)
इनमें से कुछ इवेंट, Bazel के बाहर Build Event Protocol में दिखाए जाते हैं. इन्हें BuildEvent कहा जाता है. इससे न सिर्फ़ BlazeModules, बल्कि Bazel प्रोसेस के बाहर की चीज़ों को भी बिल्ड का पता चलता है. इन्हें या तो प्रोटोकॉल मैसेज वाली फ़ाइल के तौर पर ऐक्सेस किया जा सकता है या Bazel, इवेंट स्ट्रीम करने के लिए किसी सर्वर (जिसे Build Event Service कहा जाता है) से कनेक्ट हो सकता है.
इसे build.lib.buildeventservice और build.lib.buildeventstream Java पैकेज में लागू किया जाता है.
बाहरी रिपॉज़िटरी
Bazel को मूल रूप से, मोनोरिपो (एक ऐसा सोर्स ट्री जिसमें किसी प्रॉडक्ट को बनाने के लिए ज़रूरी सभी चीज़ें शामिल होती हैं) में इस्तेमाल करने के लिए डिज़ाइन किया गया था. हालांकि, Bazel को ऐसे माहौल में इस्तेमाल किया जाता है जहां यह ज़रूरी नहीं है. "External repositories" एक ऐब्स्ट्रैक्शन है. इसका इस्तेमाल इन दोनों को जोड़ने के लिए किया जाता है: ये ऐसे कोड को दिखाते हैं जो बिल्ड के लिए ज़रूरी है, लेकिन मुख्य सोर्स ट्री में नहीं है.
WORKSPACE फ़ाइल
बाहरी डेटाबेस का सेट, WORKSPACE फ़ाइल को पार्स करके तय किया जाता है. उदाहरण के लिए, इस तरह का एलान:
local_repository(name="foo", path="/foo/bar")
इससे @foo नाम की रिपॉज़िटरी में नतीजे उपलब्ध होते हैं. यह प्रोसेस तब मुश्किल हो जाती है, जब Starlark फ़ाइलों में नए रिपॉज़िटरी नियमों को तय किया जा सकता है. इसके बाद, इनका इस्तेमाल नए Starlark कोड को लोड करने के लिए किया जा सकता है. इस कोड का इस्तेमाल, नए रिपॉज़िटरी नियमों को तय करने के लिए किया जा सकता है. यह प्रोसेस इसी तरह चलती रहती है…
इस समस्या को हल करने के लिए, WORKSPACE फ़ाइल (WorkspaceFileFunction में) को पार्स करने की प्रोसेस को load() स्टेटमेंट के हिसाब से अलग-अलग हिस्सों में बांटा जाता है. चंक इंडेक्स को WorkspaceFileKey.getIndex() से दिखाया जाता है. साथ ही, इंडेक्स X तक WorkspaceFileFunction का मतलब है कि इसे Xवें load() स्टेटमेंट तक कैलकुलेट किया जाता है.
डेटा स्टोर करने की जगह की जानकारी फ़ेच की जा रही है
रिपॉज़िटरी का कोड Bazel के लिए उपलब्ध होने से पहले, उसे फ़ेच करना ज़रूरी है. इससे Bazel, $OUTPUT_BASE/external/<repository name> के नीचे एक डायरेक्ट्री बनाता है.
रिपॉज़िटरी को फ़ेच करने की प्रोसेस इन चरणों में पूरी होती है:
PackageLookupFunctionको पता चलता है कि उसे एक रिपॉज़िटरी की ज़रूरत है. इसलिए, वहSkyKeyके तौर परRepositoryNameबनाता है. इससेRepositoryLoaderFunctionशुरू हो जाता हैRepositoryLoaderFunction, अनुरोध कोRepositoryDelegatorFunctionपर फ़ॉरवर्ड करता है. इसकी वजह साफ़ तौर पर नहीं बताई गई है. कोड के मुताबिक, ऐसा Skyframe को रीस्टार्ट करने पर चीज़ों को दोबारा डाउनलोड करने से बचने के लिए किया जाता है. हालांकि, यह वजह बहुत ठोस नहीं हैRepositoryDelegatorFunction, WORKSPACE फ़ाइल के चंक को तब तक दोहराता है, जब तक उसे वह रिपॉज़िटरी नहीं मिल जाती जिसे फ़ेच करने के लिए कहा गया है- सही
RepositoryFunctionमिल जाता है, जो रिपॉज़िटरी फ़ेच करने की सुविधा लागू करता है. यह रिपॉज़िटरी का Starlark वर्शन या Java में लागू की गई रिपॉज़िटरी के लिए हार्ड-कोड किया गया मैप होता है.
किसी रिपॉज़िटरी को फ़ेच करने में बहुत ज़्यादा समय लग सकता है. इसलिए, कैश मेमोरी की कई लेयर होती हैं:
- डाउनलोड की गई फ़ाइलों के लिए एक कैश मेमोरी होती है. इसे उनके चेकसम (
RepositoryCache) के हिसाब से व्यवस्थित किया जाता है. इसके लिए, WORKSPACE फ़ाइल में चेकसम का उपलब्ध होना ज़रूरी है. हालांकि, यह किसी भी तरह से हर्मेटिकिटी के लिए अच्छा है. इसे एक ही वर्कस्टेशन पर मौजूद Bazel सर्वर के हर इंस्टेंस के साथ शेयर किया जाता है. इससे कोई फ़र्क़ नहीं पड़ता कि वे किस वर्कस्पेस या आउटपुट बेस में चल रहे हैं. $OUTPUT_BASE/externalमें मौजूद हर रिपॉज़िटरी के लिए, "मार्कर फ़ाइल" लिखी जाती है. इसमें उस नियम का चेकसम होता है जिसका इस्तेमाल करके इसे फ़ेच किया गया था. अगर Bazel सर्वर रीस्टार्ट होता है, लेकिन चेकसम नहीं बदलता है, तो इसे फिर से फ़ेच नहीं किया जाता. इसेRepositoryDelegatorFunction.DigestWriterमें लागू किया गया है .--distdirकमांड लाइन विकल्प, किसी अन्य कैश मेमोरी को असाइन करता है. इसका इस्तेमाल, डाउनलोड किए जाने वाले आर्टफ़ैक्ट को ढूंढने के लिए किया जाता है. यह एंटरप्राइज़ सेटिंग में काम आता है, जहां Bazel को इंटरनेट से रैंडम चीज़ें फ़ेच नहीं करनी चाहिए. इसेDownloadManagerने लागू किया है .
किसी रिपॉज़िटरी को डाउनलोड करने के बाद, उसमें मौजूद आर्टफ़ैक्ट को सोर्स आर्टफ़ैक्ट माना जाता है. इससे समस्या होती है, क्योंकि Bazel आम तौर पर सोर्स आर्टफ़ैक्ट के अप-टू-डेट होने की जांच करता है. इसके लिए, वह उन पर stat() को कॉल करता है. साथ ही, ये आर्टफ़ैक्ट तब भी अमान्य हो जाते हैं, जब वे जिस रिपॉज़िटरी में हैं उसकी परिभाषा बदल जाती है. इसलिए, बाहरी रिपॉज़िटरी में मौजूद किसी आर्टफ़ैक्ट के लिए FileStateValue, उसकी बाहरी रिपॉज़िटरी पर निर्भर होने चाहिए. इसे ExternalFilesHelper मैनेज करता है.
मैनेज की गई डायरेक्ट्री
कभी-कभी, बाहरी रिपॉज़िटरी को वर्कस्पेस रूट में मौजूद फ़ाइलों में बदलाव करने की ज़रूरत होती है. जैसे, पैकेज मैनेजर, डाउनलोड किए गए पैकेज को सोर्स ट्री की सबडायरेक्ट्री में रखता है. यह Bazel के इस अनुमान के उलट है कि सोर्स फ़ाइलों में सिर्फ़ उपयोगकर्ता बदलाव करता है, न कि Bazel खुद. इससे पैकेज, वर्कस्पेस रूट के तहत मौजूद हर डायरेक्ट्री को रेफ़र कर सकते हैं. इस तरह की बाहरी रिपॉज़िटरी को काम करने के लिए, Bazel दो काम करता है:
- इस विकल्प की मदद से, उपयोगकर्ता Workspace की उन सबडाइरेक्ट्री के बारे में बता सकता है जिनमें Bazel को ऐक्सेस करने की अनुमति नहीं है. इन्हें
.bazelignoreनाम की फ़ाइल में शामिल किया जाता है औरBlacklistedPackagePrefixesFunctionमें लागू किया जाता है. - हम वर्कस्पेस की सबडायरेक्ट्री से उस बाहरी रिपॉज़िटरी तक की मैपिंग को कोड में बदलते हैं जिसे
ManagedDirectoriesKnowledgeमैनेज करता है. साथ ही, हमFileStateValueको उसी तरह मैनेज करते हैं जिस तरह सामान्य बाहरी रिपॉज़िटरी के लिए मैनेज किया जाता है.
रिपॉज़िटरी मैपिंग
ऐसा हो सकता है कि कई रिपॉज़िटरी, एक ही रिपॉज़िटरी पर निर्भर रहना चाहें, लेकिन अलग-अलग वर्शन में. यह "डायमंड डिपेंडेंसी की समस्या" का उदाहरण है. उदाहरण के लिए, अगर बिल्ड में अलग-अलग रिपॉज़िटरी में मौजूद दो बाइनरी, Guava पर निर्भर रहना चाहती हैं, तो वे दोनों शायद Guava को @guava// से शुरू होने वाले लेबल के साथ रेफ़र करेंगी. साथ ही, उन्हें उम्मीद होगी कि इसका मतलब इसके अलग-अलग वर्शन हैं.
इसलिए, Bazel बाहरी रिपॉज़िटरी के लेबल को फिर से मैप करने की अनुमति देता है, ताकि स्ट्रिंग @guava//, एक बाइनरी की रिपॉज़िटरी में एक Guava रिपॉज़िटरी (जैसे कि @guava1//) और दूसरी बाइनरी की रिपॉज़िटरी में दूसरी Guava रिपॉज़िटरी (जैसे कि @guava2//) को रेफ़र कर सके.
इसके अलावा, इसका इस्तेमाल डायमंड खरीदने के लिए भी किया जा सकता है. अगर कोई रिपॉज़िटरी @guava1// पर निर्भर करती है और दूसरी @guava2// पर, तो रिपॉज़िटरी मैपिंग की मदद से, दोनों रिपॉज़िटरी को फिर से मैप किया जा सकता है, ताकि वे कैननिकल @guava// रिपॉज़िटरी का इस्तेमाल कर सकें.
मैपिंग को WORKSPACE फ़ाइल में, हर डेटाबेस की परिभाषा के repo_mapping एट्रिब्यूट के तौर पर तय किया जाता है. इसके बाद, यह Skyframe में WorkspaceFileValue के सदस्य के तौर पर दिखता है. यहां इसे इन चीज़ों से जोड़ा जाता है:
Package.Builder.repositoryMappingइसका इस्तेमाल, पैकेज में मौजूद नियमों के लेबल वाली वैल्यू के एट्रिब्यूट को बदलने के लिए किया जाता है. इसके लिए,RuleClass.populateRuleAttributeValues()Package.repositoryMappingका इस्तेमाल विश्लेषण के चरण में किया जाता है. इसका इस्तेमाल$(location)जैसी समस्याओं को हल करने के लिए किया जाता है. ये समस्याएं, लोडिंग के चरण में पार्स नहीं होती हैंBzlLoadFunctionका इस्तेमाल, load() स्टेटमेंट में लेबल को हल करने के लिए किया जाता है
JNI बिट
Bazel का सर्वर, ज़्यादातर Java में लिखा गया है. हालांकि, कुछ ऐसे हिस्से हैं जिन्हें Java खुद नहीं कर सकता या जब हमने इसे लागू किया था, तब Java खुद नहीं कर सकता था. यह ज़्यादातर फ़ाइल सिस्टम, प्रोसेस कंट्रोल, और अन्य लो-लेवल की चीज़ों के साथ इंटरैक्शन तक सीमित है.
C++ कोड, src/main/native में मौजूद होता है. साथ ही, नेटिव तरीकों वाली Java क्लास ये हैं:
NativePosixFilesऔरNativePosixFileSystemProcessUtilsWindowsFileOperationsऔरWindowsFileProcessescom.google.devtools.build.lib.platform
कंसोल आउटपुट
कंसोल आउटपुट को दिखाना एक आसान काम लगता है. हालांकि, कई प्रोसेस (कभी-कभी रिमोट से) चलाने, फ़ाइन-ग्रेन कैशिंग, टर्मिनल आउटपुट को बेहतर और रंगीन बनाने की इच्छा, और लंबे समय तक चलने वाले सर्वर की वजह से यह काम आसान नहीं रह जाता.
क्लाइंट से आरपीसी कॉल आने के तुरंत बाद, दो RpcOutputStream
इंस्टेंस बनाए जाते हैं. ये stdout और stderr के लिए होते हैं. ये दोनों, इनमें प्रिंट किए गए डेटा को क्लाइंट को भेजते हैं. इसके बाद, इन्हें OutErr में रैप किया जाता है. यह (stdout, stderr) का पेयर होता है. कंसोल पर प्रिंट की जाने वाली हर चीज़, इन स्ट्रीम से होकर जाती है. इसके बाद, इन स्ट्रीम को BlazeCommandDispatcher.execExclusively() को सौंप दिया जाता है.
आउटपुट को डिफ़ॉल्ट रूप से, ANSI एस्केप सीक्वेंस के साथ प्रिंट किया जाता है. अगर ये वैल्यू ज़रूरी नहीं हैं (--color=no), तो इन्हें AnsiStrippingOutputStream हटा देता है. इसके अलावा, System.out और System.err को इन आउटपुट स्ट्रीम पर रीडायरेक्ट किया जाता है.
ऐसा इसलिए किया जाता है, ताकि System.err.println() का इस्तेमाल करके डीबग करने की जानकारी प्रिंट की जा सके. साथ ही, यह जानकारी क्लाइंट के टर्मिनल आउटपुट में भी दिखती रहे. यह जानकारी, सर्वर के टर्मिनल आउटपुट से अलग होती है. इस बात का ध्यान रखा जाता है कि अगर कोई प्रोसेस बाइनरी आउटपुट (जैसे कि bazel query --output=proto) जनरेट करती है, तो stdout में कोई बदलाव न हो.
छोटे मैसेज (गड़बड़ियां, चेतावनियां वगैरह) EventHandler इंटरफ़ेस के ज़रिए दिखाए जाते हैं. खास तौर पर, ये EventBus पर पोस्ट किए गए कॉन्टेंट से अलग होते हैं (यह भ्रमित करने वाला है). हर Event में एक EventKind (गड़बड़ी, चेतावनी, जानकारी, और कुछ अन्य) होता है. साथ ही, इनमें Location (सोर्स कोड में वह जगह जिसकी वजह से इवेंट हुआ) भी हो सकता है.
EventHandler लागू करने के कुछ तरीके, मिले हुए इवेंट को सेव करते हैं. इस कुकी का इस्तेमाल, यूज़र इंटरफ़ेस (यूआई) पर जानकारी को फिर से दिखाने के लिए किया जाता है. ऐसा अलग-अलग तरह की कैश मेमोरी में सेव की गई प्रोसेसिंग की वजह से होता है. उदाहरण के लिए, कैश मेमोरी में सेव किए गए कॉन्फ़िगर किए गए टारगेट से मिलने वाली चेतावनियां.
कुछ EventHandlers, इवेंट पोस्ट करने की सुविधा भी देते हैं. ये इवेंट, इवेंट बस में दिखते हैं. हालांकि, सामान्य Events में ये इवेंट _नहीं _दिखते. ये ExtendedEventHandler के लागू किए गए वर्शन हैं. इनका मुख्य काम, कैश मेमोरी में सेव किए गए EventBus इवेंट को फिर से चलाना है. ये सभी EventBus इवेंट, Postable को लागू करते हैं. हालांकि, EventBus पर पोस्ट की गई हर चीज़ के लिए, इस इंटरफ़ेस को लागू करना ज़रूरी नहीं है. सिर्फ़ वे इवेंट लागू करते हैं जिन्हें ExtendedEventHandler ने कैश मेमोरी में सेव किया है. (यह अच्छा होगा और ज़्यादातर इवेंट ऐसा करते हैं. हालांकि, इसे लागू करना ज़रूरी नहीं है)
टर्मिनल आउटपुट ज़्यादातर UiEventHandler के ज़रिए दिखता है. यह Bazel के सभी फ़ॉर्मैटिंग और प्रोग्रेस रिपोर्टिंग के लिए ज़िम्मेदार होता है. इसमें दो इनपुट होते हैं:
- इवेंट बस
- रिपोर्टर के ज़रिए, इसमें पाइप की गई इवेंट स्ट्रीम
कमांड एक्ज़ीक्यूशन मशीनरी (उदाहरण के लिए, Bazel का बाकी हिस्सा) का क्लाइंट को आरपीसी स्ट्रीम से सीधे तौर पर सिर्फ़ Reporter.getOutErr() के ज़रिए कनेक्शन होता है. इससे इन स्ट्रीम को सीधे तौर पर ऐक्सेस किया जा सकता है. इसका इस्तेमाल सिर्फ़ तब किया जाता है, जब किसी कमांड को बहुत ज़्यादा बाइनरी डेटा (जैसे कि bazel query) डंप करने की ज़रूरत होती है.
Bazel की प्रोफ़ाइलिंग करना
Bazel तेज़ी से काम करता है. Bazel भी धीमा है, क्योंकि बिल्ड तब तक बढ़ते रहते हैं, जब तक कि वे बर्दाश्त करने की सीमा तक नहीं पहुंच जाते. इस वजह से, Bazel में एक प्रोफ़ाइलर शामिल होता है. इसका इस्तेमाल, बिल्ड और Bazel को प्रोफ़ाइल करने के लिए किया जा सकता है. इसे Profiler नाम की क्लास में लागू किया गया है. यह सुविधा डिफ़ॉल्ट रूप से चालू होती है. हालांकि, यह सिर्फ़ छोटा किया गया डेटा रिकॉर्ड करती है, ताकि इसका ओवरहेड कम हो. कमांड लाइन --record_full_profiler_data की मदद से, यह सुविधा हर तरह का डेटा रिकॉर्ड कर सकती है.
यह Chrome के प्रोफ़ाइलर फ़ॉर्मैट में प्रोफ़ाइल बनाता है. इसे Chrome में सबसे अच्छी तरह से देखा जा सकता है. इसका डेटा मॉडल, टास्क स्टैक का होता है: इसमें टास्क शुरू और खत्म किए जा सकते हैं. साथ ही, इन्हें एक-दूसरे के अंदर नेस्ट किया जाता है. हर Java थ्रेड को अपना टास्क स्टैक मिलता है. TODO: यह कार्रवाइयों और कॉन्टिन्यूएशन-पासिंग स्टाइल के साथ कैसे काम करता है?
प्रोफ़ाइलर को BlazeRuntime.initProfiler() में शुरू किया जाता है और BlazeRuntime.afterCommand() में बंद किया जाता है. इसे ज़्यादा से ज़्यादा समय तक चालू रखने की कोशिश की जाती है, ताकि हम हर चीज़ की प्रोफ़ाइल बना सकें. प्रोफ़ाइल में कुछ जोड़ने के लिए, Profiler.instance().profile() को कॉल करें. यह एक Closeable दिखाता है. इसके बंद होने का मतलब है कि टास्क पूरा हो गया है. इसका इस्तेमाल try-with-resources स्टेटमेंट के साथ करना सबसे सही होता है.
हम MemoryProfiler में मेमोरी की सामान्य प्रोफ़ाइलिंग भी करते हैं. यह हमेशा चालू रहता है. साथ ही, यह ज़्यादातर मैक्सिमम हीप साइज़ और GC के व्यवहार को रिकॉर्ड करता है.
Bazel की जांच करना
Bazel में मुख्य रूप से दो तरह के टेस्ट होते हैं: एक तरह के टेस्ट में Bazel को "ब्लैक बॉक्स" के तौर पर देखा जाता है. वहीं, दूसरी तरह के टेस्ट में सिर्फ़ विश्लेषण वाला चरण पूरा किया जाता है. हम पहले वाले को "इंटिग्रेशन टेस्ट" और दूसरे वाले को "यूनिट टेस्ट" कहते हैं. हालांकि, ये इंटिग्रेशन टेस्ट की तरह ज़्यादा होते हैं, लेकिन कम इंटिग्रेट किए जाते हैं. हमारे पास कुछ यूनिट टेस्ट भी हैं, जहां इनकी ज़रूरत होती है.
इंटिग्रेशन टेस्ट दो तरह के होते हैं:
- इन्हें
src/test/shellमें, बैश टेस्ट फ़्रेमवर्क का इस्तेमाल करके लागू किया जाता है - Java में लागू किए गए. इन्हें
BuildIntegrationTestCaseकी सबक्लास के तौर पर लागू किया जाता है
BuildIntegrationTestCase इंटिग्रेशन की जांच करने के लिए सबसे सही फ़्रेमवर्क है, क्योंकि यह जांच के ज़्यादातर तरीकों के लिए अच्छी तरह से काम करता है. यह एक Java फ़्रेमवर्क है. इसलिए, इसमें डीबग करने की सुविधा मिलती है. साथ ही, इसे डेवलपमेंट के कई सामान्य टूल के साथ आसानी से इंटिग्रेट किया जा सकता है. Bazel रिपॉज़िटरी में BuildIntegrationTestCase क्लास के कई उदाहरण मौजूद हैं.
विश्लेषण से जुड़े टेस्ट, BuildViewTestCase की सबक्लास के तौर पर लागू किए जाते हैं. इसमें एक स्क्रैच फ़ाइल सिस्टम होता है. इसका इस्तेमाल करके, BUILD फ़ाइलें लिखी जा सकती हैं. इसके बाद, अलग-अलग हेल्पर मेथड, कॉन्फ़िगर किए गए टारगेट का अनुरोध कर सकते हैं, कॉन्फ़िगरेशन में बदलाव कर सकते हैं, और विश्लेषण के नतीजे के बारे में अलग-अलग चीज़ों की पुष्टि कर सकते हैं.