Bazel कोडबेस

7.3 · 7.2 · 7.1 · 7.0 · 6.5

इस दस्तावेज़ में, कोडबेस के बारे में बताया गया है. साथ ही, यह भी बताया गया है कि Bazel को कैसे बनाया गया है. यह उन लोगों के लिए है जो Ba पैकेज में योगदान देना चाहते हैं, न कि असली उपयोगकर्ताओं के लिए.

परिचय

बेज़ेल का कोडबेस बड़ा है (~350KLOC प्रोडक्शन कोड और ~260 KLOC टेस्ट कोड) और कोई भी पूरे लैंडस्केप से वाकिफ़ नहीं है: सभी लोग अपनी खास घाटी को अच्छी तरह से जानते हैं, लेकिन कुछ ही लोगों को यह पता है कि हर दिशा में पहाड़ियों पर क्या मौजूद है.

इस दस्तावेज़ में कोडबेस के बारे में खास जानकारी देने की कोशिश की गई है, ताकि यात्रा की शुरुआत आसानी से की जा सके.

Bazel के सोर्स कोड का सार्वजनिक वर्शन, GitHub पर github.com/bazelbuild/bazel पर मौजूद है. यह "सही सोर्स" नहीं है. इसे Google के अंदर मौजूद सोर्स ट्री से लिया गया है. इसमें ऐसी अतिरिक्त सुविधाएं शामिल हैं जो Google के बाहर काम की नहीं हैं. लंबे समय के लक्ष्य के तौर पर, GitHub को सटीक जानकारी का सोर्स बनाना है.

योगदानों को GitHub के सामान्य पुल रिक्वेस्ट मैकेनिज्म की मदद से स्वीकार किया जाता है. साथ ही, Googler उन्हें मैन्युअल तरीके से इंटरनल सोर्स ट्री में इंपोर्ट करता है. इसके बाद, उन्हें GitHub पर फिर से एक्सपोर्ट किया जाता है.

क्लाइंट/सर्वर आर्किटेक्चर

Bazel का ज़्यादातर हिस्सा, सर्वर प्रोसेस में मौजूद होता है. यह प्रोसेस, बिल्ड के बीच RAM में रहती है. इससे Bazel, बिल्ड के बीच स्टेटस बनाए रख पाता है.

इसलिए, Bazel कमांड-लाइन में दो तरह के विकल्प होते हैं: स्टार्टअप और कमांड. इस तरह से किसी कमांड लाइन में:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

कुछ विकल्प (--host_jvm_args=), चलाए जाने वाले कमांड के नाम से पहले और कुछ उसके बाद (-c opt) होते हैं. पहले तरह के विकल्प को "स्टार्टअप विकल्प" कहा जाता है और यह पूरी सर्वर प्रोसेस पर असर डालता है. वहीं, दूसरे तरह के विकल्प, यानी "कमांड विकल्प" सिर्फ़ एक कमांड पर असर डालते हैं.

हर सर्वर इंस्टेंस में एक सोर्स ट्री ("वर्कस्पेस") होता है. साथ ही, आम तौर पर हर वर्कस्पेस में एक ऐक्टिव सर्वर इंस्टेंस होता है. कस्टम आउटपुट बेस तय करके, इस समस्या को हल किया जा सकता है. ज़्यादा जानकारी के लिए, "डायरेक्ट्री लेआउट" सेक्शन देखें.

Basel को एक ऐसी ELF एक्ज़ीक्यूटेबल फ़ाइल के तौर पर डिस्ट्रिब्यूट किया गया है जो एक मान्य .zip फ़ाइल भी है. bazel टाइप करने पर, C++ में लागू किए गए ऊपर दिए गए ELF executable ("क्लाइंट") को कंट्रोल मिल जाता है. यह इन चरणों का इस्तेमाल करके, सही सर्वर प्रोसेस सेट अप करता है:

  1. जांचता है कि क्या इसे पहले ही अपने आप एक्सट्रैक्ट किया गया है. अगर नहीं, तो फिर से अपलोड की जा सकती है. यहीं से सर्वर लागू करने की प्रोसेस शुरू होती है.
  2. यह जांच करता है कि कोई चालू सर्वर इंस्टेंस काम कर रहा है या नहीं: वह चल रहा है, उसमें स्टार्टअप के सही विकल्प हैं, और वह सही वर्कस्पेस डायरेक्ट्री का इस्तेमाल करता है. यह $OUTPUT_BASE/server डायरेक्ट्री में जाकर, चल रहे सर्वर को ढूंढता है. इस डायरेक्ट्री में, उस पोर्ट की लॉक फ़ाइल होती है जिस पर सर्वर सुन रहा होता है.
  3. ज़रूरत पड़ने पर, पुरानी सर्वर प्रोसेस को बंद कर देता है
  4. ज़रूरत पड़ने पर, नई सर्वर प्रोसेस शुरू करता है

सही सर्वर प्रोसेस तैयार होने के बाद, जिस निर्देश को चलाना है उसे gRPC इंटरफ़ेस के ज़रिए भेजा जाता है. इसके बाद, Bazel का आउटपुट टर्मिनल पर वापस भेजा जाता है. एक ही समय पर सिर्फ़ एक निर्देश चल सकता है. इसे लागू करने के लिए, C++ और Java में अलग-अलग हिस्सों के साथ, लॉक करने के बेहतर तरीके का इस्तेमाल किया जाता है. एक साथ कई कमांड चलाने के लिए, कुछ बुनियादी ढांचा मौजूद है, क्योंकि bazel version को किसी दूसरे कमांड के साथ चलाने में कुछ परेशानी होती है. मुख्य समस्या, BlazeModules के लाइफ़ साइकल और 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 अपना सारा डेटा "आउटपुट उपयोगकर्ता रूट" में डालता है. आम तौर पर, यह $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 स्टार्टअप विकल्प का इस्तेमाल करके बदला जा सकता है. यह विकल्प दूसरी चीज़ों के अलावा, इस सीमा को पार करने में मददगार होता है कि किसी भी समय किसी भी फ़ाइल फ़ोल्डर में सिर्फ़ एक Basel इंस्टेंस चल सकता है.

आउटपुट डायरेक्ट्री में दूसरी चीज़ों के साथ-साथ ये चीज़ें भी शामिल होती हैं:

  • डेटा स्टोर करने की बाहरी जगहों को $OUTPUT_BASE/external पर फ़ेच किया गया.
  • exec root, एक डायरेक्ट्री है जिसमें मौजूदा बिल्ड के सभी सोर्स कोड के लिए सिमलंक होते हैं. यह $OUTPUT_BASE/execroot पर मौजूद है. बिल्ड के दौरान, काम करने वाली डायरेक्ट्री $EXECROOT/<name of main repository> है. हम इसे $EXECROOT में बदलने जा रहे हैं. हालांकि, यह एक लंबी अवधि का प्लान है, क्योंकि यह बहुत ही असंगत बदलाव है.
  • बिल्ड के दौरान बनाई गई फ़ाइलें.

निर्देश को लागू करने की प्रोसेस

जब Bazel सर्वर को कंट्रोल मिल जाता है और उसे उस कमांड के बारे में पता चल जाता है जिसे उसे रन करना है, तो ये इवेंट इस क्रम में होते हैं:

  1. BlazeCommandDispatcher को नए अनुरोध के बारे में सूचना दी जाती है. यह तय करता है कि निर्देश को चलाने के लिए, वर्कस्पेस की ज़रूरत है या नहीं. यह ज़रूरी नहीं है कि हर निर्देश के लिए वर्कस्पेस की ज़रूरत हो. जैसे, वर्शन या मदद जैसे निर्देशों के लिए वर्कस्पेस की ज़रूरत नहीं होती. साथ ही, यह भी तय करता है कि कोई दूसरा निर्देश चल रहा है या नहीं.

  2. सही निर्देश मिल गया है. हर कमांड में इंटरफ़ेस BlazeCommand लागू होना चाहिए और उसमें @Command एनोटेशन होना चाहिए. यह एक तरह का एंटीपैटर्न है. यह अच्छा होगा, अगर किसी कमांड के लिए ज़रूरी सभी मेटाडेटा को BlazeCommand पर मौजूद तरीकों से बताया गया हो

  3. कमांड-लाइन के विकल्पों को पार्स किया जाता है. हर कमांड के लिए, कमांड लाइन के अलग-अलग विकल्प होते हैं. इनके बारे में @Command एनोटेशन में बताया गया है.

  4. एक इवेंट बस बनाई जाती है. इवेंट बस, उन इवेंट के लिए एक स्ट्रीम है जो बिल्ड के दौरान होते हैं. इनमें से कुछ को, बिल्ड इवेंट प्रोटोकॉल के तहत Bazel से बाहर एक्सपोर्ट किया जाता है, ताकि दुनिया को यह पता चल सके कि बिल्ड कैसे हुआ.

  5. निर्देश को कंट्रोल मिल जाता है. सबसे दिलचस्प निर्देश वे होते हैं जो किसी प्रोग्राम को बने हुए कोड में बदलते हैं: बने हुए कोड में बदलना, जांच करना, चलाना, कवरेज वगैरह: यह सुविधा BuildTool से लागू की जाती है.

  6. कमांड लाइन पर टारगेट पैटर्न का सेट पार्स किया जाता है और //pkg:all और //pkg/... जैसे वाइल्डकार्ड हल किए जाते हैं. इसे AnalysisPhaseRunner.evaluateTargetPatterns() में लागू किया गया और Skyframe में TargetPatternPhaseValue के तौर पर सुधार किया गया.

  7. लोडिंग/विश्लेषण का फ़ेज़, ऐक्शन ग्राफ़ बनाने के लिए चलाया जाता है. यह कमांड का एक डायरेक्टेड एकाइक्लिक ग्राफ़ होता है, जिसे बिल्ड के लिए एक्ज़ीक्यूट किया जाना चाहिए.

  8. प्रोग्राम चलाने का चरण शुरू हो जाता है. इसका मतलब है कि अनुरोध किए गए टॉप-लेवल टारगेट बनाने के लिए, ज़रूरी हर कार्रवाई को चलाया जाता है.

कमांड लाइन के विकल्प

Bazel को कॉल करने के लिए कमांड-लाइन के विकल्पों के बारे में, OptionsParsingResult ऑब्जेक्ट में बताया गया है. इसमें "option classes" से विकल्पों की वैल्यू तक का मैप होता है. "विकल्प क्लास", OptionsBase और ग्रुप कमांड लाइन के विकल्पों की सब-क्लास है. ये विकल्प एक-दूसरे से जुड़े होते हैं. उदाहरण के लिए:

  1. प्रोग्रामिंग भाषा (CppOptions या JavaOptions) से जुड़े विकल्प. ये FragmentOptions के सबक्लास होने चाहिए और आखिर में इन्हें BuildOptions ऑब्जेक्ट में रैप कर दिया जाता है.
  2. Bazel के ऐक्शन लागू करने के तरीके से जुड़े विकल्प (ExecutionOptions)

इन विकल्पों को विश्लेषण के चरण में इस्तेमाल करने के लिए डिज़ाइन किया गया है. इन्हें Java में RuleContext.getFragment() या Starlark में ctx.fragments के ज़रिए इस्तेमाल किया जा सकता है. इनमें से कुछ उदाहरण (उदाहरण के लिए, C++ में स्कैनिंग शामिल है या नहीं) को एक्ज़ीक्यूशन के दौरान पढ़ा जाता है. हालांकि, इसके लिए हमेशा साफ़ तौर पर प्लंबिंग की ज़रूरत होती है, क्योंकि BuildConfiguration उसके बाद उपलब्ध नहीं होता. ज़्यादा जानकारी के लिए, "कॉन्फ़िगरेशन" सेक्शन देखें.

चेतावनी: हम यह दिखाना चाहते हैं कि OptionsBase इंस्टेंस में बदलाव नहीं किया जा सकता और उनका इस्तेमाल उसी तरह किया जा सकता है जैसे SkyKeys का हिस्सा. हालांकि, ऐसा नहीं है. इनमें बदलाव करने से, Bazel को ऐसे तरीके से गड़बड़ किया जा सकता है जिसे डीबग करना मुश्किल होता है. माफ़ करें, उन्हें असल में अपरिवर्तनीय बनाना एक बड़ी चुनौती है. (किसी FragmentOptions को बनाने के तुरंत बाद उसमें बदलाव करना ठीक है. ऐसा तब करें, जब किसी और को उसका रेफ़रंस रखने का मौका न मिले और equals() या hashCode() को उस पर कॉल न किया गया हो.)

Bazel, विकल्प क्लास के बारे में इन तरीकों से जानता है:

  1. कुछ Bazel में पहले से मौजूद हैं (CommonCommandOptions)
  2. हर Bazel कमांड पर मौजूद @Command एनोटेशन से
  3. ConfiguredRuleClassProvider से (ये अलग-अलग प्रोग्रामिंग भाषाओं से जुड़े कमांड लाइन विकल्प हैं)
  4. Starlark के नियम अपने विकल्प भी तय कर सकते हैं (यहां देखें)

हर विकल्प (Starlark से तय किए गए विकल्पों को छोड़कर), FragmentOptions सबक्लास का एक सदस्य वैरिएबल होता है. इसमें @Option एनोटेशन होता है, जो कुछ सहायता टेक्स्ट के साथ-साथ कमांड लाइन विकल्प का नाम और टाइप बताता है.

कमांड लाइन के विकल्प की वैल्यू का Java टाइप आम तौर पर आसान होता है (जैसे, कोई स्ट्रिंग, कोई पूर्णांक, कोई बूलियन, कोई लेबल वगैरह). हालांकि, हम ज़्यादा मुश्किल टाइप के विकल्पों के साथ भी काम करते हैं. इस मामले में, कमांड लाइन स्ट्रिंग को डेटा टाइप में बदलने का काम, com.google.devtools.common.options.Converter को लागू करने पर होता है.

Bazel को दिखने वाला सोर्स ट्री

Bazel, सॉफ़्टवेयर बनाने का काम करता है. यह सोर्स कोड को पढ़कर और उसका विश्लेषण करके ऐसा करता है. Bazel जिस सोर्स कोड पर काम करता है उसे "वर्कस्पेस" कहा जाता है. इसे रिपॉज़िटरी, पैकेज, और नियमों में बांटा जाता है.

डेटा स्टोर करने की जगह

"रिपॉज़िटरी" एक सोर्स ट्री होता है, जिस पर डेवलपर काम करता है. आम तौर पर, यह एक प्रोजेक्ट को दिखाता है. Bazel का पूर्वज, Blaze, एक मोनोरेपो पर काम करता था. इसके उलट, Bazel उन प्रोजेक्ट के साथ काम करता है जिनका सोर्स कोड कई रिपॉज़िटरी में मौजूद होता है. जिस रिपॉज़िटरी से Bazel को शुरू किया जाता है उसे "मुख्य रिपॉज़िटरी" कहा जाता है. अन्य रिपॉज़िटरी को "बाहरी रिपॉज़िटरी" कहा जाता है.

रिपॉज़िटरी को उसकी रूट डायरेक्ट्री में WORKSPACE (या WORKSPACE.bazel) नाम की फ़ाइल से मार्क किया जाता है. इस फ़ाइल में, पूरे बिल्ड के लिए "ग्लोबल" जानकारी होती है. उदाहरण के लिए, उपलब्ध बाहरी रिपॉज़िटरी का सेट. यह एक सामान्य Starlark फ़ाइल की तरह काम करती है. इसका मतलब है कि किसी भी Starlark फ़ाइल को load() किया जा सकता है. इसका इस्तेमाल आम तौर पर, उन डेटा स्टोर करने की जगहों को शामिल करने के लिए किया जाता है जिनकी ज़रूरत, साफ़ तौर पर रेफ़र की गई डेटा स्टोर करने की जगह को होती है. हम इसे "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 के बारे में अनजान globber
  • SkyframeHybridGlobber, यह एक ऐसा वर्शन है जो Skyframe का इस्तेमाल करता है और "Skyframe रीस्टार्ट" (इसके बारे में नीचे बताया गया है) से बचने के लिए, लेगसी globber पर वापस आ जाता है

Package क्लास में कुछ ऐसे सदस्य होते हैं जिनका इस्तेमाल सिर्फ़ WORKSPACE फ़ाइल को पार्स करने के लिए किया जाता है. ये सदस्य, असल पैकेज के लिए काम के नहीं होते. यह डिज़ाइन की एक कमी है, क्योंकि रेगुलर पैकेज के बारे में बताने वाले ऑब्जेक्ट में ऐसे फ़ील्ड नहीं होने चाहिए जो किसी और चीज़ के बारे में बताते हों. इनमें शामिल हैं:

  • रिपॉज़िटरी मैपिंग
  • रजिस्टर किए गए टूलचेन
  • रजिस्टर किए गए एक्ज़ीक्यूशन प्लैटफ़ॉर्म

आम तौर पर, WORKSPACE फ़ाइल को पार्स करने और सामान्य पैकेज को पार्स करने के बीच ज़्यादा अंतर होता है, ताकि Packageको दोनों की ज़रूरतों को पूरा करने की ज़रूरत न पड़े. हालांकि, ऐसा करना मुश्किल है, क्योंकि ये दोनों एक-दूसरे से काफ़ी गहरे तरीके से जुड़े हुए हैं.

लेबल, टारगेट, और नियम

पैकेज, टारगेट से बने होते हैं. ये टारगेट इन टाइप के होते हैं:

  1. फ़ाइलें: ऐसी चीज़ें जो बिल्ड के इनपुट या आउटपुट होती हैं. बैजल के पार्सल में, हम उन्हें आर्टफ़ैक्ट कहते हैं. इन आर्टफ़ैक्ट के बारे में कहीं और बताया गया है. बिल्ड के दौरान बनाई गई सभी फ़ाइलें टारगेट नहीं होतीं. आम तौर पर, Bazel के आउटपुट में कोई लेबल नहीं होता.
  2. नियम: इनसे आउटपुट पाने के तरीकों के बारे में जानकारी मिलती है. आम तौर पर, वे किसी प्रोग्रामिंग भाषा (जैसे कि cc_library, java_library या py_library) से जुड़ी होती हैं, लेकिन कुछ ऐसी भाषा भी होती है जो भाषा आधारित नहीं होती (जैसे, genrule या filegroup)
  3. पैकेज ग्रुप: किसको दिखे सेक्शन में इसके बारे में बताया गया है.

टारगेट के नाम को लेबल कहा जाता है. लेबल का सिंटैक्स @repo//pac/kage:name है. इसमें repo, उस रिपॉज़िटरी का नाम है जिसमें लेबल मौजूद है, pac/kage वह डायरेक्ट्री है जिसमें BUILD फ़ाइल मौजूद है, और name पैकेज की डायरेक्ट्री के हिसाब से फ़ाइल का पाथ है (अगर लेबल किसी सोर्स फ़ाइल का रेफ़रंस देता है). कमांड लाइन पर टारगेट का रेफ़रंस देते समय, लेबल के कुछ हिस्सों को हटाया जा सकता है:

  1. अगर रिपॉज़िटरी को छोड़ दिया जाता है, तो लेबल को मुख्य रिपॉज़िटरी में माना जाता है.
  2. अगर पैकेज का हिस्सा (जैसे, name या :name) छोड़ा जाता है, तो लेबल को मौजूदा वर्किंग डायरेक्ट्री के पैकेज में माना जाता है. अपलेवल रेफ़रंस (..) वाले रिलेटिव पाथ की अनुमति नहीं है

किसी तरह के नियम (जैसे, "C++ लाइब्रेरी") को "नियम क्लास" कहा जाता है. नियम की क्लास, Starlark (rule() फ़ंक्शन) या Java (ऐसा कहा जाता है कि "नेटिव नियम", टाइप RuleClass) में लागू की जा सकती हैं. लंबे समय में, हर भाषा के हिसाब से बने नियम, Starlark में लागू किए जाएंगे. हालांकि, कुछ लेगसी नियम फ़ैमिली (जैसे, Java या C++) फ़िलहाल Java में ही हैं.

Starlark नियम क्लास को load() स्टेटमेंट का इस्तेमाल करके, BUILD फ़ाइलों की शुरुआत में इंपोर्ट करना ज़रूरी है. वहीं, Java नियम क्लास को ConfiguredRuleClassProvider के साथ रजिस्टर करने की वजह से, Bazel उन्हें "पहचानता" है.

नियम की क्लास में यह जानकारी शामिल होती है:

  1. इसके एट्रिब्यूट (जैसे, srcs, deps): उनके टाइप, डिफ़ॉल्ट वैल्यू, सीमाएं वगैरह.
  2. हर एट्रिब्यूट से जुड़े कॉन्फ़िगरेशन ट्रांज़िशन और आसपेक्ट (अगर कोई है)
  3. नियम लागू करना
  4. ट्रांज़िटिव जानकारी देने वाले नियम, "आम तौर पर" बनाते हैं

शब्दावली से जुड़ा नोट: कोडबेस में, हम अक्सर "नियम" का इस्तेमाल, नियम क्लास से बनाए गए टारगेट के लिए करते हैं. हालांकि, Starlark और उपयोगकर्ताओं के लिए बने दस्तावेज़ में, "नियम" का इस्तेमाल सिर्फ़ नियम क्लास के लिए किया जाना चाहिए. टारगेट सिर्फ़ एक "टारगेट" है. यह भी ध्यान दें कि RuleClass के नाम में "क्लास" होने के बावजूद, किसी नियम क्लास और उस टाइप के टारगेट के बीच Java इनहेरिटेंस का कोई संबंध नहीं है.

Skyframe

Bazel के तहत काम करने वाले आकलन फ़्रेमवर्क को Skyframe कहा जाता है. इसका मॉडल यह है कि बिल्ड के दौरान बनने वाली हर चीज़ को डायरेक्ट असाइक्लिक ग्राफ़ में व्यवस्थित किया जाता है. इसके किनारे, डेटा के किसी भी हिस्से से उसकी डिपेंडेंसी की ओर इशारा करते हैं. जैसे, डेटा के ऐसे अन्य हिस्से जिन्हें इसे बनाने के लिए जानना ज़रूरी होता है.

ग्राफ़ में मौजूद नोड को SkyValue कहा जाता है और उनके नाम को SkyKey कहा जाता है. दोनों में बहुत ज़्यादा बदलाव नहीं किया जा सकता. सिर्फ़ उन चीज़ों तक पहुंचा जाना चाहिए जो नहीं बदले जा सकते. यह इनवैरिएंट, ज़्यादातर मामलों में लागू होता है. अगर ऐसा नहीं होता है, तो हम पूरी कोशिश करते हैं कि इनमें बदलाव न किया जाए या फिर सिर्फ़ ऐसे बदलाव किए जाएं जो बाहर से न दिखें. जैसे, अलग-अलग विकल्पों की क्लास BuildOptions, जो BuildConfigurationValue और उसकी SkyKey की सदस्य है. इससे यह पता चलता है कि Skyframe में कॉन्फ़िगर किए गए टारगेट जैसे सभी चीज़ों में बदलाव नहीं किया जा सकता.

स्काईफ़्रेम ग्राफ़ को देखने का सबसे आसान तरीका bazel dump --skyframe=deps को चलाना है. यह ग्राफ़ को एक लाइन में एक SkyValue डंप कर देता है. ऐसा छोटे बिल्ड के लिए करना सबसे अच्छा होता है, क्योंकि यह बहुत बड़ा हो सकता है.

स्काईफ़्रेम, com.google.devtools.build.skyframe पैकेज में मौजूद है. इसी नाम वाले पैकेज com.google.devtools.build.lib.skyframe में, Skyframe के ऊपर Bazel को लागू किया गया है. Skyframe के बारे में ज़्यादा जानकारी यहां दी गई है.

किसी दिए गए SkyKey को SkyValue में बदलने के लिए, Skyframe, कुंजी के टाइप के हिसाब से SkyFunction को लागू करेगा. फ़ंक्शन के आकलन के दौरान, यह SkyFunction.Environment.getValue() के अलग-अलग ओवरलोड को कॉल करके, Skyframe से अन्य डिपेंडेंसी का अनुरोध कर सकता है. इससे, उन डिपेंडेंसी को Skyframe के इंटरनल ग्राफ़ में रजिस्टर करने का साइड इफ़ेक्ट होता है, ताकि Skyframe को पता चल सके कि फ़ंक्शन की किसी भी डिपेंडेंसी में बदलाव होने पर, फ़ंक्शन का फिर से आकलन कैसे किया जाए. दूसरे शब्दों में, Skyframe की कैश मेमोरी और इंक्रीमेंटल कैलकुलेशन की सुविधा, SkyFunction और SkyValue के हिसाब से काम करती है.

जब भी SkyFunction किसी ऐसी डिपेंडेंसी का अनुरोध करता है जो उपलब्ध नहीं है, तो getValue() शून्य वैल्यू दिखाएगा. इसके बाद फ़ंक्शन को शून्य लौटाकर वापस Skyframe पर वापस जाना चाहिए. बाद में, Skyframe, उपलब्ध न होने वाली डिपेंडेंसी का आकलन करेगा. इसके बाद, फ़ंक्शन को शुरू से फिर से शुरू करेगा. सिर्फ़ इस बार getValue() कॉल, नॉन-नल नतीजे के साथ पूरा होगा.

ऐसे में, रीस्टार्ट करने से पहले SkyFunction में कंप्यूटेशन को दोहराने की ज़रूरत होती है. हालांकि, इसमें कैश मेमोरी में सेव की गई डिपेंडेंसी SkyValues का आकलन करने के लिए किया गया काम शामिल नहीं है. इसलिए, हम आम तौर पर इस समस्या को हल करने के लिए, ये काम करते हैं:

  1. getValuesAndExceptions() का इस्तेमाल करके, डिपेंडेंसी को एक साथ कई बार डिक्लेयर करना, ताकि फिर से शुरू करने की संख्या को सीमित किया जा सके.
  2. SkyValue को अलग-अलग SkyFunction से अलग-अलग हिस्सों में बांटना, ताकि उनकी गिनती अलग-अलग की जा सके और उन्हें कैश मेमोरी में सेव किया जा सके. ऐसा सोच-समझकर करना चाहिए, क्योंकि इससे मेमोरी का इस्तेमाल बढ़ सकता है.
  3. SkyFunction.Environment.getState() का इस्तेमाल करके या "Skyframe के पीछे" ad hoc स्टैटिक कैश रखकर, रीस्टार्ट के बीच स्टेटस सेव करना.

आम तौर पर, हमें इस तरह के तरीके अपनाने की ज़रूरत होती है, क्योंकि हमारे पास आम तौर पर, सैकड़ों हज़ार इन-फ़्लाइट Skyframe नोड होते हैं. साथ ही, Java में लाइटवेट थ्रेड काम नहीं करते.

Starlark

Starlark, डोमेन के हिसाब से बनाई गई भाषा है. इसका इस्तेमाल, लोग Bazel को कॉन्फ़िगर करने और उसे बेहतर बनाने के लिए करते हैं. इसे Python के सीमित सबसेट के तौर पर माना जाता है, जिसमें बहुत कम टाइप होते हैं. साथ ही, कंट्रोल फ़्लो पर ज़्यादा पाबंदियां होती हैं. सबसे अहम बात यह है कि एक साथ कई फ़ाइलें पढ़ने की सुविधा चालू करने के लिए, डेटा में बदलाव न होने की गारंटी दी जाती है. यह ट्यूरिंग-कंप्लीट नहीं है. इस वजह से, कुछ (सभी नहीं) उपयोगकर्ता इस भाषा में सामान्य प्रोग्रामिंग टास्क पूरा करने से बचते हैं.

Starlark को net.starlark.java पैकेज में लागू किया गया है. इसके अलावा, यहां Go में भी इसे लागू किया जा सकता है. फ़िलहाल, Bazel में इस्तेमाल किया जा रहा Java, एक इंटरप्रेटर है.

Starlark का इस्तेमाल कई कामों के लिए किया जाता है. जैसे:

  1. BUILD की भाषा. यहां नए नियम तय किए जाते हैं. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास, सिर्फ़ BUILD फ़ाइल और उससे लोड की गई .bzl फ़ाइलों के कॉन्टेंट का ऐक्सेस होता है.
  2. नियम की परिभाषाएं. नए नियम (जैसे कि किसी नई भाषा के लिए सहायता) को इस तरह परिभाषित किया जाता है. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास डायरेक्ट डिपेंडेंसी से मिले कॉन्फ़िगरेशन और डेटा का ऐक्सेस होता है (इस बारे में ज़्यादा जानकारी बाद में दी गई है).
  3. Workspace फ़ाइल. यहां बाहरी रिपॉज़िटरी (मुख्य सोर्स ट्री में मौजूद नहीं होने वाला कोड) तय किए जाते हैं.
  4. रिपॉज़िटरी के नियम की परिभाषाएं. यहां बाहरी डेटा स्टोर करने की जगहों के नए टाइप तय किए जाते हैं. इस कॉन्टेक्स्ट में चल रहा Starlark कोड उस मशीन पर आर्बिट्रेरी कोड चला सकता है जिस पर Basel चल रहा है और फ़ाइल फ़ोल्डर के बाहर पहुंच सकता है.

BUILD और .bzl फ़ाइलों के लिए उपलब्ध बोलियाँ थोड़ी अलग होती हैं, क्योंकि इनमें अलग-अलग चीज़ें बताई जाती हैं. इनके बीच के अंतर की सूची यहां दी गई है.

Starlark के बारे में ज़्यादा जानकारी यहां उपलब्ध है.

लोड होने/विश्लेषण का चरण

लोड करने/विश्लेषण करने के चरण में, Bazel यह तय करता है कि किसी खास नियम को बनाने के लिए कौनसी कार्रवाइयां ज़रूरी हैं. इसकी बुनियादी यूनिट, "कॉन्फ़िगर किया गया टारगेट" है, जो (टारगेट, कॉन्फ़िगरेशन) पेयर है.

इसे "लोड करना/विश्लेषण का फ़ेज़" कहा जाता है, क्योंकि इसे दो अलग-अलग हिस्सों में बांटा जा सकता है. इन्हें पहले क्रम से लगाया जाता था, लेकिन अब ये समय के साथ ओवरलैप हो सकते हैं:

  1. पैकेज लोड करना, यानी BUILD फ़ाइलों को उन Package ऑब्जेक्ट में बदलना जो उन्हें दिखाते हैं
  2. कॉन्फ़िगर किए गए टारगेट का विश्लेषण करना, यानी कि ऐक्शन ग्राफ़ बनाने के लिए नियमों को लागू करना

कमांड लाइन पर अनुरोध किए गए कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र में मौजूद हर कॉन्फ़िगर किए गए टारगेट का विश्लेषण, नीचे से ऊपर की ओर किया जाना चाहिए. इसका मतलब है कि सबसे पहले लीफ़ नोड और फिर कमांड लाइन पर मौजूद टारगेट का विश्लेषण किया जाना चाहिए. कॉन्फ़िगर किए गए किसी एक टारगेट के विश्लेषण के इनपुट ये हैं:

  1. कॉन्फ़िगरेशन. ("कैसे" उस नियम को बनाएं; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म, लेकिन कमांड लाइन के ऐसे विकल्प भी जिनका उपयोगकर्ता C++ कंपाइलर को पास करना चाहता है)
  2. डायरेक्ट डिपेंडेंसी. ट्रांज़िशन की जानकारी देने वाली उनकी कंपनियां, विश्लेषण किए जा रहे नियम के लिए उपलब्ध हैं. इन्हें इस तरह इसलिए कहा जाता है, क्योंकि ये कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र में जानकारी का "रोल-अप" उपलब्ध कराते हैं. जैसे, क्लासपाथ पर मौजूद सभी .jar फ़ाइलें या C++ बाइनरी में लिंक की जाने वाली सभी .o फ़ाइलें)
  3. टारगेट. यह उस पैकेज को लोड करने का नतीजा है जिसमें टारगेट मौजूद है. नियमों के लिए, इसमें इसकी विशेषताएं शामिल हैं, जो आम तौर पर मायने रखती हैं.
  4. कॉन्फ़िगर किए गए टारगेट को लागू करना. नियमों के लिए, यह Starlark या Java में हो सकता है. गैर-नियम कॉन्फ़िगर किए गए सभी टारगेट, Java में लागू किए गए हैं.

कॉन्फ़िगर किए गए टारगेट का विश्लेषण करने पर, यह नतीजा मिलता है:

  1. ट्रांज़िशन की जानकारी देने वाली सेवा देने वाली कंपनियां, कॉन्फ़िगर किए गए टारगेट को ऐक्सेस कर सकती हैं
  2. यह ऐसे आर्टफ़ैक्ट बना सकता है और इन आर्टफ़ैक्ट को बनाने के लिए ये कार्रवाइयां कर सकता है.

Java नियमों के लिए एपीआई RuleContext है, जो Starlark नियमों के ctx आर्ग्युमेंट के बराबर है. इसका एपीआई ज़्यादा बेहतर है, लेकिन साथ ही, इसमें 'बुरे काम™' करना आसान है. उदाहरण के लिए, ऐसा कोड लिखना जिसका समय या स्टोरेज की जटिलता क्वाड्रैटिक (या इससे भी खराब) हो, Bazel सर्वर को Java अपवाद की वजह से क्रैश करना या इनवैरिएंट का उल्लंघन करना (जैसे, Options इंस्टेंस में गलती से बदलाव करना या कॉन्फ़िगर किए गए टारगेट को बदलने योग्य बनाना)

कॉन्फ़िगर किए गए टारगेट की डायरेक्ट डिपेंडेंसी तय करने वाला एल्गोरिदम, DependencyResolver.dependentNodeMap() में मौजूद होता है.

कॉन्फ़िगरेशन

कॉन्फ़िगरेशन, टारगेट बनाने का तरीका है: किस प्लैटफ़ॉर्म के लिए, कमांड लाइन के कौनसे विकल्पों के साथ वगैरह.

एक ही बिल्ड में, एक ही टारगेट को कई कॉन्फ़िगरेशन के लिए बनाया जा सकता है. यह उदाहरण के लिए तब मददगार होता है, जब बिल्ड के दौरान और टारगेट कोड के लिए चलाए जाने वाले टूल के लिए एक ही कोड का इस्तेमाल किया जाता है. साथ ही, जब हम क्रॉस-कंपाइलिंग कर रहे होते हैं या कोई फ़ैट वाला Android ऐप्लिकेशन बनाते समय (ऐसा ऐप्लिकेशन जिसमें कई सीपीयू आर्किटेक्चर के लिए नेटिव कोड शामिल हो)

कॉन्फ़िगरेशन, BuildOptions इंस्टेंस होता है. हालांकि, आम तौर पर BuildOptions को BuildConfiguration में रैप किया जाता है, जो कई अन्य फ़ंक्शन उपलब्ध कराता है. यह डिपेंडेंसी ग्राफ़ के सबसे ऊपर से सबसे नीचे तक फैलता है. अगर यह बदलता है, तो बिल्ड का फिर से विश्लेषण करना होगा.

इस वजह से, गड़बड़ियां होती हैं. उदाहरण के लिए, अगर अनुरोध किए गए टेस्ट रन की संख्या में बदलाव होता है, तो पूरे बिल्ड का फिर से विश्लेषण करना पड़ता है. भले ही, इसका असर सिर्फ़ टेस्ट टारगेट पर पड़ता हो. हम कॉन्फ़िगरेशन को "छोटा" करने की योजना बना रहे हैं, ताकि ऐसा न हो. हालांकि, यह सुविधा अभी तैयार नहीं है.

जब किसी नियम को लागू करने के लिए कॉन्फ़िगरेशन का कोई हिस्सा ज़रूरी होता है, तो उसे RuleClass.Builder.requiresConfigurationFragments() का इस्तेमाल करके इसकी परिभाषा में एलान करना पड़ता है. इनके ज़रिए, गलतियों (जैसे कि Java फ़्रैगमेंट का इस्तेमाल करने वाले Python के नियम) से बचने और कॉन्फ़िगरेशन में काट-छांट करने की सुविधा मिलती है. इससे Python के विकल्पों में बदलाव होने पर, C++ टारगेट का फिर से विश्लेषण करने की ज़रूरत नहीं पड़ती.

यह ज़रूरी नहीं है कि किसी नियम का कॉन्फ़िगरेशन, उसके "पैरंट" नियम के कॉन्फ़िगरेशन से मेल खाए. डिपेंडेंसी एज में कॉन्फ़िगरेशन बदलने की प्रोसेस को "कॉन्फ़िगरेशन ट्रांज़िशन" कहा जाता है. ऐसा दो जगहों पर हो सकता है:

  1. डिपेंडेंसी एज पर. ये ट्रांज़िशन Attribute.Builder.cfg() में बताए गए हैं. ये Rule (जहां ट्रांज़िशन होता है) और BuildOptions (ओरिजनल कॉन्फ़िगरेशन) से एक या एक से ज़्यादा BuildOptions (आउटपुट कॉन्फ़िगरेशन) तक के फ़ंक्शन होते हैं.
  2. कॉन्फ़िगर किए गए टारगेट के किसी भी इनकमिंग एज पर. इनके बारे में RuleClass.Builder.cfg() में बताया गया है.

सही क्लास TransitionFactory और ConfigurationTransition हैं.

कॉन्फ़िगरेशन ट्रांज़िशन का इस्तेमाल इनके लिए किया जाता है:

  1. यह बताने के लिए कि किसी खास डिपेंडेंसी का इस्तेमाल बिल्ड के दौरान किया जाता है और इसलिए, इसे एक्सीक्यूशन आर्किटेक्चर में बनाया जाना चाहिए
  2. यह बताने के लिए कि किसी खास डिपेंडेंसी को कई आर्किटेक्चर के लिए बनाया जाना चाहिए. जैसे, फ़ैट Android APKs में नेटिव कोड के लिए

अगर किसी कॉन्फ़िगरेशन ट्रांज़िशन की वजह से एक से ज़्यादा कॉन्फ़िगरेशन आते हैं, तो इसे स्प्लिट ट्रांज़िशन कहा जाता है.

कॉन्फ़िगरेशन ट्रांज़िशन को Starlark में भी लागू किया जा सकता है (दस्तावेज़ यहां)

ट्रांसिटिव जानकारी देने वाली कंपनियां

ट्रांज़िटिव जानकारी देने वाले टूल, कॉन्फ़िगर किए गए टारगेट के लिए एक तरीका है. यह टारगेट, कॉन्फ़िगर किए गए उन अन्य टारगेट के बारे में जानकारी देता है जो उस पर निर्भर करते हैं. इनके नाम में "ट्रांज़िशन" इसलिए है, क्योंकि आम तौर पर यह कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र का एक तरह का रोल-अप होता है.

आम तौर पर, Java के ट्रांज़िशन की जानकारी देने वाले एपीआई और Starlark के ट्रांज़िशन की जानकारी देने वाले एपीआई के बीच 1:1 का अनुपात होता है. हालांकि, DefaultInfo को छोड़कर, ऐसा सभी एपीआई के लिए नहीं होता. DefaultInfo, FileProvider, FilesToRunProvider, और RunfilesProvider का एक मिला-जुला एपीआई है. ऐसा इसलिए है, क्योंकि इस एपीआई को Java के एपीआई से सीधे ट्रांसलिटरेट करने के बजाय, Starlark के तौर पर ज़्यादा इस्तेमाल किया जाता है. उनकी कुंजी, इनमें से एक चीज़ है:

  1. Java क्लास ऑब्जेक्ट. यह सुविधा सिर्फ़ उन सेवा देने वाली कंपनियों के लिए उपलब्ध है जिन्हें Starlark से ऐक्सेस नहीं किया जा सकता. ये सेवा देने वाली कंपनियां, TransitiveInfoProvider की सबक्लास होती हैं.
  2. कोई स्ट्रिंग. यह लेगसी तरीका है और इसका सुझाव नहीं दिया जाता. इसकी वजह यह है कि नामों में टकराव हो सकता है. संवेदनशील जानकारी देने वाली ऐसी कंपनियां, सीधे तौर पर build.lib.packages.Info की सब-क्लास होती हैं .
  3. सेवा देने वाली कंपनी का सिंबल. इसे Starlark से provider() फ़ंक्शन का इस्तेमाल करके बनाया जा सकता है. साथ ही, यह सेवा देने वाली नई कंपनियां बनाने का सुझाया गया तरीका है. इस सिंबल को Java में Provider.Key इंस्टेंस से दिखाया जाता है.

Java में लागू किए गए नए प्रोवाइडर, BuiltinProvider का इस्तेमाल करके लागू किए जाने चाहिए. NativeProvider अब काम नहीं करता (हमारे पास अब तक इसे हटाने का समय नहीं है) और TransitiveInfoProvider सब-क्लास को Starlark से ऐक्सेस नहीं किया जा सकता.

कॉन्फ़िगर किए गए टारगेट

कॉन्फ़िगर किए गए टारगेट, RuleConfiguredTargetFactory के तौर पर लागू किए जाते हैं. Java में लागू किए गए हर नियम क्लास के लिए एक सबक्लास होता है. Starlark के ज़रिए कॉन्फ़िगर किए गए टारगेट, StarlarkRuleConfiguredTargetUtil.buildRule() के ज़रिए बनाए जाते हैं.

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

  1. उनका filesToBuild, "इस नियम के तहत आने वाली फ़ाइलों के सेट" का धुंधला कॉन्सेप्ट. ये ऐसी फ़ाइलें होती हैं जो तब बनती हैं, जब कॉन्फ़िगर किया गया टारगेट कमांड लाइन पर या genrule के srcs में होता है.
  2. उनकी रनफ़ाइल, सामान्य और डेटा.
  3. उनके आउटपुट ग्रुप. ये "फ़ाइलों के अन्य सेट" हैं, जिन्हें नियम से बनाया जा सकता है. इन्हें BUILD में filegroup नियम के output_group एट्रिब्यूट का इस्तेमाल करके और Java में OutputGroupInfo प्रोवाइडर का इस्तेमाल करके ऐक्सेस किया जा सकता है.

रनफ़ाइलें

कुछ बाइनरी को चलाने के लिए डेटा फ़ाइलों की ज़रूरत होती है. इसका एक उदाहरण, ऐसे टेस्ट हैं जिनमें इनपुट फ़ाइलों की ज़रूरत होती है. इसे बेज़ेल में "रनफ़ाइल" के सिद्धांत से दिखाया गया है. "रनफ़ाइल ट्री", किसी खास बाइनरी के लिए डेटा फ़ाइलों की डायरेक्ट्री ट्री होती है. इसे फ़ाइल सिस्टम में एक सिमलिंक ट्री के तौर पर बनाया जाता है, जिसमें अलग-अलग सिमलिंक होते हैं. ये सिमलिंक, आउटपुट ट्री के सोर्स में मौजूद फ़ाइलों पर ले जाते हैं.

रनफ़ाइलों के सेट को Runfiles इंस्टेंस के तौर पर दिखाया जाता है. यह कॉन्सेप्ट के हिसाब से, रनफ़ाइल्स ट्री में मौजूद किसी फ़ाइल के पाथ से उस Artifact इंस्टेंस तक का मैप होता है जो उसे दिखाता है. यह एक Map से थोड़ा ज़्यादा मुश्किल है. ऐसा दो वजहों से है:

  • ज़्यादातर मामलों में, किसी फ़ाइल का रनफ़ाइल पाथ उसके एक्ज़ीकपाथ के जैसा ही होता है. हम इसका इस्तेमाल, कुछ रैम बचाने के लिए करते हैं.
  • रनफ़ाइल ट्री में, लेगसी टाइप की कई एंट्री होती हैं. इन्हें भी दिखाना ज़रूरी है.

रनफ़ाइलों को RunfilesProvider का इस्तेमाल करके इकट्ठा किया जाता है: इस क्लास का एक इंस्टेंस, कॉन्फ़िगर किए गए टारगेट (जैसे, लाइब्रेरी) और उसके ट्रांज़िशन क्लोज़र की ज़रूरतों की रनफ़ाइलों को दिखाता है. साथ ही, इन्हें नेस्ट किए गए सेट की तरह इकट्ठा किया जाता है. असल में, इन्हें नेस्ट किए गए सेट का इस्तेमाल करके लागू किया जाता है: हर टारगेट, अपनी डिपेंडेंसी की रनफ़ाइलों को जोड़ता है और कुछ अपनी रनफ़ाइलें जोड़ता है. इसके बाद, वह नतीजे वाले सेट को डिपेंडेंसी ग्राफ़ में ऊपर की ओर भेजता है. किसी RunfilesProvider इंस्टेंस में दो Runfiles इंस्टेंस होते हैं. पहला, "डेटा" एट्रिब्यूट के ज़रिए नियम पर निर्भर होने पर और दूसरा, आने वाली हर तरह की अन्य डिपेंडेंसी के लिए. ऐसा इसलिए होता है, क्योंकि डेटा एट्रिब्यूट के ज़रिए किसी टारगेट पर निर्भर होने पर, कभी-कभी अलग-अलग रनफ़ाइलें दिखती हैं. यह एक गड़बड़ी है, जिसे हम अब तक ठीक नहीं कर पाए हैं.

बाइनरी के रनफ़ाइल को RunfilesSupport के इंस्टेंस के तौर पर दिखाया जाता है. यह Runfiles से अलग है, क्योंकि RunfilesSupport के पास असल में बनाए जाने की क्षमता है (Runfiles के उलट, जो सिर्फ़ एक मैपिंग है). इसके लिए, इन अतिरिक्त कॉम्पोनेंट की ज़रूरत होती है:

  • इनपुट रनफ़ाइल मेनिफ़ेस्ट. यह, रनफ़ाइल ट्री का सिलसिलेवार ब्यौरा है. इसका इस्तेमाल, रनफ़ाइल्स ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. साथ ही, Bazel यह मानता है कि रनफ़ाइल्स ट्री में सिर्फ़ तब बदलाव होता है, जब मेनिफ़ेस्ट के कॉन्टेंट में बदलाव होता है.
  • आउटपुट रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल ऐसी रनटाइम लाइब्रेरी में किया जाता है जो रनफ़ाइल ट्री को मैनेज करती है. खास तौर पर, Windows पर ऐसे लिंक काम करते हैं जो सिम्बॉलिक लिंक के साथ काम नहीं करते.
  • Runfiles मिडलमैन. रनफ़ाइल ट्री मौजूद होने के लिए, सिमलिंक ट्री और सिमलिंक जिस आर्टफ़ैक्ट पर ले जाते हैं उसे बनाना ज़रूरी है. डिपेंडेंसी किनारों की संख्या को कम करने के लिए, रनफ़ाइल मिडलमैन का इस्तेमाल इन सभी को दिखाने के लिए किया जा सकता है.
  • उस बाइनरी को चलाने के लिए कमांड लाइन आर्ग्युमेंट जिसकी रनफ़ाइलों को RunfilesSupport ऑब्जेक्ट दिखाता है.

आसपेक्ट

ऐसेट, "डिपेंडेंसी ग्राफ़ में कैलकुलेशन को नीचे तक भेजने" का एक तरीका है. बेज़ल के उपयोगकर्ताओं के लिए यहां बताया गया है. प्रोटोकॉल बफ़र एक अच्छा उदाहरण है: proto_library नियम को किसी खास भाषा के बारे में नहीं पता होना चाहिए. हालांकि, किसी भी प्रोग्रामिंग भाषा में प्रोटोकॉल बफ़र मैसेज (प्रोटोकॉल बफ़र की "बुनियादी इकाई") को लागू करने के लिए, proto_library नियम को जोड़ा जाना चाहिए, ताकि अगर एक ही भाषा में दो टारगेट एक ही प्रोटोकॉल बफ़र पर निर्भर हों, तो वह सिर्फ़ एक बार बनाया जाए.

कॉन्फ़िगर किए गए टारगेट की तरह ही, इन्हें Skyframe में SkyValue के तौर पर दिखाया जाता है. साथ ही, इन्हें बनाने का तरीका भी कॉन्फ़िगर किए गए टारगेट बनाने के तरीके से काफ़ी मिलता-जुलता है: इनमें ConfiguredAspectFactory नाम की फ़ैक्ट्री क्लास होती है, जिसके पास RuleContext का ऐक्सेस होता है. हालांकि, कॉन्फ़िगर किए गए टारगेट फ़ैक्ट्री के उलट, यह उस कॉन्फ़िगर किए गए टारगेट और उसके प्रोवाइडर के बारे में भी जानती है जिससे यह जुड़ी होती है.

डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजे गए आसपेक्ट का सेट, Attribute.Builder.aspects() फ़ंक्शन का इस्तेमाल करके हर एट्रिब्यूट के लिए तय किया जाता है. इस प्रोसेस में हिस्सा लेने वाली कुछ क्लास के नाम भ्रमित करने वाले हैं:

  1. AspectClass, इस एस्पेक्ट को लागू करने का तरीका है. यह Java (इस मामले में यह एक सबक्लास है) या Starlark (इस मामले में यह StarlarkAspectClass का एक इंस्टेंस है) में हो सकता है. यह RuleConfiguredTargetFactory के जैसा ही है.
  2. AspectDefinition, एस्पेक्ट की परिभाषा है. इसमें, ज़रूरी सेवा देने वाली कंपनियां और सेवा देने वाली कंपनियां शामिल होती हैं. साथ ही, इसमें लागू करने का रेफ़रंस भी होता है, जैसे कि सही AspectClass इंस्टेंस. यह RuleClass के जैसे ही है.
  3. AspectParameters, किसी ऐसे पहलू को पैरामेटाइज़ करने का एक तरीका है जिसे डिपेंडेंसी ग्राफ़ में नीचे दिखाया जाता है. फ़िलहाल, यह मैप को स्ट्रिंग करने के लिए स्ट्रिंग है. इसके काम के होने का एक अच्छा उदाहरण प्रोटोकॉल बफ़र है: अगर किसी भाषा में एक से ज़्यादा एपीआई हैं, तो डिपेंडेंसी ग्राफ़ में यह जानकारी दी जानी चाहिए कि प्रोटोकॉल बफ़र किस एपीआई के लिए बनाया जाना चाहिए.
  4. Aspect उस डेटा को दिखाता है जो डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजे जाने वाले किसी पहलू का हिसाब लगाने के लिए ज़रूरी है. इसमें आसपेक्ट क्लास, उसकी परिभाषा, और उसके पैरामीटर शामिल होते हैं.
  5. RuleAspect फ़ंक्शन तय करता है कि किसी नियम में कौनसे पहलू लागू होने चाहिए. यह Rule -> Aspect फ़ंक्शन है.

एक समस्या यह है कि एस्पेक्ट, दूसरे एस्पेक्ट से जुड़े हो सकते हैं. उदाहरण के लिए, किसी Java IDE के क्लासपाथ को इकट्ठा करने वाले एस्पेक्ट को क्लासपाथ पर मौजूद सभी .jar फ़ाइलों के बारे में जानना होगा. हालांकि, उनमें से कुछ प्रोटोकॉल बफ़र हैं. ऐसे में, IDE का ऐस्पेक्ट, (proto_library नियम + Java प्रोटो ऐस्पेक्ट) पेयर से जुड़ना चाहेगा.

अलग-अलग पहलुओं की जटिलता को क्लास AspectCollection में कैप्चर किया जाता है.

प्लैटफ़ॉर्म और टूलचेन

Baज़र, मल्टी-प्लैटफ़ॉर्म बिल्ड के साथ काम करता है. इसका मतलब है कि ऐसी बिल्ड प्रोसेस में एक से ज़्यादा आर्किटेक्चर मौजूद हो सकते हैं जहां बिल्ड ऐक्शन चल सकते हैं. साथ ही, जिस कोड के लिए बनाया गया है उसके लिए अलग-अलग आर्किटेक्चर इस्तेमाल किए जा सकते हैं. Bazel में इन आर्किटेक्चर को प्लैटफ़ॉर्म कहा जाता है. इनके बारे में पूरी जानकारी यहां दी गई है

किसी प्लैटफ़ॉर्म के बारे में, सीमा सेटिंग (जैसे, "सीपीयू आर्किटेक्चर" का कॉन्सेप्ट) से सीमा की वैल्यू (जैसे, x86_64 जैसा कोई सीपीयू) तक की की-वैल्यू मैपिंग से बताया जाता है. हमारे पास @platforms रिपॉज़िटरी में सबसे ज़्यादा इस्तेमाल की जाने वाली कंस्ट्रेंट सेटिंग और वैल्यू का एक "शब्दकोश" है.

टूलचेन का कॉन्सेप्ट इस बात पर आधारित है कि कौनसे प्लैटफ़ॉर्म पर बिल्ड चल रहा है और कौनसे प्लैटफ़ॉर्म टारगेट किए जा रहे हैं. इसके आधार पर, आपको अलग-अलग कंपाइलर का इस्तेमाल करना पड़ सकता है. उदाहरण के लिए, कोई खास C++ टूलचेन किसी खास ओएस पर चल सकता है और कुछ अन्य ओएस को टारगेट कर सकता है. Bazel को यह तय करना होगा कि सेट किए गए एक्सीक्यूशन और टारगेट प्लैटफ़ॉर्म के आधार पर, C++ के किस कंपाइलर का इस्तेमाल किया जाए. टूलचेन के दस्तावेज़ यहां देखे जा सकते हैं.

ऐसा करने के लिए, टूलचेन को उन प्लैटफ़ॉर्म की सीमाओं के सेट के साथ एनोटेट किया जाता है जिन पर वे काम करते हैं. ऐसा करने के लिए, टूलचेन की परिभाषा को दो हिस्सों में बांटा गया है:

  1. toolchain() नियम, जो किसी टूलचेन के साथ काम करने वाले एक्ज़ीक्यूशन और टारगेट की सीमाओं के सेट के बारे में बताता है. साथ ही, यह भी बताता है कि यह किस तरह का टूलचेन है, जैसे कि C++ या Java. टूलचेन के टाइप के बारे में toolchain_type() नियम से पता चलता है
  2. भाषा के हिसाब से बना नियम, जिसमें असल टूलचेन के बारे में बताया गया हो (जैसे कि cc_toolchain())

ऐसा इसलिए किया जाता है, क्योंकि टूलचेन रिज़ॉल्यूशन करने के लिए, हमें हर टूलचेन की सीमाओं के बारे में पता होना चाहिए. साथ ही, भाषा के हिसाब से *_toolchain() नियमों में इससे ज़्यादा जानकारी होती है, इसलिए उन्हें लोड होने में ज़्यादा समय लगता है.

एक्सीक्यूशन प्लैटफ़ॉर्म को इनमें से किसी एक तरीके से तय किया जाता है:

  1. register_execution_platforms() फ़ंक्शन का इस्तेमाल करके, WORKSPACE फ़ाइल में
  2. कमांड लाइन पर, --extra_execution_platforms कमांड लाइन के विकल्प का इस्तेमाल करके

उपलब्ध एक्ज़ीक्यूशन प्लैटफ़ॉर्म के सेट का हिसाब, RegisteredExecutionPlatformsFunction में लगाया जाता है .

कॉन्फ़िगर किए गए टारगेट के लिए टारगेट प्लैटफ़ॉर्म, PlatformOptions.computeTargetPlatform() से तय होता है . यह प्लैटफ़ॉर्म की सूची है, क्योंकि हम एक से ज़्यादा टारगेट प्लैटफ़ॉर्म के साथ काम करना चाहते हैं. हालांकि, फ़िलहाल इसे लागू नहीं किया गया है.

कॉन्फ़िगर किए गए टारगेट के लिए इस्तेमाल किए जाने वाले टूलचेन का सेट, ToolchainResolutionFunction से तय होता है. यह इनका फ़ंक्शन है:

  • रजिस्टर किए गए टूलचेन का सेट (WORKSPACE फ़ाइल और कॉन्फ़िगरेशन में)
  • कॉन्फ़िगरेशन में, पसंद के मुताबिक लागू करने और टारगेट करने के लिए प्लैटफ़ॉर्म
  • कॉन्फ़िगर किए गए टारगेट के लिए ज़रूरी टूलचैन टाइप का सेट (UnloadedToolchainContextKey) में
  • UnloadedToolchainContextKey में, कॉन्फ़िगर किए गए टारगेट (exec_compatible_with एट्रिब्यूट) और कॉन्फ़िगरेशन (--experimental_add_exec_constraints_to_targets) के लिए, प्लैटफ़ॉर्म पर लागू होने वाली पाबंदियों का सेट

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

इसके बाद, टूलचेन को असल में ResolvedToolchainContext.load() का इस्तेमाल करके लोड किया जाता है. साथ ही, उनका अनुरोध करने वाले कॉन्फ़िगर किए गए टारगेट को लागू करके उनका इस्तेमाल किया जाता है.

हमारे पास एक लेगसी सिस्टम भी है, जो एक ही "होस्ट" कॉन्फ़िगरेशन पर निर्भर करता है. साथ ही, टारगेट कॉन्फ़िगरेशन को अलग-अलग कॉन्फ़िगरेशन फ़्लैग, जैसे कि --cpu से दिखाया जाता है. हम धीरे-धीरे ऊपर बताए गए सिस्टम पर ट्रांज़िशन कर रहे हैं. ऐसे मामलों को हैंडल करने के लिए जहां लोग लेगसी कॉन्फ़िगरेशन वैल्यू पर भरोसा करते हैं, हमने प्लैटफ़ॉर्म मैपिंग लागू की है, ताकि लेगसी फ़्लैग और नए स्टाइल के प्लैटफ़ॉर्म की पाबंदियों के बीच अनुवाद किया जा सके. उनका कोड PlatformMappingFunction में है और इसमें Starlark के बजाय किसी दूसरी "लिटल लैंग्वेज" का इस्तेमाल किया गया है.

कंस्ट्रेंट

कभी-कभी कोई व्यक्ति अपने टारगेट को कुछ ही प्लैटफ़ॉर्म के साथ काम करने के तौर पर सेट करना चाहता है. अफ़सोस की बात है कि Bazel में, इस काम के लिए कई तरीके हैं:

  • नियम के हिसाब से पाबंदियां
  • environment_group() / environment()
  • प्लैटफ़ॉर्म से जुड़ी पाबंदियां

नियम-आधारित शर्तों का इस्तेमाल ज़्यादातर Google में Java के नियमों के लिए किया जाता है. ये आने वाली प्रोसेस में हैं और बेज़ल में उपलब्ध नहीं हैं. हालांकि, सोर्स कोड में इसके रेफ़रंस शामिल हो सकते हैं. इसे कंट्रोल करने वाले एट्रिब्यूट को constraints= कहा जाता है.

एनवायरमेंट_ग्रुप() और एनवायरमेंट()

ये नियम, लेगसी प्रोसेस के तहत आते हैं. साथ ही, इनका ज़्यादा इस्तेमाल नहीं किया जाता.

सभी बिल्ड नियमों से यह तय किया जा सकता है कि उन्हें किन "एनवायरमेंट" के लिए बनाया जा सकता है. यहां "एनवायरमेंट", environment() नियम का एक इंस्टेंस होता है.

किसी नियम के लिए, इस्तेमाल किए जा सकने वाले एनवायरमेंट की जानकारी देने के कई तरीके हैं:

  1. restricted_to= एट्रिब्यूट की मदद से. यह जानकारी देने का सबसे सीधा तरीका है. इसमें उन एनवायरमेंट के सटीक सेट के बारे में बताया जाता है जिन पर इस ग्रुप के लिए नियम लागू होता है.
  2. compatible_with= एट्रिब्यूट की मदद से. इससे उन एनवायरमेंट के बारे में पता चलता है जिन पर नियम काम करता है. इनमें, डिफ़ॉल्ट रूप से काम करने वाले "स्टैंडर्ड" एनवायरमेंट भी शामिल हैं.
  3. पैकेज-लेवल एट्रिब्यूट default_restricted_to= और default_compatible_with= के ज़रिए.
  4. environment_group() नियमों में डिफ़ॉल्ट तौर पर तय की गई शर्तों के ज़रिए. हर एनवायरमेंट, विषय के हिसाब से मिलते-जुलते पीयर के ग्रुप से जुड़ा होता है. जैसे, "सीपीयू आर्किटेक्चर", "JDK वर्शन" या "मोबाइल ऑपरेटिंग सिस्टम". किसी एनवायरमेंट ग्रुप की परिभाषा में यह शामिल होता है कि अगर restricted_to= / environment() एट्रिब्यूट ने इन एनवायरमेंट को किसी और तरीके से तय नहीं किया है, तो इनमें से कौनसे एनवायरमेंट "डिफ़ॉल्ट" के साथ काम करने चाहिए. ऐसे एट्रिब्यूट के बिना बनाए गए नियम में, सभी डिफ़ॉल्ट एट्रिब्यूट शामिल होते हैं.
  5. नियम क्लास के डिफ़ॉल्ट तौर पर. इससे, दिए गए नियम की क्लास के सभी उदाहरणों के लिए, ग्लोबल डिफ़ॉल्ट बदल जाते हैं. उदाहरण के लिए, इसका इस्तेमाल करके सभी *_test नियमों की जांच की जा सकती है. इसके लिए, हर इंस्टेंस को इस सुविधा के बारे में साफ़ तौर पर बताने की ज़रूरत नहीं होती.

environment() को सामान्य नियम के तौर पर लागू किया जाता है, जबकि environment_group(), Target का सबक्लास है. हालांकि, यह Rule (EnvironmentGroup) का सबक्लास नहीं है. साथ ही, यह Starlark (StarlarkLibrary.environmentGroup()) में डिफ़ॉल्ट रूप से उपलब्ध एक फ़ंक्शन है, जो आखिर में एक ही नाम वाला टारगेट बनाता है. ऐसा इसलिए किया जाता है, ताकि एक-दूसरे पर निर्भरता की समस्या न हो. यह समस्या इसलिए होती है, क्योंकि हर एनवायरमेंट को यह बताना होता है कि वह किस एनवायरमेंट ग्रुप से जुड़ा है. साथ ही, हर एनवायरमेंट ग्रुप को अपने डिफ़ॉल्ट एनवायरमेंट के बारे में बताना होता है.

--target_environment कमांड लाइन विकल्प की मदद से, किसी बिल्ड को किसी चुनिंदा एनवायरमेंट में प्रतिबंधित किया जा सकता है.

पाबंदी की जांच करने की सुविधा, RuleContextConstraintSemantics और TopLevelConstraintSemantics में लागू की जा रही है.

प्लैटफ़ॉर्म के कंट्रोल

यह बताने का मौजूदा "आधिकारिक" तरीका, उन प्लैटफ़ॉर्म के साथ काम करता है जिनका इस्तेमाल टूलचेन और प्लैटफ़ॉर्म के लिए किया जाता है. इसकी समीक्षा, पुल के अनुरोध #10945 में की जा रही है.

किसको दिखे

अगर आपको Google जैसे बड़े डेवलपर के साथ बड़े कोडबेस पर काम करना है, तो आपको यह ध्यान रखना होगा कि कोई भी व्यक्ति आपके कोड पर अपनी मर्ज़ी से काम न कर पाए. ऐसा न करने पर, हाइरम के नियम के मुताबिक, लोग उन व्यवहारों पर भरोसा करेंगे जिन्हें आपने लागू करने की जानकारी माना था.

Bazel, visibility नाम के तंत्र की मदद से ऐसा करता है: आपके पास यह बताने का विकल्प होता है कि किसी खास टारगेट पर सिर्फ़ visibility एट्रिब्यूट का इस्तेमाल करके निर्भर किया जा सकता है. यह एट्रिब्यूट थोड़ा खास है, क्योंकि इसमें लेबल की सूची होती है. हालांकि, ये लेबल किसी खास टारगेट के पॉइंटर के बजाय, पैकेज के नामों पर पैटर्न को कोड में बदल सकते हैं. (हां, यह डिज़ाइन में गड़बड़ी है.)

इसे इन जगहों पर लागू किया जाता है:

  • 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 का इस्तेमाल करके, इसे आसान तरीके से किया, तो हमें ज़्यादा मेमोरी का इस्तेमाल करना पड़ेगा: अगर N नियमों की एक चेन है और हर नियम एक फ़ाइल जोड़ता है, तो हमारे पास 1+2+...+N कलेक्शन मेंबर होंगे.

इस समस्या को हल करने के लिए, हमने NestedSet का कॉन्सेप्ट बनाया है. यह डेटा स्ट्रक्चर है, जो अन्य NestedSet इंस्टेंस और अपने कुछ सदस्यों से मिलकर बनता है. इस तरह, सेट का एक दिशा-निर्देश वाला असाइकलिक ग्राफ़ बनता है. इन्हें बदला नहीं जा सकता और सदस्यों को फिर से इस्तेमाल किया जा सकता है. हम कई क्रम (NestedSet.Order) तय करते हैं: प्रीऑर्डर, पोस्टऑर्डर, टॉपोलॉजिकल (कोई नोड हमेशा अपने पैरंट के बाद आता है) और "इस पर ध्यान न दें, लेकिन हर बार यह एक जैसा होना चाहिए".

Starlark में, इसी डेटा स्ट्रक्चर को depset कहा जाता है.

आर्टफ़ैक्ट और कार्रवाइयां

असल बिल्ड में कमांड का एक सेट होता है, जिसे उपयोगकर्ता की पसंद का आउटपुट देने के लिए चलाया जाना चाहिए. निर्देशों को क्लास Action के इंस्टेंस के तौर पर दिखाया जाता है और फ़ाइलों को क्लास Artifact के इंस्टेंस के तौर पर दिखाया जाता है. इन्हें दो हिस्सों में व्यवस्थित किया जाता है. इन्हें "ऐक्शन ग्राफ़" कहा जाता है. इन्हें दो हिस्सों में बांटा जाता है. इन्हें, एक असाइकलिक ग्राफ़ के तौर पर रखा जाता है.

आर्टफ़ैक्ट दो तरह के होते हैं: सोर्स आर्टफ़ैक्ट (वे आर्टफ़ैक्ट जो Bazel के शुरू होने से पहले उपलब्ध होते हैं) और डेरिव्ड आर्टफ़ैक्ट (वे आर्टफ़ैक्ट जिन्हें बनाना ज़रूरी होता है). डेरिव्ड आर्टफ़ैक्ट कई तरह के हो सकते हैं:

  1. **सामान्य आर्टफ़ैक्ट. **इन फ़ाइलों के अप-टू-डेट होने की जांच, उनके चेकसम का हिसाब लगाकर की जाती है. इसके लिए, mtime को शॉर्टकट के तौर पर इस्तेमाल किया जाता है. अगर फ़ाइल के mtime में कोई बदलाव नहीं होता है, तो हम फ़ाइल का चेकसम नहीं करते.
  2. समाधान नहीं किए गए सिंबललिंक आर्टफ़ैक्ट. readlink() को कॉल करके, इनके अप-टू-डेट होने की जांच की जाती है. सामान्य आर्टफ़ैक्ट के मुकाबले, ये डैंगलिंग स्लिंक्स हो सकते हैं. आम तौर पर, इसका इस्तेमाल उन मामलों में किया जाता है जहां कुछ फ़ाइलों को किसी तरह के संग्रह में पैक किया जाता है.
  3. ट्री आर्टफ़ैक्ट. ये सिंगल फ़ाइलें नहीं, बल्कि डायरेक्ट्री ट्री हैं. फ़ाइलों के सेट और उनके कॉन्टेंट की जांच करके, फ़ाइलों के अप-टू-डेट होने की जांच की जाती है. इन्हें TreeArtifact के तौर पर दिखाया जाता है.
  4. मेटाडेटा के लगातार आर्टफ़ैक्ट. इन आर्टफ़ैक्ट में किए जाने वाले बदलावों की वजह से, दोबारा बनने की प्रोसेस ट्रिगर नहीं होती. इसका इस्तेमाल सिर्फ़ बिल्ड स्टैंप की जानकारी के लिए किया जाता है: हम सिर्फ़ इसलिए फिर से बिल्ड नहीं करना चाहते, क्योंकि मौजूदा समय बदल गया है.

सोर्स आर्टफ़ैक्ट, ट्री आर्टफ़ैक्ट या हल नहीं किए गए सिंबललिंक आर्टफ़ैक्ट क्यों नहीं हो सकते, इसकी कोई बुनियादी वजह नहीं है. ऐसा इसलिए है, क्योंकि हमने इसे अब तक लागू नहीं किया है. हालांकि, हमें इसे लागू करना चाहिए -- BUILD फ़ाइल में सोर्स डायरेक्ट्री का रेफ़रंस देना, Bazel की कुछ ऐसी गड़बड़ियों में से एक है जो लंबे समय से मौजूद हैं. हमारे पास इस तरह के काम करने वाला एक लागू तरीका है, जिसे BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM प्रॉपर्टी से चालू किया जाता है

Artifact के एक खास तरह के उदाहरण हैं, मध्यस्थ. इन्हें Artifact इंस्टेंस से दिखाया जाता है, जो MiddlemanAction के आउटपुट होते हैं. इनका इस्तेमाल, कुछ चीज़ों के लिए खास तौर पर किया जाता है:

  • आर्टफ़ैक्ट को एक साथ ग्रुप करने के लिए, एग्रीगेट करने वाले मिडलमैन का इस्तेमाल किया जाता है. ऐसा इसलिए किया जाता है, ताकि अगर कई कार्रवाइयां इनपुट के एक ही बड़े सेट का इस्तेमाल करती हैं, तो हमारे पास N*M डिपेंडेंसी एज न हों, सिर्फ़ N+M (इन्हें नेस्ट किए गए सेट से बदला जा रहा है)
  • डिपेंडेंसी मिडलमैन को शेड्यूल करने से यह पक्का होता है कि एक कार्रवाई, दूसरी कार्रवाई से पहले पूरी हो. ज़्यादातर इसका इस्तेमाल लिंटिंग के लिए किया जाता है. हालांकि, C++ कंपाइलेशन के लिए भी इसका इस्तेमाल किया जाता है (ज़्यादा जानकारी के लिए CcCompilationContext.createMiddleman() देखें)
  • Runfiles मिडलमैन का इस्तेमाल, Runfiles ट्री की मौजूदगी की पुष्टि करने के लिए किया जाता है, ताकि किसी को आउटपुट मेनिफ़ेस्ट और Runfiles ट्री के रेफ़रंस वाले हर आर्टफ़ैक्ट पर अलग से निर्भर न होना पड़े.

ऐक्शन को सबसे अच्छी तरह से एक ऐसे निर्देश के तौर पर समझा जा सकता है जिसे चलाने के लिए, एक खास तरह के एनवायरमेंट की ज़रूरत होती है. साथ ही, यह भी ज़रूरी है कि यह निर्देश, एक खास तरह के आउटपुट जनरेट करता हो. किसी कार्रवाई के ब्यौरे के मुख्य कॉम्पोनेंट ये हैं:

  • वह कमांड लाइन जिसे चलाना है
  • इसके लिए ज़रूरी इनपुट आर्टफ़ैक्ट
  • ऐसे एनवायरमेंट वैरिएबल जिन्हें सेट करना ज़रूरी है
  • ऐसे एनोटेशन जिनमें उस प्लैटफ़ॉर्म के बारे में बताया गया हो जिस पर इसे चलाना है \

कुछ और खास मामले भी हैं, जैसे कि ऐसी फ़ाइल लिखना जिसका कॉन्टेंट, Bazel को पता हो. ये AbstractAction के सबक्लास हैं. ज़्यादातर कार्रवाइयां SpawnAction या StarlarkAction होती हैं (यही भी होती है कि ये अलग-अलग क्लास नहीं होनी चाहिए). हालांकि, Java और C++ के ऐक्शन टाइप (JavaCompileAction, CppCompileAction, और CppLinkAction) हैं.

हम आखिर में सभी चीज़ों को SpawnAction पर ले जाना चाहते हैं; JavaCompileAction काफ़ी करीब है, लेकिन .d फ़ाइल को पार्स करने और शामिल करने की स्कैनिंग की वजह से, C++ थोड़ा खास मामला है.

ऐक्शन ग्राफ़ को ज़्यादातर Skyframe ग्राफ़ में "एम्बेड" किया जाता है: कॉन्सेप्ट के हिसाब से, किसी ऐक्शन को लागू करने को ActionExecutionFunction को कॉल करने के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से Skyframe डिपेंडेंसी एज की मैपिंग के बारे में ActionExecutionFunction.getInputDeps() और Artifact.key() में बताया गया है. इसमें Skyframe एज की संख्या कम रखने के लिए, कुछ ऑप्टिमाइज़ेशन किए गए हैं:

  • डेरिव्ड आर्टफ़ैक्ट के पास अपने SkyValue नहीं होते. इसके बजाय, Artifact.getGeneratingActionKey() का इस्तेमाल उस कार्रवाई की कुंजी का पता लगाने के लिए किया जाता है जो इसे जनरेट करती है
  • नेस्ट किए गए सेट की अपनी Skyframe कुंजी होती है.

शेयर की गई कार्रवाइयां

कुछ कार्रवाइयां, कॉन्फ़िगर किए गए कई टारगेट से जनरेट होती हैं. Starlark नियमों के दायरे में ज़्यादा कार्रवाइयां नहीं आती हैं, क्योंकि उन्हें सिर्फ़ अपने कॉन्फ़िगरेशन और पैकेज के हिसाब से तय की गई डायरेक्ट्री में, डेरिव्ड ऐक्शन डालने की अनुमति होती है. हालांकि, एक ही पैकेज में मौजूद नियमों में अंतर हो सकता है. वहीं, Java में लागू किए गए नियमों से, डेरिव्ड आर्टफ़ैक्ट को कहीं भी डाला जा सकता है.

इसे एक गलत सुविधा माना जाता है, लेकिन इससे छुटकारा पाना बहुत मुश्किल है, क्योंकि इससे एक्ज़ीक्यूशन में लगने वाले समय में काफ़ी बचत होती है. उदाहरण के लिए, किसी सोर्स फ़ाइल को प्रोसेस करने की ज़रूरत होती है और उस फ़ाइल का रेफ़रंस एक से ज़्यादा नियमों (हैंडवेव-हैंडवेव) से लगाया जाता है. इसमें कुछ रैम का खर्च आता है: शेयर की गई कार्रवाई के हर इंस्टेंस को मेमोरी में अलग-अलग सेव करना ज़रूरी है.

अगर दो कार्रवाइयां एक ही आउटपुट फ़ाइल जनरेट करती हैं, तो वे एक जैसी होनी चाहिए: उनमें एक जैसे इनपुट, एक जैसे आउटपुट होने चाहिए और एक ही कमांड लाइन को चलाना चाहिए. यह समानता संबंध Actions.canBeShared() में लागू किया जाता है और हर कार्रवाई को देखकर, विश्लेषण और निष्पादन के चरणों के बीच इसकी पुष्टि की जाती है. इसे SkyframeActionExecutor.findAndStoreArtifactConflicts() में लागू किया गया है और यह Bazel की उन कुछ जगहों में से एक है जहां बिल्ड के "ग्लोबल" व्यू की ज़रूरत होती है.

लागू करने का चरण

इसके बाद, Bazel असल में बिल्ड ऐक्शन चलाना शुरू करता है. जैसे, आउटपुट देने वाले कमांड.

विश्लेषण के बाद, Bazel सबसे पहले यह तय करता है कि कौनसे आर्टफ़ैक्ट बनाने हैं. इसके लिए लॉजिक, TopLevelArtifactHelper में कोड में बदला गया है. यह कमांड लाइन पर कॉन्फ़िगर किए गए टारगेट का filesToBuild और खास आउटपुट ग्रुप का कॉन्टेंट है. इसका मकसद, "अगर यह टारगेट कमांड लाइन पर है, तो इन आर्टफ़ैक्ट को बनाएं" को साफ़ तौर पर बताना है.

अगला चरण, एक्सीक्यूशन रूट बनाना है. Bazel के पास फ़ाइल सिस्टम (--package_path) में मौजूद अलग-अलग जगहों से सोर्स पैकेज पढ़ने का विकल्प होता है. इसलिए, इसे पूरे सोर्स ट्री के साथ, लोकल तौर पर की जाने वाली कार्रवाइयां उपलब्ध करानी होती हैं. इसे क्लास SymlinkForest मैनेज करता है. यह विश्लेषण के फ़ेज़ में इस्तेमाल किए गए हर टारगेट को ध्यान में रखकर काम करता है. साथ ही, एक डायरेक्ट्री ट्री बनाता है, जो हर पैकेज को इस्तेमाल किए गए टारगेट के साथ उसकी असल जगह से लिंक करता है. इसका दूसरा विकल्प यह है कि आप निर्देशों के लिए सही पाथ भेजें (--package_path को ध्यान में रखते हुए). ऐसा नहीं करना चाहिए, क्योंकि:

  • जब किसी पैकेज को पैकेज पाथ एंट्री से दूसरे में ले जाया जाता है, तो यह ऐक्शन कमांड लाइन बदल देता है (इसे आम तौर पर इस्तेमाल किया जाता था)
  • अगर कोई कार्रवाई, स्थानीय तौर पर की जाती है, तो इससे अलग कमांड लाइन बनती हैं. हालांकि, अगर कार्रवाई को रिमोट तौर पर किया जाता है, तो इससे अलग कमांड लाइन बनती हैं
  • इसके लिए, इस्तेमाल किए जा रहे टूल के हिसाब से कमांड लाइन ट्रांसफ़ॉर्मेशन की ज़रूरत होती है (जैसे, Java क्लासपाथ और C++ शामिल पाथ के बीच का अंतर)
  • किसी ऐक्शन की कमांड लाइन बदलने पर, उसकी ऐक्शन कैश मेमोरी एंट्री अमान्य हो जाती है
  • --package_path के इस्तेमाल पर धीरे-धीरे रोक लगाई जा रही है

इसके बाद, बेज़ल ऐक्शन ग्राफ़ (दो हिस्सों में बंटे, डायरेक्ट ग्राफ़ में ऐक्शन और उनके इनपुट और आउटपुट आर्टफ़ैक्ट) को ट्रैक करना और ऐक्शन चलाना शुरू कर देता है. हर कार्रवाई को SkyValue क्लास ActionExecutionValue के इंस्टेंस से दिखाया जाता है.

कार्रवाई करना महंगा है, इसलिए हमारे पास कैश मेमोरी की कुछ लेयर होती हैं जिन्हें SkyFrame के पीछे चलाया जा सकता है:

  • ActionExecutionFunction.stateMap में डेटा शामिल होता है, जिससे स्काईफ़्रेम ActionExecutionFunction के सस्ते रीस्टार्ट होने की संभावना बढ़ जाती है
  • लोकल ऐक्शन कैश मेमोरी में, फ़ाइल सिस्टम की स्थिति का डेटा होता है
  • रिमोट एक्ज़िक्यूशन सिस्टम में आम तौर पर खुद की कैश मेमोरी भी होती है

लोकल ऐक्शन कैश मेमोरी

यह कैश, Skyframe के पीछे मौजूद एक और लेयर है. भले ही, Skyframe में कोई कार्रवाई फिर से की गई हो, फिर भी यह स्थानीय ऐक्शन कैश में हिट हो सकती है. यह, लोकल फ़ाइल सिस्टम की स्थिति दिखाता है और इसे डिस्क पर सीरियलाइज़ किया जाता है. इसका मतलब है कि जब कोई नया Bazel सर्वर शुरू किया जाता है, तो Skyframe ग्राफ़ खाली होने के बावजूद, लोकल ऐक्शन कैश मेमोरी में हिट मिल सकते हैं.

इस कैश मेमोरी में हिट की जांच, ActionCacheChecker.getTokenIfNeedToExecute() तरीके का इस्तेमाल करके की जाती है .

नाम के उलट, यह मैप, डेरिव्ड आर्टफ़ैक्ट के पाथ से उस ऐक्शन तक का मैप होता है जिसने उसे उत्सर्जित किया है. कार्रवाई के बारे में इस तरह बताया गया है:

  1. इनपुट और आउटपुट फ़ाइलों का सेट और उनका चेकसम
  2. इसकी "ऐक्शन बटन" आम तौर पर एक्ज़ीक्यूट की जाने वाली कमांड लाइन होती है. हालांकि, आम तौर पर इससे उन सभी चीज़ों को दिखाया जाता है जिन्हें इनपुट फ़ाइलों के चेकसम से कैप्चर नहीं किया जाता. जैसे, FileWriteAction के लिए, यह लिखे गए डेटा का चेकसम है

एक बेहद प्रयोग के तौर पर उपलब्ध "टॉप-डाउन ऐक्शन कैश" भी है जो अब भी डेवलप हो रहा है. यह कैश मेमोरी में कई बार जाने से बचने के लिए, ट्रांज़िटिव हैश का इस्तेमाल करता है.

इनपुट की खोज और इनपुट को छोटा करना

कुछ कार्रवाइयां, इनपुट के सेट से ज़्यादा जटिल होती हैं. किसी कार्रवाई के इनपुट के सेट में बदलाव के दो तरीके होते हैं:

  • कोई कार्रवाई अपने एक्ज़ीक्यूशन से पहले नए इनपुट खोज सकती है या यह तय कर सकती है कि इसके कुछ इनपुट ज़रूरी नहीं हैं. C++ का उदाहरण लें, जहां यह अनुमान लगाना बेहतर होता है कि C++ फ़ाइल, ट्रांज़िटिव क्लोज़र से कौनसी हेडर फ़ाइलों का इस्तेमाल करती है, ताकि हमें हर फ़ाइल को रिमोट एक्सीक्यूटर को भेजने की ज़रूरत न पड़े. इसलिए, हमारे पास हर हेडर फ़ाइल को "इनपुट" के तौर पर रजिस्टर न करने का विकल्प है. हालांकि, ट्रांज़िटिव तौर पर शामिल किए गए हेडर के लिए सोर्स फ़ाइल को स्कैन करें और सिर्फ़ उन हेडर फ़ाइलों को इनपुट के तौर पर मार्क करें जिनके बारे में #include स्टेटमेंट में बताया गया है. हम ज़्यादा अनुमान लगाते हैं, ताकि हमें C प्रोसेसर्वर को पूरी तरह से लागू करने की ज़रूरत न पड़े. फ़िलहाल, यह विकल्प Bazel में "गलत" के तौर पर हार्ड-वाइर्ड है और इसका इस्तेमाल सिर्फ़ Google में किया जाता है.
  • किसी कार्रवाई को पूरा करने के दौरान, हो सकता है कि कुछ फ़ाइलों का इस्तेमाल न किया गया हो. C++ में, इसे ".d फ़ाइलें" कहा जाता है: कंपाइलर बताता है कि कौनसी हेडर फ़ाइलों का इस्तेमाल किया गया था. Make की तुलना में, बेहतर इंक्रीमेंटलिटी पाने के लिए, Bazel इस फ़ैक्ट का इस्तेमाल करता है. यह शामिल करने वाले स्कैनर की तुलना में बेहतर अनुमान देता है, क्योंकि यह कंपाइलर पर निर्भर करता है.

इन्हें ऐक्शन के तरीकों का इस्तेमाल करके लागू किया जाता है:

  1. Action.discoverInputs() को कॉल किया जाता है. यह ज़रूरी होने पर, नेस्ट किए गए आर्टफ़ैक्ट का सेट दिखाता है. ये सोर्स आर्टफ़ैक्ट होने चाहिए, ताकि ऐक्शन ग्राफ़ में ऐसे डिपेंडेंसी एज न हों जो कॉन्फ़िगर किए गए टारगेट ग्राफ़ में मिलता-जुलता न हो.
  2. Action.execute() को कॉल करके कार्रवाई की जाती है.
  3. Action.execute() के आखिर में की गई कार्रवाई Action.updateInputs() को कॉल करके, बेज़ल को बता सकती है कि उसके हर इनपुट की ज़रूरत नहीं है. अगर इस्तेमाल किए गए इनपुट को इस्तेमाल नहीं किए गए के तौर पर रिपोर्ट किया जाता है, तो इससे गलत इंक्रीमेंटल बिल्ड बन सकते हैं.

जब कोई ऐक्शन कैश मेमोरी, किसी नए ऐक्शन इंस्टेंस (जैसे, सर्वर को रीस्टार्ट करने के बाद बनाया गया) पर हिट दिखाती है, तो Bazel खुद updateInputs() को कॉल करता है, ताकि इनपुट का सेट, इनपुट की खोज और पहले की गई छंटाई का नतीजा दिखा सके.

Starlark ऐक्शन, ctx.actions.run() के unused_inputs_list= आर्ग्युमेंट का इस्तेमाल करके, कुछ इनपुट को इस्तेमाल न किए गए के तौर पर घोषित करने के लिए, इस सुविधा का इस्तेमाल कर सकते हैं.

कार्रवाइयां चलाने के अलग-अलग तरीके: रणनीतियां/ActionContexts

कुछ कार्रवाइयां अलग-अलग तरीकों से चलाई जा सकती हैं. उदाहरण के लिए, किसी कमांड लाइन को स्थानीय तौर पर, स्थानीय तौर पर अलग-अलग तरह के सैंडबॉक्स में या फिर किसी दूसरी जगह से चलाया जा सकता है. इस कॉन्सेप्ट को ActionContext (या Strategy, क्योंकि हमने नाम बदलने की प्रोसेस को सिर्फ़ आधा पूरा किया है...) कहा जाता है

किसी ऐक्शन कॉन्टेक्स्ट का लाइफ़ साइकल इस तरह होता है:

  1. जब कार्रवाइयां शुरू की जाती हैं, तो BlazeModule इंस्टेंस से पूछा जाता है कि उनके पास कौनसे ऐक्शन कॉन्टेक्स्ट हैं. ऐसा ExecutionTool के कंस्ट्रक्टर में होता है. ऐक्शन कॉन्टेक्स्ट टाइप की पहचान, Java Class के किसी ऐसे इंस्टेंस से की जाती है जो ActionContext के किसी सब-इंटरफ़ेस को रेफ़र करता है. साथ ही, यह भी तय करता है कि ऐक्शन कॉन्टेक्स्ट को किस इंटरफ़ेस को लागू करना चाहिए.
  2. कार्रवाई के सही संदर्भ को उपलब्ध विकल्पों में से चुना जाता है. इसके बाद, उसे ActionExecutionContext और BlazeExecutor पर भेज दिया जाता है.
  3. कार्रवाइयां, ActionExecutionContext.getContext() और BlazeExecutor.getStrategy() का इस्तेमाल करके कॉन्टेक्स्ट का अनुरोध करती हैं (इसके लिए, सिर्फ़ एक तरीका होना चाहिए…)

काम करने के लिए, रणनीतियां बिना किसी शुल्क के अन्य रणनीतियों का इस्तेमाल कर सकती हैं. उदाहरण के लिए, इसका इस्तेमाल ऐसी डाइनैमिक रणनीति में किया जाता है जो स्थानीय तौर पर या किसी दूसरी जगह से, दोनों तरह से कार्रवाइयां शुरू करती है और फिर इनमें से जो भी हो सके उसके हिसाब से काम करती है.

एक ध्यान देने लायक रणनीति वह है जो स्थायी वर्कर प्रोसेस (WorkerSpawnStrategy) को लागू करती है. कुछ टूल में स्टार्टअप का समय ज़्यादा होता है. इसलिए, इन टूल को हर कार्रवाई के लिए एक बार में शुरू करने के बजाय, एक ऐक्शन के बीच फिर से इस्तेमाल किया जाना चाहिए (यह सही समस्या के बारे में बताता है, क्योंकि बेज़ल वर्कर प्रोसेस के वादे पर निर्भर करता है कि वह अलग-अलग अनुरोधों के बीच मॉनिटर की जा सकने वाली स्थिति नहीं लागू करता)

टूल बदलने पर, वर्कर्स प्रोसेस को फिर से शुरू करना होगा. किसी वर्कफ़्लो का फिर से इस्तेमाल किया जा सकता है या नहीं, यह तय करने के लिए WorkerFilesHash का इस्तेमाल करके, इस्तेमाल किए गए टूल का चेकसम कैलकुलेट किया जाता है. यह इस बात पर निर्भर करता है कि कार्रवाई के कौनसे इनपुट टूल का हिस्सा हैं और कौनसे इनपुट हैं; यह कार्रवाई बनाने वाले व्यक्ति तय करता है: Spawn.getToolFiles() और Spawn की रनफ़ाइलों को टूल के हिस्से के तौर पर गिना जाता है.

रणनीतियों (या कार्रवाई से जुड़े कॉन्टेक्स्ट!) के बारे में ज़्यादा जानकारी:

  • कार्रवाइयों के लिए अलग-अलग रणनीतियों के बारे में जानकारी यहां दी गई है.
  • डाइनैमिक स्ट्रेटजी के बारे में जानकारी, जहां हम स्थानीय और दूसरी जगह से कार्रवाई करते हैं, ताकि यह देखा जा सके कि कौनसी रणनीति सबसे पहले उपलब्ध है. इसके लिए, यहां जाएं.
  • स्थानीय तौर पर कार्रवाइयां करने की बारीकियों के बारे में जानकारी यहां उपलब्ध है.

लोकल रिसोर्स मैनेजर

Bazel, कई कार्रवाइयों को एक साथ चल सकता है. एक साथ कई स्थानीय कार्रवाइयां चलानी चाहिए या नहीं, यह कार्रवाई के हिसाब से अलग-अलग होता है: किसी कार्रवाई के लिए ज़्यादा संसाधनों की ज़रूरत होने पर, एक ही समय पर कम इंस्टेंस चलाए जाने चाहिए, ताकि स्थानीय मशीन पर लोड कम हो.

इसे ResourceManager क्लास में लागू किया गया है: हर कार्रवाई के लिए, ResourceSet इंस्टेंस (सीपीयू और रैम) के तौर पर, ज़रूरी लोकल संसाधनों के अनुमान के साथ एनोटेट करना होगा. इसके बाद, जब ऐक्शन कॉन्टेक्स्ट ऐसा कुछ करते हैं जिसके लिए स्थानीय संसाधनों की ज़रूरत होती है, तो वे ResourceManager.acquireResources() को कॉल करते हैं और ज़रूरी संसाधन उपलब्ध होने तक ब्लॉक रहते हैं.

लोकल रिसॉर्स मैनेजमेंट के बारे में ज़्यादा जानकारी यहां उपलब्ध है.

आउटपुट डायरेक्ट्री का स्ट्रक्चर

हर कार्रवाई के लिए आउटपुट डायरेक्ट्री में एक अलग जगह होनी चाहिए, जहां वह आउटपुट को रखती है. आम तौर पर, डेरिव्ड आर्टफ़ैक्ट की जगह इस तरह होती है:

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

किसी खास कॉन्फ़िगरेशन से जुड़ी डायरेक्ट्री का नाम कैसे तय किया जाता है? ऐसी दो प्रॉपर्टी हैं जो एक-दूसरे से मेल नहीं खातीं:

  1. अगर एक ही बिल्ड में दो कॉन्फ़िगरेशन हो सकते हैं, तो उनके पास अलग-अलग डायरेक्ट्री होनी चाहिए, ताकि दोनों के पास एक ही ऐक्शन का अपना वर्शन हो. अगर ऐसा नहीं है और दोनों कॉन्फ़िगरेशन एक ही आउटपुट फ़ाइल बनाने वाले ऐक्शन की कमांड लाइन के बारे में अलग-अलग हैं, तो Bazel को यह नहीं पता होता कि कौनसा ऐक्शन चुनना है. इसे "ऐक्शन का विरोध" कहा जाता है
  2. अगर दो कॉन्फ़िगरेशन "लगभग" एक ही चीज़ को दिखाते हैं, तो उनका नाम एक ही होना चाहिए, ताकि कमांड लाइन मैच होने पर, एक में की गई कार्रवाइयों का फिर से इस्तेमाल किया जा सके: उदाहरण के लिए, Java कंपाइलर के कमांड लाइन विकल्पों में किए गए बदलावों की वजह से, C++ कंपाइल करने की कार्रवाइयां फिर से नहीं चलाई जानी चाहिए.

अब तक, हम इस समस्या को हल करने का कोई ऐसा तरीका नहीं ढूंढ पाए हैं जो कॉन्फ़िगरेशन ट्रिम करने की समस्या से मिलता-जुलता हो. विकल्पों के बारे में ज़्यादा लंबी जानकारी यहां उपलब्ध है. समस्या वाले मुख्य हिस्से, Starlark नियम (जिनके लेखक आम तौर पर Bazel के बारे में अच्छी तरह से नहीं जानते) और ऐसेपेक्ट हैं. ये ऐसेपेक्ट, उन चीज़ों के स्पेस में एक और डाइमेंशन जोड़ते हैं जिनसे "एक ही" आउटपुट फ़ाइल बन सकती है.

फ़िलहाल, कॉन्फ़िगरेशन के लिए पाथ सेगमेंट <CPU>-<compilation mode> है. इसमें अलग-अलग सफ़िक्स जोड़े गए हैं, ताकि Java में लागू किए गए कॉन्फ़िगरेशन ट्रांज़िशन से ऐक्शन में कोई विरोध न हो. इसके अलावा, Starlark कॉन्फ़िगरेशन ट्रांज़िशन के सेट का चेकसम जोड़ा गया है, ताकि उपयोगकर्ता कार्रवाई से जुड़ी समस्याएं पैदा न कर सकें. यह पूरी तरह से सही नहीं है. इसे OutputDirectories.buildMnemonic() में लागू किया जाता है. साथ ही, यह हर कॉन्फ़िगरेशन फ़्रैगमेंट पर निर्भर करता है, जो आउटपुट डायरेक्ट्री के नाम में अपना हिस्सा जोड़ता है.

जांच

Bazel में टेस्ट चलाने के लिए, कई सुविधाएं उपलब्ध हैं. यह इनके साथ काम करता है:

  • रिमोट तौर पर टेस्ट चलाना (अगर रिमोट तौर पर टेस्ट चलाने की सुविधा उपलब्ध है)
  • एक साथ कई बार टेस्ट चलाना (डेटा इकट्ठा करने या समय का डेटा इकट्ठा करने के लिए)
  • शार्डिंग टेस्ट (स्पीड के लिए कई प्रोसेस पर एक ही टेस्ट में टेस्ट केस को अलग करना)
  • काम न करने वाले टेस्ट फिर से चलाना
  • टेस्ट को टेस्ट सुइट में ग्रुप करना

टेस्ट, रेगुलर तौर पर कॉन्फ़िगर किए गए ऐसे टारगेट होते हैं जिनमें TestProvider होता है. इससे यह पता चलता है कि टेस्ट को कैसे चलाया जाना चाहिए:

  • ऐसे आर्टफ़ैक्ट जिनकी बिल्डिंग के नतीजे में टेस्ट चल रहा है. यह एक "कैश मेमोरी का स्टेटस" फ़ाइल है, जिसमें TestResultData मैसेज को सीरियलाइज़ किया गया है
  • टेस्ट को कितनी बार चलाना चाहिए
  • टेस्ट को कितने हिस्सों में बांटना है
  • टेस्ट को चलाने के तरीके से जुड़े कुछ पैरामीटर, जैसे कि टेस्ट का टाइम आउट

यह तय करना कि कौनसे टेस्ट चलाने हैं

कौनसे टेस्ट चलाए जाएं, यह तय करना बहुत मुश्किल प्रक्रिया है.

सबसे पहले, टारगेट पैटर्न को पार्स करने के दौरान, टेस्ट सुइट को बार-बार बड़ा किया जाता है. यह एक्सपैंशन, TestsForTargetPatternFunction में लागू किया गया है. एक तरह से यह आश्चर्य की बात है कि अगर किसी टेस्ट सुइट में कोई टेस्ट नहीं है, तो इसका मतलब है कि उसके पैकेज में मौजूद हर टेस्ट के लिए ऐसा है. इसे Package.beforeBuild() में, सुइट के नियमों की जांच करने के लिए $implicit_tests नाम का इंप्लिसिट एट्रिब्यूट जोड़कर लागू किया जाता है.

इसके बाद, कमांड लाइन के विकल्पों के हिसाब से साइज़, टैग, टाइम आउट, और भाषा के हिसाब से जांच को फ़िल्टर किया जाता है. इसे TestFilter में लागू किया जाता है और टारगेट पार्स करने के दौरान, TargetPatternPhaseFunction.determineTests() से इसे कॉल किया जाता है. साथ ही, नतीजे को TargetPatternPhaseValue.getTestsToRunLabels() में डाला जाता है. फ़िल्टर किए जा सकने वाले नियम एट्रिब्यूट को कॉन्फ़िगर नहीं किया जा सकता, क्योंकि यह विश्लेषण के चरण से पहले होता है. इसलिए, कॉन्फ़िगरेशन उपलब्ध नहीं होता.

इसके बाद, इसे BuildView.createResult() में और प्रोसेस किया जाता है: जिन टारगेट का विश्लेषण नहीं हो पाया उन्हें फ़िल्टर कर दिया जाता है और टेस्ट को एक्सक्लूज़िव और नॉन-एक्सक्लूज़िव टेस्ट में बांट दिया जाता है. इसके बाद, इसे AnalysisResult में डाल दिया जाता है. इससे ExecutionTool को पता चलता है कि कौनसे टेस्ट चलाने हैं.

इस पूरी प्रोसेस को ज़्यादा पारदर्शी बनाने के लिए, tests() क्वेरी ऑपरेटर (TestsFunction में लागू किया गया) उपलब्ध है. इससे यह पता चलता है कि कमांड लाइन पर किसी खास टारगेट के बताए जाने पर कौनसे टेस्ट चलाए जाते हैं. माफ़ करें, इसे फिर से लागू किया जा रहा है. इसलिए, हो सकता है कि यह ऊपर बताए गए तरीके से कई मायनों में अलग हो.

टेस्ट चलाना

कैश मेमोरी की स्थिति बताने वाले आर्टफ़ैक्ट के लिए अनुरोध करके, जांच की जाती है. इसके बाद, TestRunnerAction को लागू किया जाता है. यह --test_strategy कमांड लाइन विकल्प से चुने गए TestActionContext को कॉल करता है. TestActionContext, टेस्ट को अनुरोध किए गए तरीके से चलाता है.

टेस्ट एक विस्तृत प्रोटोकॉल के मुताबिक किए जाते हैं. इसमें एनवायरमेंट वैरिएबल का इस्तेमाल करके यह बताया जाता है कि टेस्ट से क्या उम्मीद की जा सकती है. Basel को टेस्ट से क्या उम्मीदें हैं और कौनसे टेस्ट बेज़ल से हो सकते हैं, इस बारे में ज़्यादा जानकारी यहां दी गई है. सबसे आसान तरीके से, 0 वाले एक्सिट कोड का मतलब है कि प्रोसेस पूरी हो गई है. किसी भी अन्य कोड का मतलब है कि प्रोसेस पूरी नहीं हुई है.

कैश मेमोरी की स्थिति वाली फ़ाइल के अलावा, हर जांच प्रोसेस से कई अन्य फ़ाइलें बनती हैं. इन्हें "टेस्ट लॉग डायरेक्ट्री" में डाला जाता है. यह टारगेट कॉन्फ़िगरेशन की आउटपुट डायरेक्ट्री की सबडायरेक्ट्री होती है, जिसे testlogs कहा जाता है:

  • test.xml, JUnit स्टाइल वाली एक्सएमएल फ़ाइल, जिसमें टेस्ट स्HARD में मौजूद अलग-अलग टेस्ट केस की जानकारी होती है
  • test.log, टेस्ट का कंसोल आउटपुट. stdout और stderr को अलग नहीं किया गया है.
  • test.outputs, "बिना एलान की गई आउटपुट डायरेक्ट्री"; इसका इस्तेमाल उन टेस्ट के लिए किया जाता है जो टर्मिनल पर प्रिंट करने के अलावा, फ़ाइलों को भी आउटपुट करना चाहते हैं.

टेस्ट को लागू करने के दौरान दो चीज़ें हो सकती हैं, जो सामान्य टारगेट बनाते समय नहीं हो सकतीं: एक्सक्लूज़िव टेस्ट लागू करना और आउटपुट स्ट्रीम करना.

कुछ टेस्ट को एक्सक्लूज़िव मोड में चलाना ज़रूरी होता है. उदाहरण के लिए, इन्हें अन्य टेस्ट के साथ नहीं चलाना चाहिए. इसे जांच के नियम में tags=["exclusive"] जोड़कर या --test_strategy=exclusive के साथ जांच चलाकर पाया जा सकता है . हर खास जांच को एक अलग SkyFrame के ज़रिए चलाया जाता है. इसमें, "मुख्य" बिल्ड के बाद टेस्ट को लागू करने का अनुरोध किया जाता है. इसे SkyframeExecutor.runExclusiveTest() में लागू किया गया है.

सामान्य कार्रवाइयों के उलट, जिनका टर्मिनल आउटपुट कार्रवाई पूरी होने पर डंप हो जाता है, उपयोगकर्ता टेस्ट के आउटपुट को स्ट्रीम करने का अनुरोध कर सकता है, ताकि उन्हें लंबे समय तक चलने वाले टेस्ट की प्रोग्रेस के बारे में जानकारी मिल सके. इसकी जानकारी, --test_output=streamed कमांड लाइन विकल्प से मिलती है. इससे, खास टेस्ट को चलाने का मतलब है, ताकि अलग-अलग टेस्ट के आउटपुट एक-दूसरे में न मिलें.

इसे StreamedTestOutput क्लास में लागू किया गया है. यह काम, टेस्ट की test.log फ़ाइल में हुए बदलावों को पोल करके करता है. साथ ही, Bazel के नियमों वाले टर्मिनल में नए बाइट डालता है.

लागू किए गए टेस्ट के नतीजे, इवेंट बस में उपलब्ध होते हैं. ये नतीजे, अलग-अलग इवेंट (जैसे कि TestAttempt, TestResult या TestingCompleteEvent) पर नज़र रखते हैं. इन्हें बिल्ड इवेंट प्रोटोकॉल में भेजा जाता है और AggregatingTestListener के ज़रिए, इन्हें कंसोल पर भेज दिया जाता है.

कवरेज कलेक्शन

कवरेज की रिपोर्ट, bazel-testlogs/$PACKAGE/$TARGET/coverage.dat फ़ाइलों में LCOV फ़ॉर्मैट में टेस्ट के ज़रिए दी जाती है .

कवरेज इकट्ठा करने के लिए, हर टेस्ट को 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 फ़्लैग दिया जाता है, तो यह टेस्ट के साथ-साथ बाइनरी और लाइब्रेरी के लिए भी जनरेट होता है.

बेसलाइन कवरेज अभी उपलब्ध नहीं है.

हम हर नियम के लिए, कवरेज इकट्ठा करने के लिए फ़ाइलों के दो ग्रुप ट्रैक करते हैं: इंस्ट्रूमेंट की गई फ़ाइलों का सेट और इंस्ट्रूमेंटेशन मेटाडेटा फ़ाइलों का सेट.

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

इंस्ट्रुमेंटेशन मेटाडेटा फ़ाइलों का सेट उन अतिरिक्त फ़ाइलों का सेट है, जिनकी ज़रूरत टेस्ट को बेज़ल से ज़रूरी एलसीओवी फ़ाइलें जनरेट करने के लिए होती हैं. व्यावहारिक तौर पर, इसमें रनटाइम के हिसाब से बनी फ़ाइलें होती हैं. उदाहरण के लिए, कंपाइलेशन के दौरान gcc .gcno फ़ाइलों को छोड़ता है. कवरेज मोड चालू होने पर, इन्हें टेस्ट ऐक्शन के इनपुट के सेट में जोड़ा जाता है.

कवरेज को इकट्ठा किया जा रहा है या नहीं, यह BuildConfiguration में सेव किया जाता है. यह बेहद आसान है, क्योंकि यह इस बिट के आधार पर टेस्ट ऐक्शन और ऐक्शन ग्राफ़ को बदलने का आसान तरीका है. हालांकि, इसका मतलब यह भी है कि अगर इस बिट को फ़्लिप किया जाता है, तो सभी टारगेट का फिर से विश्लेषण करने की ज़रूरत होगी (कुछ भाषाओं, जैसे कि C++ में कवरेज इकट्ठा करने के लिए अलग-अलग कंपाइलर विकल्पों की ज़रूरत होती है. इससे कवरेज इकट्ठा हो सकता है. हालांकि, फिर से विश्लेषण करने की ज़रूरत पड़ती है).

कवरेज की सहायता करने वाली फ़ाइलों पर, लेबल के ज़रिए चुपचाप डिपेंडेंसी का इस्तेमाल किया जाता है, ताकि उन्हें कॉल करने की नीति से बदला जा सके. इससे, उन्हें Bazel के अलग-अलग वर्शन के बीच अलग-अलग किया जा सकता है. आम तौर पर, इन अंतरों को हटा दिया जाता है और हम इनमें से किसी एक को स्टैंडर्ड के तौर पर इस्तेमाल करते हैं.

हम एक "कवरेज रिपोर्ट" भी जनरेट करते हैं, जो Bazel के हर टेस्ट के लिए इकट्ठा की गई कवरेज को मर्ज करती है. इसे CoverageReportActionFactory मैनेज करता है और इसे BuildView.createResult() से कॉल किया जाता है . यह, पहले टेस्ट के :coverage_report_generator एट्रिब्यूट को देखकर, ज़रूरी टूल का ऐक्सेस पाता है.

क्वेरी इंजन

Bazel में एक छोटी भाषा होती है. इसका इस्तेमाल, अलग-अलग ग्राफ़ के बारे में उससे अलग-अलग चीज़ें पूछने के लिए किया जाता है. क्वेरी के ये टाइप दिए गए हैं:

  • bazel query का इस्तेमाल, टारगेट ग्राफ़ की जांच करने के लिए किया जाता है
  • bazel cquery का इस्तेमाल, कॉन्फ़िगर किए गए टारगेट ग्राफ़ की जांच करने के लिए किया जाता है
  • bazel aquery का इस्तेमाल, ऐक्शन ग्राफ़ की जांच करने के लिए किया जाता है

इनमें से हर सुविधा को AbstractBlazeQueryEnvironment की सबक्लास बनाकर लागू किया जाता है. QueryFunction को सबक्लास करके, क्वेरी के अन्य फ़ंक्शन जोड़े जा सकते हैं. क्वेरी के नतीजों को स्ट्रीम करने की अनुमति देने के लिए, उन्हें कुछ डेटा स्ट्रक्चर में इकट्ठा करने के बजाय, QueryFunction को query2.engine.Callback पास किया जाता है. यह ऐसे नतीजों के लिए कॉल करता है जिन्हें वह दिखाना चाहता है.

क्वेरी का नतीजा कई तरीकों से दिखाया जा सकता है: लेबल, लेबल और नियम वाली क्लास, एक्सएमएल, प्रोटोबस वगैरह. इन्हें OutputFormatter की सब-क्लास के तौर पर लागू किया जाता है.

कुछ क्वेरी आउटपुट फ़ॉर्मैट की एक खास ज़रूरत यह है कि बेज़ल को पैकेज लोडिंग से मिलने वाली _all _जानकारी देनी होगी, ताकि लोग आउटपुट में अंतर कर सकें और यह पता लगा सकें कि किसी टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को क्रम से लगाया जाना चाहिए. इसलिए, ऐसे कुछ ही एट्रिब्यूट टाइप हैं जिनमें स्टारलार्क की मुश्किल वैल्यू वाले एट्रिब्यूट नहीं हैं. आम तौर पर, समस्या का हल किसी लेबल का इस्तेमाल करना और जटिल जानकारी को उस लेबल वाले नियम में जोड़ना है. यह समस्या हल करने का एक अच्छा तरीका नहीं है और इस शर्त को हटाना अच्छा होगा.

मॉड्यूल सिस्टम

Bazel में मॉड्यूल जोड़कर, इसे बेहतर बनाया जा सकता है. हर मॉड्यूल को BlazeModule सब-क्लास करना होगा. यह नाम Baze के इतिहास का हिस्सा है, जब इसे Blaze कहा जाता था. साथ ही, किसी निर्देश को एक्ज़ीक्यूट करने के दौरान होने वाले अलग-अलग इवेंट के बारे में जानकारी मिल जाती है.

इनका इस्तेमाल ज़्यादातर, "नॉन-कोर" फ़ंक्शन के अलग-अलग हिस्सों को लागू करने के लिए किया जाता है. इन फ़ंक्शन की ज़रूरत, Bazel के कुछ वर्शन (जैसे, Google में इस्तेमाल होने वाले वर्शन) को होती है:

  • रिमोट एक्ज़ीक्यूशन सिस्टम के इंटरफ़ेस
  • नए निर्देश

BlazeModule में मिलने वाले एक्सटेंशन पॉइंट का सेट कुछ हद तक मुश्किल होता है. इसका इस्तेमाल, डिज़ाइन के अच्छे सिद्धांतों के उदाहरण के तौर पर न करें.

इवेंट बस

BlazeModules, बाकी Bazel के साथ इवेंट बस (EventBus) के ज़रिए मुख्य रूप से कम्यूनिकेट करते हैं: हर बिल्ड के लिए एक नया इंस्टेंस बनाया जाता है. Bazel के अलग-अलग हिस्से, उसमें इवेंट पोस्ट कर सकते हैं. साथ ही, मॉड्यूल उन इवेंट के लिए लिसनर रजिस्टर कर सकते हैं जिनमें उनकी दिलचस्पी है. उदाहरण के लिए, इन चीज़ों को इवेंट के तौर पर दिखाया जाता है:

  • बनाए जाने वाले बिल्ड टारगेट की सूची तय कर दी गई है (TargetParsingCompleteEvent)
  • टॉप-लेवल कॉन्फ़िगरेशन तय कर दिए गए हैं (BuildConfigurationEvent)
  • टारगेट बनाया गया या नहीं (TargetCompleteEvent)
  • कोई टेस्ट चलाया गया (TestAttempt, TestSummary)

इनमें से कुछ इवेंट, बैज के बाहर बिल्ड इवेंट प्रोटोकॉल में दिखाए जाते हैं (ये BuildEvents हैं). इससे न सिर्फ़ BlazeModule, बल्कि Bazel प्रोसेस के बाहर की चीज़ें भी बिल्ड को मॉनिटर कर सकती हैं. इन्हें प्रोटोकॉल मैसेज वाली फ़ाइल के तौर पर ऐक्सेस किया जा सकता है. इसके अलावा, इवेंट स्ट्रीम करने के लिए, Bazel किसी सर्वर (जिसे बिल्ड इवेंट सेवा कहा जाता है) से कनेक्ट हो सकता है.

इसे build.lib.buildeventservice और build.lib.buildeventstream Java पैकेज में लागू किया गया है.

बाहरी डेटा स्टोर करने की जगहें

Bazel को मूल रूप से, मोनोरेपो (एक सोर्स ट्री जिसमें प्रोग्राम बनाने के लिए ज़रूरी सभी चीज़ें होती हैं) में इस्तेमाल करने के लिए डिज़ाइन किया गया था. हालांकि, Bazel को अब ऐसी दुनिया में इस्तेमाल किया जा रहा है जहां यह ज़रूरी नहीं है कि वह मोनोरेपो में ही इस्तेमाल किया जाए. "बाहरी रिपॉज़िटरी" एक ऐसा एब्स्ट्रैक्शन है जिसका इस्तेमाल इन दोनों दुनिया को जोड़ने के लिए किया जाता है: ये ऐसे कोड को दिखाते हैं जो बिल्ड के लिए ज़रूरी है, लेकिन मुख्य सोर्स ट्री में नहीं है.

WORKSPACE फ़ाइल

बाहरी रिपॉज़िटरी का सेट, WORKSPACE फ़ाइल को पार्स करके तय किया जाता है. उदाहरण के लिए, इस तरह का एलान:

    local_repository(name="foo", path="/foo/bar")

@foo नाम की रिपॉज़िटरी में नतीजे उपलब्ध हैं. यह तब मुश्किल हो जाता है, जब कोई व्यक्ति Starlark फ़ाइलों में नए रिपॉज़िटरी नियम तय कर सकता है. इसके बाद, इनका इस्तेमाल नए Starlark कोड को लोड करने के लिए किया जा सकता है. इस कोड का इस्तेमाल, नए रिपॉज़िटरी नियम तय करने के लिए किया जा सकता है.

इस मामले को हैंडल करने के लिए, WorkspaceFileFunction में मौजूद WORKSPACE फ़ाइल को पार्स करने की प्रोसेस को load() स्टेटमेंट के हिसाब से अलग-अलग हिस्सों में बांटा जाता है. हिस्से इंडेक्स को WorkspaceFileKey.getIndex() से दिखाया जाता है और जब तक इंडेक्स X का मतलब Xवां load() स्टेटमेंट तक इसका आकलन करना नहीं होता, तब तक WorkspaceFileFunction की गिनती की जाती है.

डेटा स्टोर करने की जगहें फ़ेच करना

रिपॉज़िटरी का कोड, Bazel के लिए उपलब्ध होने से पहले, उसे फ़ेच करना ज़रूरी है. इससे Bazel, $OUTPUT_BASE/external/<repository name> के नीचे एक डायरेक्ट्री बनाता है.

रिपॉज़िटरी को फ़ेच करने के लिए, यह तरीका अपनाएं:

  1. PackageLookupFunction को पता चलता है कि उसे एक रिपॉज़िटरी की ज़रूरत है और वह SkyKey के तौर पर RepositoryName बनाता है, जो RepositoryLoaderFunction को ट्रिगर करता है
  2. RepositoryLoaderFunction, किसी वजह से अनुरोध को RepositoryDelegatorFunction को फ़ॉरवर्ड करता है (कोड के मुताबिक, Skyframe के रीस्टार्ट होने पर उसे फिर से डाउनलोड करने से बचना चाहिए, लेकिन यह ठोस वजह नहीं है)
  3. RepositoryDelegatorFunction, WORKSPACE फ़ाइल के चंक को तब तक दोहराकर, वह रिपॉज़िटरी नियम ढूंढता है जिसे फ़ेच करने के लिए कहा गया है. ऐसा तब तक किया जाता है, जब तक कि अनुरोध की गई रिपॉज़िटरी नहीं मिल जाती
  4. सही RepositoryFunction पाया गया है जो रिपॉज़िटरी को लागू करता है; यह या तो डेटा स्टोर करने की जगह का Starlark लागू करता है या Java में डेटा स्टोर करने की जगहों के लिए हार्ड कोड किया गया मैप होता है.

कैश मेमोरी की कई लेयर होती हैं, क्योंकि रिपॉज़िटरी फ़ेच करना बहुत महंगा हो सकता है:

  1. डाउनलोड की गई फ़ाइलों के लिए एक कैश मेमोरी है, जिसे उनके चेकसम (RepositoryCache) से जोड़ा जाता है. इसके लिए यह ज़रूरी है कि Workspace फ़ाइल में चेकसम उपलब्ध हो, लेकिन फिर भी यह हरमैटिसिटी के लिए अच्छा है. इसे एक ही वर्कस्टेशन पर इस्तेमाल करने वाले सभी बेज़ेल सर्वर इंस्टेंस के ज़रिए शेयर किया जाता है. भले ही, वे किसी भी वर्कस्पेस या आउटपुट बेस पर चल रहे हों.
  2. $OUTPUT_BASE/external के तहत हर रिपॉज़िटरी के लिए एक "मार्कर फ़ाइल" लिखी जाती है. इसमें उस नियम का चेकसम होता है जिसका इस्तेमाल उसे फ़ेच करने के लिए किया गया था. अगर Bazel के सर्वर को रीस्टार्ट करने के बाद भी चेकसम में कोई बदलाव नहीं होता है, तो उसे फिर से फ़ेच नहीं किया जाता. इसे RepositoryDelegatorFunction.DigestWriter में लागू किया गया है.
  3. --distdir कमांड लाइन विकल्प, एक और कैश मेमोरी तय करता है. इसका इस्तेमाल, डाउनलोड किए जाने वाले आर्टफ़ैक्ट को खोजने के लिए किया जाता है. यह एंटरप्राइज़ सेटिंग में काम आता है, जहां Bazel को इंटरनेट से कोई भी चीज़ फ़ेच नहीं करनी चाहिए. इसे DownloadManager लागू करता है.

किसी रिपॉज़िटरी को डाउनलोड करने के बाद, उसमें मौजूद आर्टफ़ैक्ट को सोर्स आर्टफ़ैक्ट माना जाता है. इससे समस्या होती है, क्योंकि आम तौर पर Bazel, सोर्स आर्टफ़ैक्ट के अप-टू-डेट होने की जांच करने के लिए, उन पर stat() को कॉल करता है. साथ ही, इन आर्टफ़ैक्ट को अमान्य भी कर दिया जाता है, जब वे जिस रिपॉज़िटरी में मौजूद होते हैं उसकी परिभाषा में बदलाव होता है. इसलिए, किसी बाहरी रिपॉज़िटरी में मौजूद आर्टफ़ैक्ट के लिए FileStateValue, उस बाहरी रिपॉज़िटरी पर निर्भर होने चाहिए. इसे ExternalFilesHelper मैनेज करता है.

मैनेज की जा रही डायरेक्ट्री

कभी-कभी, बाहरी रिपॉज़िटरी को वर्कस्पेस रूट में मौजूद फ़ाइलों में बदलाव करना पड़ता है. जैसे, पैकेज मैनेजर, जो डाउनलोड किए गए पैकेज को सोर्स ट्री की सबडायरेक्ट्री में रखता है. यह बात, Bazel के इस अनुमान से मेल नहीं खाती कि सोर्स फ़ाइलों में सिर्फ़ उपयोगकर्ता बदलाव करता है, न कि Bazel. साथ ही, इससे पैकेज को Workspace के रूट में मौजूद हर डायरेक्ट्री का रेफ़रंस देने की अनुमति मिलती है. इस तरह की बाहरी रिपॉज़िटरी को काम करने के लिए, Bazel दो काम करता है:

  1. इससे उपयोगकर्ता को Workspace की सबडायरेक्ट्री तय करने की अनुमति मिलती है. इसके लिए, Basel को ऐक्सेस करने की अनुमति नहीं है. इन्हें .bazelignore नाम की फ़ाइल में लिस्ट किया जाता है और इसकी सुविधा BlacklistedPackagePrefixesFunction में लागू की जाती है.
  2. हम वर्कस्पेस की सबडायरेक्ट्री से एक्सटर्नल रिपॉज़िटरी (डेटा स्टोर करने की जगह) में की जाने वाली मैपिंग को कोड में बदलते हैं. इसे 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 और NativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations और WindowsFileProcesses
  • com.google.devtools.build.lib.platform

कंसोल आउटपुट

कॉन्सल आउटपुट को दिखाना आसान लगता है. हालांकि, कई प्रोसेस (कभी-कभी रिमोट से) को चलाना, बेहतर कैश मेमोरी, बेहतर और रंगीन टर्मिनल आउटपुट, और लंबे समय तक चलने वाले सर्वर को मैनेज करना आसान नहीं है.

क्लाइंट से RPC कॉल आने के तुरंत बाद, stdout और stderr के लिए दो RpcOutputStream इंस्टेंस बनाए जाते हैं, जो क्लाइंट में प्रिंट किए गए डेटा को फ़ॉरवर्ड करते हैं. इसके बाद, इन्हें OutErr (stdout, stderr) पेयर में रैप किया जाता है. कंसोल पर प्रिंट करने के लिए, सभी चीज़ों को इन स्ट्रीम से भेजा जाता है. इसके बाद, इन स्ट्रीम को BlazeCommandDispatcher.execExclusively() को सौंप दिया जाता है.

आउटपुट डिफ़ॉल्ट रूप से, ANSI एस्केप सीक्वेंस के साथ प्रिंट होता है. अगर इन चीज़ों की ज़रूरत न हो (--color=no), तो AnsiStrippingOutputStream इन्हें हटा देता है. इसके अलावा, System.out और System.err को इन आउटपुट स्ट्रीम पर रीडायरेक्ट किया जाता है. ऐसा इसलिए किया जाता है, ताकि डीबग करने से जुड़ी जानकारी को System.err.println() का इस्तेमाल करके प्रिंट किया जा सके और वह क्लाइंट के टर्मिनल आउटपुट में दिखे. यह आउटपुट, सर्वर के आउटपुट से अलग होता है. ध्यान रखा जाता है कि अगर कोई प्रोसेस bazel query --output=proto जैसे बाइनरी आउटपुट देती है, तो स्टैंडर्ड आउटपुट में कोई बदलाव न किया जाए.

EventHandler इंटरफ़ेस की मदद से, छोटे मैसेज (गड़बड़ियां, चेतावनियां वगैरह) दिखाए जाते हैं. ध्यान दें कि ये EventBus में पोस्ट किए जाने वाले डेटा से अलग होते हैं. हर Event में एक EventKind (गड़बड़ी, चेतावनी, जानकारी वगैरह) होती है. साथ ही, उनमें एक Location (सोर्स कोड में वह जगह जिसकी वजह से यह इवेंट हुआ) हो सकती है.

EventHandler के कुछ लागू होने के तरीके, उन्हें मिले इवेंट सेव करते हैं. इसका इस्तेमाल, कैश मेमोरी में प्रोसेस करने के अलग-अलग तरीकों की वजह से, यूज़र इंटरफ़ेस (यूआई) पर जानकारी को फिर से चलाने के लिए किया जाता है. उदाहरण के लिए, कैश मेमोरी में कॉन्फ़िगर किए गए टारगेट से मिलने वाली चेतावनियां.

कुछ EventHandler, इवेंट पोस्ट करने की अनुमति भी देते हैं. ये इवेंट, आखिर में इवेंट बस में दिखते हैं. सामान्य Event वहां _नहीं_ दिखते. ये ExtendedEventHandler के लागू होने के तरीके हैं. इनका मुख्य इस्तेमाल, कैश मेमोरी में सेव किए गए EventBus इवेंट को फिर से चलाने के लिए किया जाता है. ये EventBus इवेंट, Postable को लागू करते हैं. हालांकि, यह ज़रूरी नहीं है कि EventBus पर पोस्ट की गई हर चीज़ इस इंटरफ़ेस पर लागू हो. सिर्फ़ उन इवेंट को ही लागू किया जाता है जिन्हें ExtendedEventHandler की मदद से कैश मेमोरी में सेव किया गया हो (यह अच्छा होगा और ज़्यादातर चीज़ें काम करेंगी. हालांकि, इसे लागू नहीं किया जाता)

टर्मिनल आउटपुट का उत्सर्जन ज़्यादातर UiEventHandler से होता है. इसकी मदद से, बेज़ेल की सभी फ़ैंसी आउटपुट फ़ॉर्मैटिंग और उसकी प्रोग्रेस की जानकारी मिलती है. इसमें दो इनपुट होते हैं:

  • इवेंट बस
  • Reporter की मदद से इसमें भेजी गई इवेंट स्ट्रीम

क्लाइंट की आरपीसी स्ट्रीम से, कमांड को लागू करने वाली मशीन (उदाहरण के लिए, बाज़ल का बाकी हिस्सा) का सीधा कनेक्शन सिर्फ़ Reporter.getOutErr() के ज़रिए होता है. इससे इन स्ट्रीम को सीधे ऐक्सेस किया जा सकता है. इसका इस्तेमाल सिर्फ़ तब किया जाता है, जब किसी कमांड को bazel query जैसे बहुत ज़्यादा बाइनरी डेटा को डंप करना हो.

Bazel की प्रोफ़ाइल बनाना

Basel एक तेज़ है. Bazel भी धीमा है, क्योंकि बिल्ड की संख्या तब तक बढ़ती रहती है, जब तक कि वह ज़्यादा से ज़्यादा नहीं हो जाती. इस वजह से, Bazel में एक प्रोफ़ाइलर शामिल होता है. इसका इस्तेमाल, बिल्ड और Bazel की प्रोफ़ाइल बनाने के लिए किया जा सकता है. इसे Profiler नाम की क्लास में लागू किया गया है. यह सुविधा डिफ़ॉल्ट रूप से चालू रहती है. हालांकि, यह सिर्फ़ छोटा डेटा रिकॉर्ड करती है, ताकि इसका ओवरहेड संभाला जा सके. कमांड लाइन --record_full_profiler_data की मदद से, यह सब कुछ रिकॉर्ड किया जाता है.

इससे एक ऐसी प्रोफ़ाइल बनती है जो Chrome के प्रोफ़ाइलर फ़ॉर्मैट में होती है. यह Chrome में सबसे अच्छी तरह दिखती है. इसका डेटा मॉडल, टास्क स्टैक का होता है: इसमें टास्क शुरू और खत्म किए जा सकते हैं. साथ ही, ये एक-दूसरे के अंदर व्यवस्थित तरीके से नेस्ट होने चाहिए. हर Java थ्रेड को अपना टास्क स्टैक मिलता है. TODO: यह कार्रवाइयों और लगातार पास होने के तरीके के साथ कैसे काम करता है?

प्रोफ़ाइलर, BlazeRuntime.initProfiler() और BlazeRuntime.afterCommand() में बंद हो गया है और चालू हो गया है. साथ ही, यह ज़्यादा से ज़्यादा समय तक लाइव रहने की कोशिश करता है, ताकि हम हर चीज़ की प्रोफ़ाइल बना सकें. प्रोफ़ाइल में कुछ जोड़ने के लिए, Profiler.instance().profile() पर कॉल करें. यह एक Closeable दिखाता है, जिसका बंद होना टास्क का खत्म होना दिखाता है. इसका इस्तेमाल, try-with-resources के स्टेटमेंट के साथ करना सबसे अच्छा होता है.

हम MemoryProfiler में अल्पमेंटरी मेमोरी प्रोफ़ाइलिंग भी करते हैं. यह हमेशा चालू रहता है और ज़्यादातर हीप साइज़ और जीसी ऐक्शन को रिकॉर्ड करता है.

Bazel की जांच करना

Bazel में दो तरह के मुख्य टेस्ट होते हैं: एक ऐसा टेस्ट जो Bazel को "ब्लैक बॉक्स" के तौर पर देखता है और दूसरा ऐसा टेस्ट जो सिर्फ़ विश्लेषण का फ़ेज़ चलाता है. हम पहले को "इंटिग्रेशन टेस्ट" और दूसरे को "यूनिट टेस्ट" कहते हैं. हालांकि, ये इंटिग्रेशन टेस्ट की तरह ही होते हैं, लेकिन इनमें इंटिग्रेशन कम होता है. हमारे पास कुछ यूनिट टेस्ट भी हैं, जो ज़रूरी होने पर किए जाते हैं.

इंटिग्रेशन टेस्ट दो तरह के होते हैं:

  1. एक ऐसा प्रोग्राम जिसे src/test/shell के तहत, बहुत अच्छे बैश टेस्ट फ़्रेमवर्क का इस्तेमाल करके लागू किया गया हो
  2. Java में लागू की गई सुविधाएं. इन्हें BuildIntegrationTestCase की सब-क्लास के तौर पर लागू किया जाता है

BuildIntegrationTestCase, इंटिग्रेशन टेस्टिंग के लिए सबसे ज़्यादा इस्तेमाल किया जाने वाला फ़्रेमवर्क है, क्योंकि यह ज़्यादातर टेस्टिंग स्थितियों के लिए बेहतर तरीके से तैयार है. यह एक Java फ़्रेमवर्क है. इसलिए, इसमें डीबग करने की सुविधा मिलती है. साथ ही, इसे कई सामान्य डेवलपमेंट टूल के साथ आसानी से इंटिग्रेट किया जा सकता है. बेल रिपॉज़िटरी में BuildIntegrationTestCase क्लास के कई उदाहरण दिए गए हैं.

विश्लेषण टेस्ट, BuildViewTestCase के सबक्लास के तौर पर लागू किए जाते हैं. यह एक स्क्रैच फ़ाइल सिस्टम है. इसका इस्तेमाल BUILD फ़ाइलों को लिखने के लिए किया जा सकता है. इसके बाद, हेल्पर के अलग-अलग तरीके, कॉन्फ़िगर किए गए टारगेट का अनुरोध कर सकते हैं, कॉन्फ़िगरेशन में बदलाव कर सकते हैं, और विश्लेषण के नतीजे के बारे में अलग-अलग चीज़ों का दावा कर सकते हैं.