द बैजल कोड बेस

किसी समस्या की शिकायत करें सोर्स देखें Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

परिचय

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

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

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

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

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

Bazel का ज़्यादातर हिस्सा, सर्वर प्रोसेस में मौजूद होता है. यह प्रोसेस, बिल्ड के बीच RAM में रहती है. इससे 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 को किसी दूसरे कमांड के साथ चलाने में कुछ परेशानी होती है. मुख्य ब्लॉकर, 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() मैनेज करता है.

डायरेक्ट्री का लेआउट

बिल्ड के दौरान Baज़ल, डायरेक्ट्री का कुछ मुश्किल सेट बना देता है. आउटपुट डायरेक्ट्री लेआउट में पूरी जानकारी उपलब्ध है.

"वर्कस्पेस", वह सोर्स ट्री है जिसमें 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 स्टार्टअप विकल्प का इस्तेमाल करके बदला जा सकता है. यह विकल्प, कई कामों के लिए मददगार होता है. जैसे, किसी भी समय किसी भी वर्कस्पेस में सिर्फ़ एक Bazel इंस्टेंस चल सकता है.

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

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

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

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

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

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

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

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

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

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

"रिपॉज़िटरी" एक सोर्स ट्री होता है, जिस पर डेवलपर काम करता है. आम तौर पर, यह एक प्रोजेक्ट को दिखाता है. Bazel का पूर्वज, Blaze, एक मोनोरेपो पर काम करता था. वहीं दूसरी ओर, Basel का सोर्स कोड एक से ज़्यादा डेटा स्टोर करने की जगहों में इस्तेमाल होता है. जिस रिपॉज़िटरी से 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 के हिसाब से, हम इन्हें आर्टफ़ैक्ट कहते हैं. इनके बारे में कहीं और बताया गया है. बिल्ड के दौरान बनाई गई सभी फ़ाइलें टारगेट नहीं होतीं. आम तौर पर, 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 में कॉन्फ़िगर किए गए टारगेट जैसे सभी चीज़ों में बदलाव नहीं किया जा सकता.

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

Skyframe, com.google.devtools.build.skyframe पैकेज में मौजूद है. इसी तरह के नाम वाले पैकेज com.google.devtools.build.lib.skyframe में, Skyframe के ऊपर बेज़ल लागू किया गया है. 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 स्टैटिक कैश रखकर, रीस्टार्ट के बीच स्टेटस सेव करना.

बुनियादी तौर पर, हमें इस तरह के वैकल्पिक हलों की ज़रूरत है, क्योंकि हमारे पास नियमित रूप से, इन-फ़्लाइट स्काईफ़्रेम नोड की संख्या लाखों में हैं. Java में, लाइटवेट थ्रेड की सुविधा नहीं है.

Starlark

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

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

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

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

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

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

लोडिंग/विश्लेषण का चरण

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

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

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

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

  1. कॉन्फ़िगरेशन. ("वह नियम कैसे बनाया जाए; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म के साथ-साथ कमांड-लाइन के विकल्पों जैसी चीज़ें भी, जिन्हें उपयोगकर्ता C++ कंपाइलर में भेजना चाहता है)
  2. डायरेक्ट डिपेंडेंसी. ट्रांज़िशन की जानकारी देने वाली उनकी कंपनियां, विश्लेषण किए जा रहे नियम के लिए उपलब्ध हैं. उन्हें इस तरह कहा जाता है, क्योंकि वे कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में जानकारी का "रोल-अप" देते हैं, जैसे कि क्लासपाथ पर मौजूद सभी .Jर फ़ाइलें या ऐसी सभी .o फ़ाइलें जिन्हें C++ बाइनरी से लिंक करना ज़रूरी होता है)
  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 से थोड़ा ज़्यादा मुश्किल है. ऐसा दो वजहों से है:

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

रनफ़ाइलों को 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 में कैप्चर किया जाता है.

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

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

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

टूलचेन का कॉन्सेप्ट इस बात पर आधारित है कि कौनसे प्लैटफ़ॉर्म पर बिल्ड चल रहा है और कौनसे प्लैटफ़ॉर्म टारगेट किए जा रहे हैं. इसके आधार पर, आपको अलग-अलग कंपाइलर का इस्तेमाल करना पड़ सकता है. उदाहरण के लिए, कोई खास C++ टूलचेन किसी खास ओएस पर चल सकता है और कुछ अन्य ओएस को टारगेट कर सकता है. बेज़ल को 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_group() और environment()

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

सभी बिल्ड रूल से यह बताया जा सकता है कि उन्हें किस "एनवायरमेंट" के लिए बनाया जा सकता है और जहां "एनवायरमेंट", 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) तय करते हैं: प्रीऑर्डर, पोस्टऑर्डर, टॉपोलॉजिकल (कोई नोड हमेशा अपने पैरंट के बाद आता है) और "इस पर ध्यान न दें, लेकिन हर बार यह एक जैसा होना चाहिए".

स्टारलार्क में इसी डेटा स्ट्रक्चर को 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 को कॉल करने के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से स्काईफ़्रेम डिपेंडेंसी एज पर मैप करने की जानकारी ActionExecutionFunction.getInputDeps() और Artifact.key() में दी गई है. साथ ही, स्काईफ़्रेम किनारों की संख्या को कम रखने के लिए, इसमें कुछ ऑप्टिमाइज़ेशन शामिल किए गए हैं:

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

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

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

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

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

एक्ज़ीक्यूशन का चरण

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

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

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

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

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

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

  • ActionExecutionFunction.stateMap में ActionExecutionFunction के Skyframe को फिर से शुरू करने की लागत कम करने के लिए डेटा शामिल है
  • लोकल ऐक्शन कैश मेमोरी में, फ़ाइल सिस्टम की स्थिति का डेटा होता है
  • रिमोट तौर पर प्रोसेस करने वाले सिस्टम में आम तौर पर अपना कैश मेमोरी भी होता है

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

यह कैश, 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 को यह बताया जा सके कि उसके सभी इनपुट ज़रूरी नहीं थे. अगर इस्तेमाल किए गए इनपुट को इस्तेमाल नहीं किए गए के तौर पर रिपोर्ट किया जाता है, तो इससे गलत इंक्रीमेंटल बिल्ड बन सकते हैं.

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

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

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

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

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

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

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

स्थानीय संसाधन प्रबंधन की ज़्यादा जानकारी यहां उपलब्ध है.

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

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

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

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

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

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

फ़िलहाल, कॉन्फ़िगरेशन के लिए पाथ सेगमेंट <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, टेस्ट को अनुरोध किए गए तरीके से चलाता है.

टेस्ट, एक खास प्रोटोकॉल के हिसाब से चलाए जाते हैं. यह प्रोटोकॉल, एनवायरमेंट वैरिएबल का इस्तेमाल करके, टेस्ट को यह बताता है कि उनसे क्या उम्मीद की जा रही है. Bazel के लिए टेस्ट से क्या उम्मीद की जा सकती है और टेस्ट के लिए Bazel से क्या उम्मीद की जा सकती है, इस बारे में ज़्यादा जानकारी यहां दी गई है. सबसे आसान तरीके से, 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 पर है . यह जांचों के अलावा, बाइनरी और लाइब्रेरी के लिए भी जनरेट की जाती है. ऐसा तब होता है, जब आपने Basel को --nobuild_tests_only फ़्लैग पास किया हो.

फ़िलहाल, बेसलाइन कवरेज काम नहीं कर रही है.

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

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

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

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

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

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

क्वेरी इंजन

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

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

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

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

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

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

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

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

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

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

इवेंट बस

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

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

इनमें से कुछ इवेंट, बिल्ड इवेंट प्रोटोकॉल में, Bazel के बाहर दिखाए जाते हैं. ये BuildEvent होते हैं. इससे BlazeModule के साथ-साथ, बेज़ल प्रोसेस से बाहर की चीज़ों को भी बिल्ड के बारे में पता चल पाता है. इन्हें प्रोटोकॉल मैसेज वाली फ़ाइल के तौर पर ऐक्सेस किया जा सकता है. इसके अलावा, इवेंट स्ट्रीम करने के लिए, 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 तक WorkspaceFileFunction का हिसाब लगाने का मतलब है कि Xवें load() स्टेटमेंट तक इसका आकलन किया जाता है.

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

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

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

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

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

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

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

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

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

  1. इससे उपयोगकर्ता को Workspace की सबडायरेक्ट्री तय करने की अनुमति मिलती है. इसके लिए, Basel को ऐक्सेस करने की अनुमति नहीं है. इन्हें .bazelignore नाम की फ़ाइल में लिस्ट किया जाता है और इसकी सुविधा BlacklistedPackagePrefixesFunction में लागू की जाती है.
  2. हम Workspace की सबडायरेक्ट्री से उस बाहरी रिपॉज़िटरी तक की मैपिंग को 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 खुद नहीं कर सकता या जब हमने उसे लागू किया था, तो वह खुद ऐसा नहीं कर सकता. यह मुख्य रूप से फ़ाइल सिस्टम, प्रोसेस कंट्रोल, और कई अन्य लो-लेवल चीज़ों के साथ इंटरैक्शन तक सीमित है.

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() को दे दी जाती हैं.

आउटपुट, डिफ़ॉल्ट रूप से एएनएसआई एस्केप सीक्वेंस के साथ प्रिंट होता है. जब ये ज़रूरी न हों (--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 एक तेज़ है. Basel का आकार धीमा होता है, क्योंकि बिल्ड के बढ़ने की प्रक्रिया में तब तक कोई रुकावट नहीं होती, जब तक कि वह पैदा न हो जाए. इस वजह से, 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 को "ब्लैक बॉक्स" के तौर पर देखता है और दूसरा ऐसा टेस्ट जो सिर्फ़ विश्लेषण का फ़ेज़ चलाता है. हम पहले को "इंटिग्रेशन टेस्ट" और दूसरे को "यूनिट टेस्ट" कहते हैं. हालांकि, ये इंटिग्रेशन टेस्ट की तरह ही होते हैं, लेकिन इनमें इंटिग्रेशन कम होता है. हमारे पास कुछ यूनिट टेस्ट भी हैं, जो ज़रूरी होने पर किए जाते हैं.

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

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

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

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