Bazel कोडबेस

समस्या की शिकायत करें सोर्स देखें

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

शुरुआती जानकारी

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

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

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

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

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

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

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

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

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

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

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

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

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

किसी निर्देश के खत्म होने पर, Bagel सर्वर क्लाइंट को वापस जाने के लिए दिया जाने वाला एग्ज़िट कोड भेजता है. 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 का इस्तेमाल किया जा रहा है. आम तौर पर, यह जानकारी ऐसी होती है जिसे सोर्स कंट्रोल से चेक आउट किया जाता है.

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

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

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

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

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

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

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

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

सोर्स ट्री, जैसा कि बेज़ल ने देखा

Baज़र, सॉफ़्टवेयर बनाने का कारोबार करता है. इसे बनाने के लिए, सोर्स कोड को पढ़कर और उसे समझना ज़रूरी है. बेज़ल सोर्स कोड की कुल कुलता को "Workspace" कहा जाता है और इसे डेटा स्टोर करने की जगहों, पैकेज, और नियमों के हिसाब से बनाया गया है.

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

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

रिपॉज़िटरी को उसकी रूट डायरेक्ट्री में 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 ऑब्जेक्ट है. यह ज़्यादातर स्ट्रिंग (टारगेट का नाम) से लेकर टारगेट तक का मैप होता है.

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

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

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

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

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

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

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

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

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

स्काईफ़्रेम

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

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

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

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

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

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

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

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

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

स्टारलार्क

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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 में फ़ाइलग्रुप नियम के आउटपुट_ग्रुप एट्रिब्यूट का इस्तेमाल करके और Java में OutputGroupInfo प्रोवाइडर का इस्तेमाल करके ऐक्सेस किया जा सकता है.

रनफ़ाइल

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

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

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

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

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

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

पक्ष

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

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

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

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

थोड़ी सी मुश्किल यह है कि पहलू दूसरे पहलुओं के साथ जुड़ सकते हैं; उदाहरण के लिए, Java IDE के लिए क्लासपाथ इकट्ठा करने वाला शायद क्लासपाथ पर सभी .Jर फ़ाइलों के बारे में जानना चाहेगा, लेकिन उनमें से कुछ प्रोटोकॉल बफ़र होते हैं. इस मामले में, 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() नियम का एक इंस्टेंस है.

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

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

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

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

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

आर्टफ़ैक्ट दो तरह के होते हैं: सोर्स आर्टफ़ैक्ट (एक ऐसा आर्टफ़ैक्ट जो बेज़ल को एक्ज़ीक्यूट करने से पहले उपलब्ध होता है) और डिराइव्ड आर्टफ़ैक्ट (जिन्हें बनाया जाना चाहिए). प्रॉडक्ट से मिले आर्टफ़ैक्ट कई तरह के हो सकते हैं:

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

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

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

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

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

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

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

आखिर में हम सब कुछ SpawnAction में ले जाना चाहते हैं; JavaCompileAction काफ़ी करीब है, लेकिन .d फ़ाइल पार्स करने और स्कैन करने की सुविधा की वजह से C++ एक खास केस है.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

इनपुट डिस्कवरी और इनपुट की काट-छांट

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

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

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

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

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

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

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

किए गए टेस्ट के नतीजे, इवेंट बस में उपलब्ध होते हैं. ये नतीजे, अलग-अलग इवेंट (जैसे कि 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++ में कवरेज इकट्ठा करने के लिए अलग-अलग कंपाइलर विकल्पों की ज़रूरत होती है. इससे कवरेज इकट्ठा हो सकता है. हालांकि, फिर से विश्लेषण करने की ज़रूरत पड़ती है).

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

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

क्वेरी इंजन

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

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

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

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

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

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

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

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

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

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

इवेंट बस

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

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

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

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

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

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

Workspace फ़ाइल

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

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

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

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

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

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

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

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

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

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

डेटा स्टोर करने की जगह की मैपिंग

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

इसलिए, Basel की मदद से बाहरी रिपॉज़िटरी लेबल को फिर से मैप किया जा सकता है, ताकि स्ट्रिंग @guava// एक बाइनरी और दूसरी Guava रिपॉज़िटरी (जैसे कि @guava2//) की दूसरी जगह के रिपॉज़िटरी में एक Guava रिपॉज़िटरी (जैसे कि @guava1//) को रेफ़र कर सके.

इसके अलावा, इसका इस्तेमाल डायमंड join के लिए भी किया जा सकता है. अगर कोई रिपॉज़िटरी @guava1// पर निर्भर करती है और दूसरी @guava2// पर निर्भर करती है, तो रिपॉज़िटरी मैपिंग, दोनों डेटा स्टोर करने की जगहों को फिर से मैप करने की अनुमति देती है. इससे, किसी कैननिकल @guava// रिपॉज़िटरी का इस्तेमाल किया जा सकता है.

मैपिंग को वर्कस्पेस फ़ाइल में, अलग-अलग रिपॉज़िटरी की परिभाषाओं के repo_mapping एट्रिब्यूट के तौर पर बताया जाता है. इसके बाद, यह SkyFrame में WorkspaceFileValue के सदस्य के तौर पर दिखता है, जहां से:

  • Package.Builder.repositoryMapping का इस्तेमाल, पैकेज में मौजूद नियमों के लेबल की वैल्यू वाले एट्रिब्यूट को RuleClass.populateRuleAttributeValues() से बदलने के लिए किया जाता है
  • Package.repositoryMapping जिसका इस्तेमाल विश्लेषण के चरण में किया जाता है ($(location) जैसी उन चीज़ों को ठीक करने के लिए जिन्हें लोड करने के चरण में पार्स नहीं किया गया है)
  • load() स्टेटमेंट में लेबल का समाधान करने के लिए BzlLoadFunction

JNI बिट

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

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

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

कंसोल आउटपुट

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

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

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

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

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

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

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

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

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

प्रोफ़ाइलिंग बेज़ल

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

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

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

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

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

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

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

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

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

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