Bazel कोडबेस

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

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

परिचय

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

"आउटपुट बेस" वह जगह है जहां किसी खास वर्कस्पेस से अटैच किए गए Baze इंस्टेंस, उस ईमेल पते के बारे में जानकारी देते हैं. हर आउटपुट बेस में एक समय पर, ज़्यादा से ज़्यादा एक Basel सर्वर इंस्टेंस होता है. आम तौर पर, यह $OUTPUT_USER_ROOT/<checksum of the path to the workspace> पर होता है. इसे --output_base स्टार्टअप विकल्प का इस्तेमाल करके बदला जा सकता है. यह विकल्प दूसरी चीज़ों के अलावा, इस सीमा को पार करने में मददगार होता है कि किसी भी समय किसी भी फ़ाइल फ़ोल्डर में सिर्फ़ एक Basel इंस्टेंस चल सकता है.

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

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

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

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

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

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

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

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

  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 का हिस्सा). ऐसा नहीं है. हालांकि, उनमें बदलाव करना बेज़ेल को ऐसे आसान तरीकों से तोड़ने का अच्छा तरीका है जिन्हें डीबग करना मुश्किल है. माफ़ करें, उन्हें असल में अपरिवर्तनीय बनाना एक बड़ी चुनौती है. (किसी FragmentOptions को बनाने के तुरंत बाद उसमें बदलाव करना ठीक है. ऐसा तब करें, जब किसी और को उसका रेफ़रंस रखने का मौका न मिले और equals() या hashCode() को उस पर कॉल न किया गया हो.)

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

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

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

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

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

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

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

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

Skyframe

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

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

Skyframe ग्राफ़ को देखने का सबसे आसान तरीका है, bazel dump --skyframe=deps को चलाना. इससे ग्राफ़, हर लाइन में एक 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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, "इस नियम के तहत आने वाली फ़ाइलों के सेट" का धुंधला कॉन्सेप्ट. ये वे फ़ाइलें होती हैं जो तब बनाई जाती हैं, जब कॉन्फ़िगर किया गया टारगेट, कमांड लाइन पर या जेनरूल के सोर्स में होता है.
  2. उनकी रनफ़ाइल, सामान्य और डेटा.
  3. उनके आउटपुट ग्रुप. ये "फ़ाइलों के अन्य सेट" हैं, जिन्हें नियम से बनाया जा सकता है. इन्हें BUILD में फ़ाइलग्रुप नियम के asset_group एट्रिब्यूट का इस्तेमाल करके और Java में OutputGroupInfo प्रोवाइडर की मदद से ऐक्सेस किया जा सकता है.

रनफ़ाइल

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

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

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

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

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

  • रनफ़ाइल मेनिफ़ेस्ट इनपुट. यह, रनफ़ाइल ट्री का सिलसिलेवार ब्यौरा है. इसका इस्तेमाल, रनफ़ाइल्स ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. साथ ही, Bazel यह मानता है कि रनफ़ाइल्स ट्री में सिर्फ़ तब बदलाव होता है, जब मेनिफ़ेस्ट के कॉन्टेंट में बदलाव होता है.
  • आउटपुट के लिए, रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल, रनटाइम लाइब्रेरी करती हैं, जो रनफ़ाइल ट्री को मैनेज करती हैं. खास तौर पर, 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 में कैप्चर किया जाता है.

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

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 में है और स्टारलार्क के अलावा "छोटी भाषा" का इस्तेमाल करता है.

कंस्ट्रेंट

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

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

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

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

ये नियम, लेगसी सिस्टम के तहत काम करते हैं और इनका ज़्यादातर इस्तेमाल नहीं किया जाता.

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

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

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

  • RuleVisibility इंटरफ़ेस, कॉन्टेंट दिखने की जानकारी दिखाता है. यह एक कॉन्स्टेंट (पूरी तरह से सार्वजनिक या पूरी तरह से निजी) या लेबल की सूची हो सकती है.
  • लेबल, पैकेज ग्रुप (पैकेज की पहले से तय सूची), पैकेज (//pkg:__pkg__) या पैकेज के सबसे छोटे उपवृक्ष (//pkg:__subpackages__) में से किसी एक का रेफ़रंस दे सकते हैं. यह कमांड-लाइन सिंटैक्स से अलग है, जिसमें //pkg:* या //pkg/... का इस्तेमाल किया जाता है.
  • पैकेज ग्रुप को उनके टारगेट (PackageGroup) और कॉन्फ़िगर किए गए टारगेट (PackageGroupConfiguredTarget) के तौर पर लागू किया जाता है. अगर हम चाहें, तो हम इन्हें आसान नियमों से बदल सकते हैं. उनका लॉजिक, इनकी मदद से लागू किया जाता है: PackageSpecification, जो //pkg/... जैसे सिंगल पैटर्न से मेल खाता है; PackageGroupContents जो किसी एक package_group के packages एट्रिब्यूट से मेल खाता है. साथ ही, PackageSpecificationProvider, जो package_group और इसके ट्रांज़िटिव includes से एग्रीगेट होता है.
  • दिखने की सेटिंग वाले लेबल की सूचियों को डिपेंडेंसी में बदलने का काम, DependencyResolver.visitTargetVisibility और कुछ अन्य जगहों पर किया जाता है.
  • असल जांच CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility() में की जाती है

नेस्ट किए गए सेट

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

  • बिल्ड के लिए इस्तेमाल की जाने वाली C++ हेडर फ़ाइलें
  • ऑब्जेक्ट फ़ाइलें, जो cc_library के ट्रांसिशन क्लोज़र को दिखाती हैं
  • .jar फ़ाइलों का सेट, जो किसी Java नियम को संकलित या चलाने के लिए क्लासपाथ पर होना चाहिए
  • Python नियमों के ट्रांज़िटिव क्लोज़र में Python फ़ाइलों का सेट

अगर हमने List या Set का इस्तेमाल करके, इसे आसान तरीके से किया, तो हमें ज़्यादा मेमोरी का इस्तेमाल करना पड़ेगा: अगर N नियमों की एक चेन है और हर नियम एक फ़ाइल जोड़ता है, तो हमारे पास 1+2+...+N कलेक्शन मेंबर होंगे.

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

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

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

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

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

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

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

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

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

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

  • वह कमांड लाइन जिसे चलाना है
  • इसके लिए ज़रूरी इनपुट आर्टफ़ैक्ट
  • वे एनवायरमेंट वैरिएबल जिन्हें सेट करने की ज़रूरत है
  • ऐसी व्याख्या जो आस-पास के माहौल (जैसे कि प्लैटफ़ॉर्म) के बारे में बताती है, जहां इसे चलाना ज़रूरी है \

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

जांच

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

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

टेस्ट, रेगुलर तौर पर कॉन्फ़िगर किए गए ऐसे टारगेट होते हैं जिनमें 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-शैली की एक एक्सएमएल फ़ाइल, जिसमें टेस्ट शार्ड में अलग-अलग टेस्ट केस की जानकारी दी गई है
  • test.log, इस फ़ंक्शन का कंसोल आउटपुट है. stdout और stderr को अलग-अलग नहीं किया गया है.
  • test.outputs, "एलान नहीं की गई आउटपुट डायरेक्ट्री" है. इसका इस्तेमाल उन टेस्ट में किया जाता है जो टर्मिनल पर प्रिंट की गई फ़ाइलों के साथ-साथ फ़ाइलें भी आउटपुट करना चाहते हैं.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

क्वेरी इंजन

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

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

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

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

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

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

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

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

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

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

इवेंट बस

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

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

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

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

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

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

  1. इससे उपयोगकर्ता को Workspace की उन सबडायरेक्ट्री के बारे में बताने की अनुमति मिलती है जिनमें Bazel को ऐक्सेस करने की अनुमति नहीं है. इनकी जानकारी .bazelignore नाम की फ़ाइल में दी जाती है और यह सुविधा BlacklistedPackagePrefixesFunction में लागू की जाती है.
  2. हम वर्कस्पेस की सबडायरेक्ट्री से एक्सटर्नल रिपॉज़िटरी (डेटा स्टोर करने की जगह) में की जाने वाली मैपिंग को कोड में बदलते हैं. इसे ManagedDirectoriesKnowledge में मैनेज किया जाता है. साथ ही, FileStateValue को उसी तरह हैंडल किया जाता है जिस तरह बाहरी डेटा स्टोर करने की सामान्य जगहों के लिए इन मैपिंग को हैंडल किया जाता है.

रिपॉज़िटरी मैपिंग

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

इसलिए, Basel को किसी बाहरी रिपॉज़िटरी लेबल को फिर से मैप करने की अनुमति मिलती है. इससे @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 खुद नहीं कर सकता या जिन्हें हमने लागू करने के दौरान, 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 वहां _नहीं_ दिखते. ये ExtendedEventHandler के लागू होने के तरीके हैं. इनका मुख्य इस्तेमाल, कैश मेमोरी में सेव किए गए EventBus इवेंट को फिर से चलाने के लिए किया जाता है. ये सभी EventBus इवेंट, Postable को लागू करते हैं. हालांकि, EventBus पर पोस्ट की गई हर चीज़ को ज़रूरी रूप से यह इंटरफ़ेस लागू नहीं करता. सिर्फ़ ExtendedEventHandler से कैश मेमोरी में सेव की गई चीज़ें ही लागू होती हैं. हालांकि, ऐसा करना अच्छा होता है और ज़्यादातर चीज़ें ऐसा करती हैं, लेकिन इसे लागू करना ज़रूरी नहीं है

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

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

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

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

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

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

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

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

Bazel की जांच करना

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

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

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

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

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