द बैजल कोड बेस

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

परिचय

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

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

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

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

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

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

यही वजह है कि बेज़ल कमांड लाइन में दो तरह के विकल्प होते हैं: स्टार्टअप और कमांड. इस तरह की कमांड लाइन में:

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

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

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

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

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

सही सर्वर प्रोसेस तैयार होने के बाद, जिस निर्देश को चलाना है उसे gRPC इंटरफ़ेस के ज़रिए भेजा जाता है. इसके बाद, Bazel का आउटपुट टर्मिनल पर वापस भेजा जाता है. एक ही समय पर सिर्फ़ एक निर्देश चल सकता है. इसे लागू करने के लिए, C++ और Java में अलग-अलग हिस्सों के साथ, लॉक करने के बेहतर तरीके का इस्तेमाल किया जाता है. एक साथ कई निर्देश चलाने की कुछ इंफ़्रास्ट्रक्चर है, क्योंकि किसी दूसरे निर्देश के साथ bazel version न चला पाना कुछ हद तक शर्मिंदा करने वाला है. मुख्य समस्या, BlazeModules के लाइफ़ साइकल और BlazeRuntime में कुछ स्टेटस है.

किसी निर्देश के आखिर में, Bazel सर्वर वह बाहर निकलने का कोड भेजता है जिसे क्लाइंट को दिखाना चाहिए. bazel run को लागू करने की एक दिलचस्प बात यह है कि इस निर्देश का काम, अभी-अभी बनाए गए बेज़ल को चलाना है, लेकिन यह सर्वर प्रोसेस से ऐसा नहीं कर सकता, क्योंकि उसमें कोई टर्मिनल नहीं है. इसलिए, यह क्लाइंट को बताता है कि उसे कौनसी बाइनरी को ujexec() और किन आर्ग्युमेंट के साथ इस्तेमाल करना चाहिए.

जब कोई Ctrl-C दबाता है, तो क्लाइंट इसे gRPC कनेक्शन पर रद्द करें कॉल में बदल देता है, जो जितनी जल्दी हो सके कमांड को खत्म करने की कोशिश करता है. तीसरे Ctrl-C के बाद, क्लाइंट सर्वर को SIGKILL भेजता है.

क्लाइंट का सोर्स कोड src/main/cpp में है और सर्वर से बातचीत करने के लिए इस्तेमाल किया जाने वाला प्रोटोकॉल src/main/protobuf/command_server.proto में है .

सर्वर का मुख्य एंट्री पॉइंट BlazeRuntime.main() है और क्लाइंट के gRPC कॉल को GrpcServerImpl.run() मैनेज करता है.

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

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

"workspace", वह सोर्स ट्री है जिसमें Basel का इस्तेमाल किया जा रहा है. आम तौर पर, यह जानकारी ऐसी होती है जिसे सोर्स कंट्रोल से चेक आउट किया जाता है.

Bazel अपना सारा डेटा "आउटपुट उपयोगकर्ता रूट" में डालता है. आम तौर पर, यह $HOME/.cache/bazel/_bazel_${USER} होता है, लेकिन --output_user_root स्टार्टअप विकल्प का इस्तेमाल करके, इसे बदला जा सकता है.

"इंस्टॉल बेस" वह जगह है जहां Basel को एक्सट्रैक्ट किया जाता है. यह अपने-आप होता है और हर Bazel वर्शन को, इंस्टॉल बेस में उसके चेकसम के आधार पर एक सबडायरेक्ट्री मिलती है. यह डिफ़ॉल्ट रूप से $OUTPUT_USER_ROOT/install पर होती है और इसे --install_base कमांड लाइन विकल्प का इस्तेमाल करके बदला जा सकता है.

"आउटपुट बेस" वह जगह होती है जहां किसी खास वर्कस्पेस से जुड़ा Bazel इंस्टेंस, आउटपुट लिखता है. हर आउटपुट बेस में एक समय पर, ज़्यादा से ज़्यादा एक Basel सर्वर इंस्टेंस होता है. आम तौर पर, यह $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. एक इवेंट बस बनाई जाती है. इवेंट बस, उन इवेंट के लिए एक स्ट्रीम है जो बिल्ड के दौरान होते हैं. इनमें से कुछ को Build इवेंट प्रोटोकॉल के तहत बेज़ल के बाहर एक्सपोर्ट किया जाता है, ताकि दुनिया को बताया जा सके कि बिल्ड कैसा होता है.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Baज़र, सॉफ़्टवेयर बनाने का कारोबार करता है. ऐसा सोर्स कोड को पढ़कर और उसे समझकर किया जाता है. 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 नाम की फ़ाइल में दी जाती है. अगर दोनों का इस्तेमाल किया जाता है, तो Baze, BUILD.bazel को पसंद करता है. BUILD फ़ाइलों को अब भी स्वीकार किए जाने की वजह यह है कि Baze के पूर्वज, Blaze ने इस फ़ाइल नाम का इस्तेमाल किया था. हालांकि, यह आम तौर पर इस्तेमाल किया जाने वाला पाथ सेगमेंट है. खास तौर पर, Windows पर, जहां फ़ाइल के नाम केस-इन्सेंसिव होते हैं.

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

BUILD फ़ाइल का आकलन करने की प्रोसेस को "पैकेज लोड करना" कहा जाता है. इसे PackageFactory क्लास में लागू किया गया है. यह Starlark इंटरप्रेटर को कॉल करके काम करता है. साथ ही, इसके लिए उपलब्ध नियम क्लास के सेट के बारे में जानकारी ज़रूरी है. पैकेज लोडिंग का नतीजा एक Package ऑब्जेक्ट है. यह ज़्यादातर किसी स्ट्रिंग (टारगेट का नाम) से टारगेट पर मैप होता है.

पैकेज लोड करने के दौरान, ग्लोबिंग की वजह से समस्याएं आती हैं: Bazel को हर सोर्स फ़ाइल को साफ़ तौर पर सूची में शामिल करने की ज़रूरत नहीं होती. इसके बजाय, यह ग्लोब (जैसे, glob(["**/*.java"])) चला सकता है. शेल के विपरीत, यह बार-बार होने वाली ग्लोबिंग के साथ काम करता है, जो सब-डायरेक्ट्री में जाती है (लेकिन सब-पैकेज में नहीं). इसके लिए, फ़ाइल सिस्टम का ऐक्सेस ज़रूरी है. यह प्रोसेस धीमी हो सकती है. इसलिए, हम इसे एक साथ और बेहतर तरीके से चलाने के लिए, सभी तरह की तरकीबें अपनाते हैं.

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

  • LegacyGlobber, एक तेज़ और खुशी से Skyframe के बारे में अनजान globber
  • SkyframeHybridGlobber एक ऐसा वर्शन है जो SkyFrame का इस्तेमाल करता है और “Skyframe को रीस्टार्ट होने” से बचाने के लिए, लेगसी ग्लोबर पर वापस जाता है (इस बारे में नीचे बताया गया है)

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 के साथ रजिस्टर किए जाने की वजह से, बैजल की ओर से "आम तौर पर" जाना जाता है.

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

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

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

Skyframe

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

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

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

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

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

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

स्टारलार्क का इस्तेमाल कई तरह के कॉन्टेक्स्ट में किया जाता है. जैसे:

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

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

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

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

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

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

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

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

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

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

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

Java नियमों के लिए एपीआई RuleContext है, जो Starlark नियमों के ctx आर्ग्युमेंट के बराबर है. इसका एपीआई ज़्यादा बेहतर है, लेकिन साथ ही, Bad ThingsTM को करना ज़्यादा आसान है. उदाहरण के लिए, ऐसा कोड लिखना जिसके समय या स्पेस की जटिलता क्वाड्रेटिक (या खराब) हो. इसका इस्तेमाल 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 प्रोवाइडर का इस्तेमाल करके ऐक्सेस किया जा सकता है.

रनफ़ाइल

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

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

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

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

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

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

पक्ष

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

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

कंस्ट्रेंट

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

  • नियम-विशिष्ट सीमाएं
  • 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() नियमों में डिफ़ॉल्ट स्पेसिफ़िकेशन के ज़रिए. हर एनवायरमेंट, थीम के आधार पर मिलते-जुलते ऐप्लिकेशन के ग्रुप से जुड़ा होता है. जैसे, "सीपीयू आर्किटेक्चर", "जेडीके वर्शन" या "मोबाइल ऑपरेटिंग सिस्टम". किसी एनवायरमेंट ग्रुप की परिभाषा में यह शामिल होता है कि अगर restricted_to= / environment() एट्रिब्यूट ने इन एनवायरमेंट को किसी और तरीके से तय नहीं किया है, तो इनमें से कौनसे एनवायरमेंट "डिफ़ॉल्ट" के साथ काम करने चाहिए. बिना एट्रिब्यूट वाले नियम के सभी डिफ़ॉल्ट एट्रिब्यूट इनहेरिट किए जाते हैं.
  5. नियम क्लास के डिफ़ॉल्ट तौर पर. ऐसा करने से, किसी नियम की क्लास के सभी इंस्टेंस के लिए, ग्लोबल डिफ़ॉल्ट सेटिंग बदल जाती है. उदाहरण के लिए, इसका इस्तेमाल सभी *_test नियमों को टेस्ट करने लायक बनाने के लिए किया जा सकता है. हर इंस्टेंस में साफ़ तौर पर इस सुविधा की जानकारी देने की ज़रूरत नहीं होती.

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

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

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

प्लैटफ़ॉर्म से जुड़ी पाबंदियां

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

किसको दिखे

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

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

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

  • 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_TRACK_SOURCE_DIRECTORIES=1 जेवीएम प्रॉपर्टी से चालू होती है)

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

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

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

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

कुछ और खास मामले भी हैं, जैसे कि ऐसी फ़ाइल लिखना जिसका कॉन्टेंट, 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 असल में बिल्ड ऐक्शन चलाना शुरू करता है. जैसे, आउटपुट देने वाले कमांड.

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

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

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

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

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

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

लोकल ऐक्शन कैश

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

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

अपने नाम के उलट, यह एक मिले-जुले आर्टफ़ैक्ट के पाथ से उस कार्रवाई तक का मैप है जिससे इसे निकाला गया. कार्रवाई के बारे में इस तरह बताया गया है:

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

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

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

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

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

इन्हें कार्रवाई पर दिए गए तरीकों का इस्तेमाल करके लागू किया जाता है:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

जांच

Basel के पास, दौड़ने की जांचों को बेहतर तरीके से इस्तेमाल करने की सुविधा है. यह इनके साथ काम करता है:

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

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

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

यह तय करना कि कौनसे टेस्ट किए जाएं

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

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

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

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

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

टेस्ट चलाना

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

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

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

  • test.xml, JUnit-शैली की एक एक्सएमएल फ़ाइल, जिसमें टेस्ट शार्ड में अलग-अलग टेस्ट केस की जानकारी दी गई है
  • 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 की मदद से कंसोल में भेजा जाता है.

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

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

कवरेज इकट्ठा करने के लिए, हर टेस्ट को collect_coverage.sh नाम की स्क्रिप्ट में रैप किया जाता है.

यह स्क्रिप्ट, कवरेज कलेक्शन को चालू करने और यह तय करने के लिए टेस्ट एनवायरमेंट को सेट अप करती है कि कवरेज रनटाइम में कवरेज फ़ाइलें कहां लिखी गई हों. इसके बाद, यह टेस्ट करता है. एक टेस्ट में कई सबप्रोसेस अपने-आप चल सकती हैं. इसमें कई अलग-अलग प्रोग्रामिंग भाषाओं (अलग-अलग कवरेज कलेक्शन रनटाइम के साथ) में लिखे गए पार्ट शामिल हो सकते हैं. रैपर स्क्रिप्ट, ज़रूरत के हिसाब से बनने वाली फ़ाइलों को LCOV फ़ॉर्मैट में बदलने और उन्हें एक फ़ाइल में मर्ज करने के लिए ज़िम्मेदार है.

collect_coverage.sh को टेस्ट की रणनीतियों के ज़रिए इंटरपोज़ किया जाता है. इसके लिए, collect_coverage.sh को टेस्ट के इनपुट पर होना ज़रूरी है. यह काम, एलिमेंट के लिए डिफ़ॉल्ट तौर पर लागू होने वाले एट्रिब्यूट :coverage_support की मदद से किया जाता है. इस एट्रिब्यूट की वैल्यू, कॉन्फ़िगरेशन फ़्लैग --coverage_support की वैल्यू पर सेट होती है (TestConfiguration.TestOptions.coverageSupport देखें)

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

दूसरा अहम सिद्धांत है बेसलाइन कवरेज. यह किसी लाइब्रेरी, बिनेरी या टेस्ट की कवरेज है. इससे पता चलता है कि उसमें कोई कोड नहीं चलाया गया था. यह समस्या हल करता है कि अगर आपको किसी बाइनरी के लिए टेस्ट कवरेज का हिसाब लगाना है, तो सभी टेस्ट की कवरेज को मर्ज करना काफ़ी नहीं है. ऐसा इसलिए, क्योंकि बाइनरी में ऐसा कोड हो सकता है जो किसी भी टेस्ट से लिंक न हो. इसलिए, हम हर बाइनरी के लिए एक कवरेज फ़ाइल जनरेट करते हैं. इसमें सिर्फ़ वे फ़ाइलें शामिल होती हैं जिनके लिए हम कवरेज इकट्ठा करते हैं. इनमें ऐसी कोई लाइन नहीं होती जिसकी कवरेज ली गई हो. टारगेट के लिए बेसलाइन कवरेज फ़ाइल, bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat पर है . यह जांचों के अलावा, बाइनरी और लाइब्रेरी के लिए भी जनरेट की जाती है. ऐसा तब होता है, जब आपने Basel को --nobuild_tests_only फ़्लैग पास किया हो.

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

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

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

इंस्ट्रुमेंटेशन मेटाडेटा फ़ाइलों का सेट उन अतिरिक्त फ़ाइलों का सेट है, जिनकी ज़रूरत टेस्ट को बेज़ल से ज़रूरी एलसीओवी फ़ाइलें जनरेट करने के लिए होती हैं. व्यावहारिक तौर पर, इसमें रनटाइम के हिसाब से बनी फ़ाइलें होती हैं. उदाहरण के लिए, कंपाइलेशन के दौरान 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 _जानकारी देनी होगी, ताकि लोग आउटपुट में अंतर कर सकें और यह पता लगा सकें कि किसी टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को क्रम से लगाया जाना चाहिए. इसलिए, ऐसे कुछ ही एट्रिब्यूट टाइप हैं जिनमें स्टारलार्क की मुश्किल वैल्यू वाले एट्रिब्यूट नहीं हैं. आम तौर पर, समस्या का हल किसी लेबल का इस्तेमाल करना और जटिल जानकारी को उस लेबल वाले नियम में जोड़ना है. यह समस्या हल करने का एक अच्छा तरीका नहीं है और इस शर्त को हटाना अच्छा होगा.

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

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

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

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

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

इवेंट बस

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

  • बनाए जाने वाले बिल्ड टारगेट की सूची तय कर ली गई है (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 तक WorkspaceFileFunction का हिसाब लगाने का मतलब है कि Xवें load() स्टेटमेंट तक इसका आकलन किया जाता है.

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

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

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

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

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

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

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

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

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

  1. इससे उपयोगकर्ता को Workspace की उन सबडायरेक्ट्री के बारे में बताने की अनुमति मिलती है जिनमें Bazel को ऐक्सेस करने की अनुमति नहीं है. इनकी जानकारी .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 बिट

Basel का सर्वर_ अक्सर Java में _ Write होता है. हालांकि, उन हिस्सों में अपवाद है जिन्हें Java खुद नहीं कर सकता या जब हमने उसे लागू किया था, तो वह खुद ऐसा नहीं कर सकता. इसका मकसद ज़्यादातर, फ़ाइल सिस्टम, प्रोसेस कंट्रोल, और अन्य निचले लेवल की चीज़ों के साथ इंटरैक्शन करना होता है.

C++ कोड, src/main/native में होता है और नेटिव तरीकों वाली Java क्लास इस तरह से हैं:

  • NativePosixFiles और NativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations और WindowsFileProcesses
  • com.google.devtools.build.lib.platform

कंसोल आउटपुट

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

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

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

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

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

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

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

  • इवेंट बस
  • इवेंट स्ट्रीम, रिपोर्टर के ज़रिए इसमें शामिल हुई

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

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

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

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

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

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

बेज़ल टेस्टिंग

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

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

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

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

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