इस दस्तावेज़ में, कोडबेस के बारे में बताया गया है. साथ ही, यह भी बताया गया है कि Bazel को कैसे बनाया गया है. यह उन लोगों के लिए है जो Bazel में योगदान देना चाहते हैं, न कि आम उपयोगकर्ताओं के लिए.
परिचय
बेज़ेल का कोडबेस बड़ा है (~350KLOC प्रोडक्शन कोड और ~260 KLOC टेस्ट कोड) और कोई भी पूरे लैंडस्केप से वाकिफ़ नहीं है: सभी लोग अपनी खास घाटी को अच्छी तरह से जानते हैं, लेकिन कुछ ही लोगों को यह पता है कि हर दिशा में पहाड़ियों पर क्या मौजूद है.
इस दस्तावेज़ में कोडबेस के बारे में खास जानकारी दी गई है, ताकि लोग इस पर काम करना शुरू कर सकें. इससे, उन्हें बीच में किसी भी तरह की समस्या का सामना नहीं करना पड़ेगा.
Baज़ल के सोर्स कोड का सार्वजनिक वर्शन, GitHub में मौजूद है. इसके लिए, github.com/ba सुझावों का इस्तेमाल करें/bazu. यह "सही सोर्स" नहीं है. इसे 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
) के बाद होते हैं. पहले वाले विकल्प को "स्टार्टअप विकल्प" कहा जाता है और इसका असर पूरी तरह से सर्वर प्रोसेस पर पड़ता है. वहीं, बाद वाले विकल्प "कमांड का विकल्प", सिर्फ़ एक निर्देश पर असर डालता है.
हर सर्वर इंस्टेंस से एक वर्कस्पेस जुड़ा होता है. यह "रिपॉज़िटरी" के तौर पर जाना जाने वाला सोर्स ट्री का कलेक्शन होता है. आम तौर पर, हर वर्कस्पेस में एक ऐक्टिव सर्वर इंस्टेंस होता है. कस्टम आउटपुट बेस तय करके, इस समस्या को हल किया जा सकता है. ज़्यादा जानकारी के लिए, "डायरेक्ट्री लेआउट" सेक्शन देखें.
Bazel को एक ELF एक्ज़ीक्यूटेबल के तौर पर डिस्ट्रिब्यूट किया जाता है. यह एक मान्य .zip फ़ाइल भी है.
bazel
टाइप करने पर, C++ में लागू किए गए ऊपर दिए गए ELF executable ("क्लाइंट") को कंट्रोल मिल जाता है. यह नीचे दिए गए चरणों का इस्तेमाल करके सही सर्वर प्रोसेस सेट अप करता है:
- जांचता है कि क्या इसे पहले ही अपने आप एक्सट्रैक्ट किया गया है. अगर ऐसा नहीं है, तो यह ऐसा करता है. यहीं से सर्वर लागू करने की प्रोसेस शुरू होती है.
- यह जांचता है कि क्या कोई ऐसा सर्वर इंस्टेंस मौजूद है जो काम कर रहा है: वह चल रहा है,
उसमें स्टार्टअप के सही विकल्प हैं और सही फ़ाइल फ़ोल्डर की डायरेक्ट्री का इस्तेमाल किया गया है. यह
$OUTPUT_BASE/server
डायरेक्ट्री में जाकर, चल रहे सर्वर को ढूंढता है. इस डायरेक्ट्री में, उस पोर्ट की लॉक फ़ाइल होती है जिस पर सर्वर सुन रहा होता है. - ज़रूरत पड़ने पर, पुरानी सर्वर प्रोसेस को बंद कर देता है
- ज़रूरत पड़ने पर, नई सर्वर प्रोसेस शुरू करता है
सही सर्वर प्रोसेस तैयार होने के बाद, जिस निर्देश को चलाना है उसे gRPC इंटरफ़ेस के ज़रिए भेजा जाता है. इसके बाद, Bazel का आउटपुट टर्मिनल पर वापस भेजा जाता है. एक ही समय पर सिर्फ़ एक निर्देश चल सकता है. इसे लागू करने के लिए, C++ और Java में अलग-अलग हिस्सों के साथ, लॉक करने के बेहतर तरीके का इस्तेमाल किया जाता है. एक साथ कई कमांड चलाने के लिए, कुछ बुनियादी ढांचा मौजूद है, क्योंकि bazel version
को किसी दूसरे कमांड के साथ चलाने में कुछ परेशानी होती है. मुख्य समस्या, BlazeModule
s के लाइफ़ साइकल और BlazeRuntime
में कुछ स्टेटस है.
किसी निर्देश के आखिर में, Bazel सर्वर वह बाहर निकलने का कोड भेजता है जिसे क्लाइंट को दिखाना चाहिए. bazel run
को लागू करना एक दिलचस्प बात है: इस कमांड का काम, हाल ही में Bazel से बनाए गए कुछ कोड को चलाना है. हालांकि, यह सर्वर प्रोसेस से ऐसा नहीं कर सकता, क्योंकि इसमें टर्मिनल नहीं है. इसलिए, यह क्लाइंट को बताता है कि उसे किस बाइनरी को exec()
करना चाहिए और किन आर्ग्युमेंट के साथ.
जब कोई Ctrl-C दबाता है, तो क्लाइंट इसे gRPC कनेक्शन पर रद्द करें कॉल में बदल देता है, जो जितनी जल्दी हो सके कमांड को खत्म करने की कोशिश करता है. तीसरे Ctrl-C के बाद, क्लाइंट सर्वर को SIGKILL भेजता है.
क्लाइंट का सोर्स कोड src/main/cpp
में है और सर्वर से बातचीत करने के लिए इस्तेमाल किया जाने वाला प्रोटोकॉल src/main/protobuf/command_server.proto
में है .
सर्वर का मुख्य एंट्री पॉइंट BlazeRuntime.main()
है और क्लाइंट के gRPC कॉल को GrpcServerImpl.run()
मैनेज करता है.
डायरेक्ट्री का लेआउट
बिल्ड के दौरान Baज़ल, डायरेक्ट्री का कुछ मुश्किल सेट बना देता है. पूरा ब्यौरा आउटपुट डायरेक्ट्री लेआउट में उपलब्ध है.
"मुख्य रिपॉज़िटरी", वह सोर्स ट्री है जिसमें Bazel को चलाया जाता है. आम तौर पर, यह जानकारी ऐसी होती है जिसे सोर्स कंट्रोल से चेक आउट किया जाता है. इस डायरेक्ट्री के रूट को "फ़ाइल फ़ोल्डर का रूट" कहा जाता है.
Bazel अपना सारा डेटा "आउटपुट उपयोगकर्ता रूट" में डालता है. आम तौर पर, यह $HOME/.cache/bazel/_bazel_${USER}
होता है. हालांकि, --output_user_root
स्टार्टअप विकल्प का इस्तेमाल करके इसे बदला जा सकता है.
"इंस्टॉल बेस" वह जगह है जहां Basel को एक्सट्रैक्ट किया जाता है. यह अपने-आप होता है और हर 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 सर्वर को कंट्रोल मिल जाता है और उसे उस कमांड के बारे में पता चल जाता है जिसे उसे रन करना है, तो ये इवेंट इस क्रम में होते हैं:
BlazeCommandDispatcher
को नए अनुरोध के बारे में सूचना दी गई है. यह तय करता है कि निर्देश को चलाने के लिए, वर्कस्पेस की ज़रूरत है या नहीं. यह ज़रूरी नहीं है कि हर निर्देश के लिए वर्कस्पेस की ज़रूरत हो. जैसे, वर्शन या मदद जैसे निर्देशों के लिए वर्कस्पेस की ज़रूरत नहीं होती. साथ ही, यह भी तय करता है कि कोई दूसरा निर्देश चल रहा है या नहीं.सही निर्देश मिल गया है. हर कमांड में इंटरफ़ेस
BlazeCommand
लागू होना चाहिए और उसमें@Command
एनोटेशन होना चाहिए. यह एक तरह का एंटीपैटर्न है. यह अच्छा होगा, अगर किसी कमांड के लिए ज़रूरी सभी मेटाडेटा कोBlazeCommand
पर मौजूद तरीकों से बताया गया होकमांड-लाइन के विकल्पों को पार्स किया जाता है. हर कमांड के लिए, कमांड लाइन के अलग-अलग विकल्प होते हैं. इनके बारे में
@Command
एनोटेशन में बताया गया है.एक इवेंट बस बनाई जाती है. इवेंट बस, उन इवेंट के लिए एक स्ट्रीम है जो बिल्ड के दौरान होते हैं. इनमें से कुछ को, बिल्ड इवेंट प्रोटोकॉल के तहत Bazel से बाहर एक्सपोर्ट किया जाता है, ताकि दुनिया को यह पता चल सके कि बिल्ड कैसे हुआ.
निर्देश को कंट्रोल मिल जाता है. सबसे दिलचस्प निर्देश वे होते हैं जो बिल्ड को रन करते हैं: बिल्ड, टेस्ट, रन, कवरेज वगैरह: इस सुविधा को
BuildTool
ने लागू किया है.कमांड लाइन पर टारगेट पैटर्न का सेट पार्स किया जाता है और
//pkg:all
और//pkg/...
जैसे वाइल्डकार्ड हल किए जाते हैं. इसेAnalysisPhaseRunner.evaluateTargetPatterns()
में लागू किया गया और Skyframe मेंTargetPatternPhaseValue
के तौर पर सुधार किया गया.लोडिंग/विश्लेषण का फ़ेज़, ऐक्शन ग्राफ़ बनाने के लिए चलाया जाता है. यह कमांड का एक डायरेक्टेड एकाइक्लिक ग्राफ़ होता है, जिसे बिल्ड के लिए एक्ज़ीक्यूट किया जाना चाहिए.
प्रोग्राम चलाने का चरण शुरू हो जाता है. इसका मतलब है कि अनुरोध किए गए टॉप-लेवल टारगेट बनाने के लिए, ज़रूरी हर कार्रवाई को चलाया जाता है.
कमांड लाइन के विकल्प
बेज़ल इनवोकेशन के लिए कमांड लाइन के विकल्पों की जानकारी OptionsParsingResult
ऑब्जेक्ट में दी गई है. इसके तहत, "विकल्प" की क्लास से लेकर विकल्पों की वैल्यू तक का मैप शामिल होता है. "विकल्प क्लास", OptionsBase
की एक सबक्लास होती है. साथ ही, यह एक-दूसरे से जुड़े कमांड लाइन विकल्पों को एक साथ ग्रुप करती है. उदाहरण के लिए:
- प्रोग्रामिंग भाषा (
CppOptions
याJavaOptions
) से जुड़े विकल्प. येFragmentOptions
के सबक्लास होने चाहिए और आखिर में इन्हेंBuildOptions
ऑब्जेक्ट में रैप कर दिया जाता है. - Bazel के ऐक्शन लागू करने के तरीके से जुड़े विकल्प (
ExecutionOptions
)
इन विकल्पों को विश्लेषण के चरण में इस्तेमाल करने के लिए डिज़ाइन किया गया है. इन्हें Java में RuleContext.getFragment()
या Starlark में ctx.fragments
के ज़रिए इस्तेमाल किया जा सकता है.
इनमें से कुछ विकल्पों को, प्रोग्राम के लागू होने के दौरान पढ़ा जाता है. उदाहरण के लिए, C++ में शामिल स्कैनिंग की सुविधा का इस्तेमाल करना है या नहीं. हालांकि, इसके लिए हमेशा साफ़ तौर पर प्लंबिंग की ज़रूरत होती है, क्योंकि उस समय BuildConfiguration
उपलब्ध नहीं होता. ज़्यादा जानकारी के लिए, "कॉन्फ़िगरेशन" सेक्शन देखें.
चेतावनी: हम यह दिखाना चाहते हैं कि OptionsBase
इंस्टेंस में बदलाव नहीं किया जा सकता और उनका इस्तेमाल उसी तरह किया जा सकता है जैसे SkyKeys
का हिस्सा. हालांकि, ऐसा नहीं है. इनमें बदलाव करने से, Bazel को ऐसे तरीके से गड़बड़ किया जा सकता है जिसे डीबग करना मुश्किल होता है. दुर्भाग्य से, उन्हें असल में नहीं बदला जा सकने वाला बनाना एक बहुत बड़ी कोशिश है.
(किसी FragmentOptions
को बनाने के तुरंत बाद उसमें बदलाव करना ठीक है. ऐसा तब करें, जब किसी और को उसका रेफ़रंस रखने का मौका न मिले और equals()
या hashCode()
को उस पर कॉल न किया गया हो.)
Bazel, विकल्प क्लास के बारे में इन तरीकों से जानता है:
- कुछ Bazel में पहले से मौजूद हैं (
CommonCommandOptions
) - हर Basel कमांड के लिए,
@Command
एनोटेशन से ConfiguredRuleClassProvider
से (ये अलग-अलग प्रोग्रामिंग भाषाओं से जुड़े कमांड लाइन विकल्प हैं)- Starlark नियम भी अपने विकल्प तय कर सकते हैं (यहां देखें)
हर विकल्प (Starlark से तय किए गए विकल्पों को छोड़कर), FragmentOptions
सबक्लास का एक सदस्य वैरिएबल होता है. इसमें @Option
एनोटेशन होता है, जो कुछ सहायता टेक्स्ट के साथ-साथ कमांड लाइन विकल्प का नाम और टाइप बताता है.
कमांड लाइन के विकल्प की वैल्यू का Java टाइप आम तौर पर आसान होता है (जैसे, कोई स्ट्रिंग, कोई पूर्णांक, कोई बूलियन, कोई लेबल वगैरह). हालांकि, हम ज़्यादा मुश्किल टाइप के विकल्पों के साथ भी काम करते हैं. इस मामले में, कमांड लाइन स्ट्रिंग को डेटा टाइप में बदलने का काम, com.google.devtools.common.options.Converter
को लागू करने पर होता है.
सोर्स ट्री, जैसा कि बेज़ल ने देखा
Bazel, सॉफ़्टवेयर बनाने का काम करता है. यह सोर्स कोड को पढ़कर और उसका विश्लेषण करके ऐसा करता है. Bazel जिस सोर्स कोड पर काम करता है उसे "वर्कस्पेस" कहा जाता है. इसे रिपॉज़िटरी, पैकेज, और नियमों में बांटा जाता है.
डेटा स्टोर करने की जगह
"डेटा स्टोर करने की जगह" एक सोर्स ट्री है जिस पर डेवलपर काम करता है. यह आम तौर पर एक प्रोजेक्ट के बारे में बताता है. Bazel का पूर्वज, Blaze, एक मोनोरेपो पर काम करता था. इसके उलट, Bazel उन प्रोजेक्ट के साथ काम करता है जिनका सोर्स कोड कई रिपॉज़िटरी में मौजूद होता है. जिस रिपॉज़िटरी से Bazel को शुरू किया जाता है उसे "मुख्य रिपॉज़िटरी" कहा जाता है. अन्य रिपॉज़िटरी को "बाहरी रिपॉज़िटरी" कहा जाता है.
किसी रिपॉज़िटरी को उसकी रूट डायरेक्ट्री में मौजूद, रिपॉज़िटरी की बाउंड्री फ़ाइल (MODULE.bazel
, REPO.bazel
या लेगसी कॉन्टेक्स्ट में, WORKSPACE
या WORKSPACE.bazel
) से मार्क किया जाता है. मुख्य रिपॉज़िटरी, वह सोर्स ट्री होता है जहां से Bazel को शुरू किया जा रहा है. बाहरी डेटा स्टोर को कई तरीकों से तय किया जाता है. ज़्यादा जानकारी के लिए, बाहरी डिपेंडेंसी की खास जानकारी देखें.
बाहरी रिपॉज़िटरी का कोड, $OUTPUT_BASE/external
में लिंक किया गया है या डाउनलोड किया गया है.
बिल्ड करते समय, पूरे सोर्स ट्री को एक साथ जोड़ना ज़रूरी होता है. यह काम SymlinkForest
करता है. यह मुख्य रिपॉज़िटरी में मौजूद हर पैकेज को $EXECROOT
से और हर बाहरी रिपॉज़िटरी को $EXECROOT/external
या $EXECROOT/..
से लिंक करता है.
पैकेज
हर रिपॉज़िटरी में पैकेज, मिलती-जुलती फ़ाइलों का कलेक्शन, और डिपेंडेंसी की जानकारी होती है. इन्हें BUILD
या BUILD.bazel
नाम की फ़ाइल से तय किया जाता है. अगर दोनों का इस्तेमाल किया जाता है, तो Baze, BUILD.bazel
को पसंद करता है.
BUILD
फ़ाइलों को अब भी स्वीकार किए जाने की वजह यह है कि Baze के पूर्वज, Blaze ने इस फ़ाइल नाम का इस्तेमाल किया था. हालांकि, यह आम तौर पर इस्तेमाल किया जाने वाला पाथ सेगमेंट है. खास तौर पर, Windows पर, जहां फ़ाइल के नाम केस-इन्सेंसिव होते हैं.
पैकेज एक-दूसरे से अलग होते हैं: किसी पैकेज की BUILD
फ़ाइल में बदलाव करने से, दूसरे पैकेज में बदलाव नहीं होता. BUILD
फ़ाइलों को जोड़ने या हटाने से, अन्य पैकेज बदल सकते हैं. ऐसा इसलिए होता है, क्योंकि बार-बार लागू होने वाले ग्लोब पैकेज की सीमाओं पर रुक जाते हैं. इसलिए, BUILD
फ़ाइल की मौजूदगी से बार-बार लागू होने की प्रोसेस रुक जाती है.
BUILD
फ़ाइल का आकलन करने की प्रोसेस को "पैकेज लोड करना" कहा जाता है. इसे PackageFactory
क्लास में लागू किया गया है. यह Starlark इंटरप्रेटर को कॉल करके काम करता है. साथ ही, इसके लिए उपलब्ध नियम क्लास के सेट के बारे में जानकारी ज़रूरी है. पैकेज लोड करने का नतीजा, एक Package
ऑब्जेक्ट होता है. यह ज़्यादातर किसी स्ट्रिंग (टारगेट का नाम) से टारगेट पर मैप होता है.
पैकेज लोड करने के दौरान, ग्लोबिंग की वजह से समस्याएं आती हैं: Bazel को हर सोर्स फ़ाइल को साफ़ तौर पर सूची में शामिल करने की ज़रूरत नहीं होती. इसके बजाय, यह ग्लोब (जैसे, glob(["**/*.java"])
) चला सकता है. शेल के विपरीत, यह बार-बार होने वाली ग्लोबिंग के साथ काम करता है, जो सब-डायरेक्ट्री में जाती है (लेकिन सब-पैकेज में नहीं). इसके लिए, फ़ाइल सिस्टम का ऐक्सेस ज़रूरी है. यह प्रोसेस धीमी हो सकती है. इसलिए, हम इसे एक साथ और बेहतर तरीके से चलाने के लिए, सभी तरह की तरकीबें अपनाते हैं.
ग्लोबिंग की सुविधा इन क्लास में लागू की गई है:
LegacyGlobber
, एक तेज़ और मज़ेदार SkyFrame, अज्ञात ग्लोबरSkyframeHybridGlobber
एक ऐसा वर्शन है जो Skyframe का इस्तेमाल करता है और "Skyframe को रीस्टार्ट होने" से बचाने के लिए लेगसी ग्लोबर पर वापस जाता है (इस बारे में नीचे बताया गया है)
Package
क्लास में कुछ ऐसे सदस्य होते हैं जिनका इस्तेमाल खास तौर पर "बाहरी" पैकेज (एक्सटर्नल डिपेंडेंसी से जुड़े) को पार्स करने के लिए किया जाता है. साथ ही, जो असल पैकेज के लिहाज़ से काम के नहीं होते हैं. यह डिज़ाइन में मौजूद एक गड़बड़ी है. इसकी वजह यह है कि रेगुलर पैकेज की जानकारी देने वाले ऑब्जेक्ट में, ऐसे फ़ील्ड नहीं होने चाहिए जिनमें किसी और चीज़ की जानकारी हो. इनमें शामिल हैं:
- रिपॉज़िटरी की मैपिंग
- रजिस्टर किए गए टूलचेन
- रजिस्टर किए गए एक्सीक्यूशन प्लैटफ़ॉर्म
आम तौर पर, "बाहरी" पैकेज को पार्स करने और सामान्य पैकेज को पार्स करने के बीच ज़्यादा अंतर होता है, ताकि Package
को दोनों की ज़रूरतों को पूरा करने की ज़रूरत न पड़े. अफ़सोस की बात है कि ऐसा करना मुश्किल है, क्योंकि ये दोनों एक-दूसरे से बहुत गहरे तरीके से जुड़े हुए हैं.
लेबल, टारगेट, और नियम
पैकेज, टारगेट से बने होते हैं. ये टारगेट इन टाइप के होते हैं:
- फ़ाइलें: ऐसी चीज़ें जो बिल्ड का इनपुट या आउटपुट होती हैं. Bazel के हिसाब से, हम इन्हें आर्टफ़ैक्ट कहते हैं. इनके बारे में कहीं और बताया गया है. बिल्ड के दौरान बनाई गई सभी फ़ाइलें टारगेट नहीं होतीं. आम तौर पर, Bazel के आउटपुट में कोई लेबल नहीं होता.
- नियम: इनमें इनपुट से आउटपुट पाने का तरीका बताया गया है. आम तौर पर, ये किसी प्रोग्रामिंग भाषा (जैसे,
cc_library
,java_library
याpy_library
) से जुड़े होते हैं. हालांकि, कुछ ऐसे भी होते हैं जो किसी भाषा से जुड़े नहीं होते (जैसे,genrule
याfilegroup
) - पैकेज ग्रुप: इनके बारे में पैकेज किसको दिखे सेक्शन में बताया गया है.
टारगेट के नाम को लेबल कहा जाता है. लेबल का सिंटैक्स @repo//pac/kage:name
है. इसमें repo
, उस रिपॉज़िटरी का नाम है जिसमें लेबल मौजूद है, pac/kage
वह डायरेक्ट्री है जिसमें BUILD
फ़ाइल मौजूद है, और name
पैकेज की डायरेक्ट्री के हिसाब से फ़ाइल का पाथ है (अगर लेबल किसी सोर्स फ़ाइल का रेफ़रंस देता है). कमांड-लाइन पर किसी टारगेट का रेफ़रंस देते समय, लेबल के कुछ हिस्सों को छोड़ा जा सकता है:
- अगर रिपॉज़िटरी की जानकारी हटा दी गई है, तो लेबल को मुख्य डेटा स्टोर करने की जगह में ले जाया जाता है.
- अगर पैकेज का हिस्सा (जैसे,
name
या:name
) छोड़ा जाता है, तो लेबल को मौजूदा वर्किंग डायरेक्ट्री के पैकेज में माना जाता है. अपलेवल रेफ़रंस (..) वाले रिलेटिव पाथ की अनुमति नहीं है
एक तरह के नियम (जैसे कि "C++ लाइब्रेरी") को "नियम क्लास" कहा जाता है. नियम की क्लास Starlark (rule()
फ़ंक्शन) या Java (जिन्हें "नेटिव नियम", RuleClass
टाइप कहते हैं) में लागू की जा सकती है. लंबे समय में, Starlark में हर भाषा के हिसाब से नियम लागू किए जाएंगे. हालांकि, कुछ लेगसी रूल फ़ैमिली (जैसे कि Java या C++), फ़िलहाल Java में ही लागू होंगी.
Starlark नियम क्लास को load()
स्टेटमेंट का इस्तेमाल करके, BUILD
फ़ाइलों की शुरुआत में इंपोर्ट करना ज़रूरी है. वहीं, Java नियम क्लास को ConfiguredRuleClassProvider
के साथ रजिस्टर करने की वजह से, Bazel उन्हें "पहचानता" है.
नियम की क्लास में यह जानकारी शामिल होती है:
- इसके एट्रिब्यूट (जैसे,
srcs
,deps
): उनके टाइप, डिफ़ॉल्ट वैल्यू, सीमाएं वगैरह. - हर एट्रिब्यूट से जुड़े कॉन्फ़िगरेशन ट्रांज़िशन और आसपेक्ट (अगर कोई है)
- नियम लागू करना
- ट्रांज़िटिव जानकारी देने वाले नियम, "आम तौर पर" बनाते हैं
शब्दावली से जुड़ा नोट: कोडबेस में, हम अक्सर "नियम" का इस्तेमाल, नियम क्लास से बनाए गए टारगेट के लिए करते हैं. हालांकि, Starlark और उपयोगकर्ताओं के लिए बने दस्तावेज़ में, "नियम" का इस्तेमाल सिर्फ़ नियम क्लास के लिए किया जाना चाहिए. टारगेट सिर्फ़ एक "टारगेट" है. यह भी ध्यान रखें कि RuleClass
के नाम में "क्लास" होने के बावजूद, नियम क्लास और उस टाइप के टारगेट के बीच Java इनहेरिटेंस संबंध नहीं है.
Skyframe
Basel के इवैलुएशन फ़्रेमवर्क को Skyframe कहा जाता है. इसका मॉडल यह है कि किसी भी बिल्ड के दौरान, जो भी चीज़ें बनाई जानी हैं उन्हें एक डायरेक्टेड ऐसाइक्लिक ग्राफ़ में व्यवस्थित किया जाता है. इस ग्राफ़ में, डेटा के किसी भी हिस्से से उसकी डिपेंडेंसी पर जाने वाले किनारे होते हैं. डिपेंडेंसी, डेटा के ऐसे अन्य हिस्से होते हैं जिनके बारे में जानने के बाद ही, डेटा का पूरा हिस्सा बनाया जा सकता है.
ग्राफ़ में मौजूद नोड को SkyValue
कहा जाता है और उनके नाम को
SkyKey
कहा जाता है. दोनों में बदलाव नहीं किया जा सकता. इनमें सिर्फ़ ऐसे ऑब्जेक्ट को ऐक्सेस किया जा सकता है जिनमें बदलाव नहीं किया जा सकता. यह इनवैरिएंट, ज़्यादातर मामलों में लागू होता है. अगर ऐसा नहीं होता है, तो हम पूरी कोशिश करते हैं कि इनमें बदलाव न किया जाए या फिर सिर्फ़ ऐसे बदलाव किए जाएं जो बाहर से न दिखें. जैसे, अलग-अलग विकल्पों की क्लास BuildOptions
, जो BuildConfigurationValue
और उसकी SkyKey
की सदस्य है.
इससे यह पता चलता है कि 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
का आकलन करने के लिए किया गया काम शामिल नहीं है. इसलिए, हम आम तौर पर इस समस्या को हल करने के लिए, ये काम करते हैं:
getValuesAndExceptions()
का इस्तेमाल करके, डिपेंडेंसी को एक साथ कई बार डिक्लेयर करना, ताकि फिर से शुरू करने की संख्या को सीमित किया जा सके.- किसी
SkyValue
को अलग-अलग हिस्सों में बांटना, जिन्हें अलग-अलगSkyFunction
से कैलकुलेट किया जाता है, ताकि उन्हें अलग से कैलकुलेट और कैश मेमोरी में सेव किया जा सके. इसे रणनीति के हिसाब से किया जाना चाहिए, क्योंकि इससे मेमोरी के इस्तेमाल में बढ़ोतरी हो सकती है. SkyFunction.Environment.getState()
का इस्तेमाल करके या "Skyframe के पीछे" ad hoc स्टैटिक कैश रखकर, रीस्टार्ट के बीच स्टेटस सेव करना. जटिल SkyFunctions के साथ, रीस्टार्ट के बीच स्टेटस मैनेजमेंट मुश्किल हो सकता है. इसलिए, लॉजिकल कंसिस्टेंसी के लिए स्ट्रक्चर्ड तरीके से काम करने के लिए,StateMachine
को लॉन्च किया गया था. इसमेंSkyFunction
में हैरारकी वाले कैलकुलेशन को निलंबित और फिर से शुरू करने के लिए हुक शामिल हैं. उदाहरण:DependencyResolver#computeDependencies
, कॉन्फ़िगर किए गए टारगेट के डायरेक्ट डिपेंडेंसी के संभावित बड़े सेट का हिसाब लगाने के लिए,getState()
के साथStateMachine
का इस्तेमाल करता है. इससे रीस्टार्ट हो सकता है.
आम तौर पर, Bazel को इस तरह के तरीके अपनाने की ज़रूरत होती है, क्योंकि आम तौर पर, सैकड़ों से हज़ारों इन-फ़्लाइट Skyframe नोड होते हैं. साथ ही, Java में लाइटव़ेइट थ्रेड के लिए, 2023 तक StateMachine
लागू करने की सुविधा बेहतर नहीं होती.
Starlark
Starlark, डोमेन के लिए खास तौर पर इस्तेमाल की जाने वाली भाषा है. लोग इस भाषा का इस्तेमाल करके, बैजल को कॉन्फ़िगर करने और उसका दायरा बढ़ाने के लिए इस्तेमाल करते हैं. इसे Python के सीमित सबसेट के तौर पर माना जाता है, जिसमें बहुत कम टाइप होते हैं. साथ ही, कंट्रोल फ़्लो पर ज़्यादा पाबंदियां होती हैं. सबसे अहम बात यह है कि एक साथ कई फ़ाइलें पढ़ने की सुविधा चालू करने के लिए, डेटा में बदलाव न होने की गारंटी दी जाती है. यह ट्यूरिंग-पूरी नहीं है, जिसकी वजह से कुछ (लेकिन सभी नहीं) उपयोगकर्ताओं को उसी भाषा में सामान्य प्रोग्रामिंग करने से मना किया जाता है.
Starlark को net.starlark.java
पैकेज में लागू किया गया है.
इसके अलावा, यहां Go में भी इसे लागू किया जा सकता है. Baज़ल में इस्तेमाल किया जाने वाला Java लागू करने का तरीका, फ़िलहाल एक अनुवादक के तौर पर काम करता है.
स्टारलार्क का इस्तेमाल कई तरह के कॉन्टेक्स्ट में किया जाता है. जैसे:
BUILD
फ़ाइलें. यहां नए बिल्ड टारगेट तय किए जाते हैं. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास सिर्फ़BUILD
फ़ाइल और इससे लोड की गई.bzl
फ़ाइलों के कॉन्टेंट का ऐक्सेस होता है.MODULE.bazel
फ़ाइल. यहां बाहरी डिपेंडेंसी तय की जाती हैं. इस संदर्भ में चल रहे Starlark कोड के पास, पहले से तय किए गए कुछ निर्देशों का ऐक्सेस बहुत सीमित होता है..bzl
फ़ाइलें. यहां नए बिल्ड नियम, repo नियम, मॉड्यूल के एक्सटेंशन तय किए जाते हैं. यहां मौजूद Starlark कोड, नए फ़ंक्शन तय कर सकता है और अन्य.bzl
फ़ाइलों से लोड हो सकता है.
BUILD
और .bzl
फ़ाइलों के लिए उपलब्ध बोलियाँ थोड़ी अलग होती हैं, क्योंकि इनमें अलग-अलग चीज़ें बताई जाती हैं. इनके बीच के अंतर की सूची यहां दी गई है.
Starlark के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
लोडिंग/विश्लेषण का चरण
लोड करने/विश्लेषण करने के चरण में, Bazel यह तय करता है कि किसी खास नियम को बनाने के लिए कौनसी कार्रवाइयां ज़रूरी हैं. इसकी बुनियादी इकाई एक "कॉन्फ़िगर किया गया टारगेट" है, जो संभावना है कि एक (टारगेट, कॉन्फ़िगरेशन) जोड़ी है.
इसे "लोडिंग/विश्लेषण का फ़ेज़" कहा जाता है, क्योंकि इसे दो अलग-अलग हिस्सों में बांटा जा सकता है. पहले इन हिस्सों को क्रम से चलाया जाता था, लेकिन अब ये एक-दूसरे के साथ ओवरलैप हो सकते हैं:
- पैकेज लोड करना, यानी
BUILD
फ़ाइलों को उनPackage
ऑब्जेक्ट में बदलना जो उन्हें दिखाते हैं - कॉन्फ़िगर किए गए टारगेट का विश्लेषण करना. इसका मतलब है कि ऐक्शन ग्राफ़ बनाने के लिए, नियमों को लागू करना
कमांड लाइन पर अनुरोध किए गए कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में, कॉन्फ़िगर किए गए हर टारगेट का विश्लेषण नीचे से किया जाना चाहिए. इसका मतलब है कि पहले लीफ़ नोड का विश्लेषण किया जाना चाहिए. इसके बाद, कमांड लाइन पर मौजूद टारगेट तक का विश्लेषण किया जाना चाहिए. कॉन्फ़िगर किए गए किसी एक टारगेट के विश्लेषण के इनपुट ये हैं:
- कॉन्फ़िगरेशन. ("कैसे" उस नियम को बनाएं; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म, लेकिन कमांड लाइन के ऐसे विकल्प भी जिनका उपयोगकर्ता C++ कंपाइलर को पास करना चाहता है)
- डायरेक्ट डिपेंडेंसी. ट्रांज़िशन की जानकारी देने वाली उनकी कंपनियां, विश्लेषण किए जा रहे नियम के लिए उपलब्ध हैं. इन्हें इस तरह इसलिए कहा जाता है, क्योंकि ये कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र में जानकारी का "रोल-अप" उपलब्ध कराते हैं. जैसे, क्लासपाथ पर मौजूद सभी .jar फ़ाइलें या C++ बाइनरी में लिंक की जाने वाली सभी .o फ़ाइलें)
- टारगेट. यह उस पैकेज को लोड करने का नतीजा है जिसमें टारगेट मौजूद है. नियमों के लिए, इसमें उनके एट्रिब्यूट शामिल होते हैं. आम तौर पर, यही बात मायने रखती है.
- कॉन्फ़िगर किए गए टारगेट को लागू करना. नियमों के लिए, यह Starlark या Java में हो सकता है. नियम के बिना कॉन्फ़िगर किए गए सभी टारगेट, Java में लागू किए जाते हैं.
कॉन्फ़िगर किए गए टारगेट का विश्लेषण करने पर, यह नतीजा मिलता है:
- ट्रांज़िशन की जानकारी देने वाली सेवा देने वाली कंपनियां, कॉन्फ़िगर किए गए टारगेट को ऐक्सेस कर सकती हैं
- यह कौन-कौनसी आर्टफ़ैक्ट बना सकता है और उन्हें बनाने के लिए क्या किया जा सकता है.
Java नियमों के लिए एपीआई RuleContext
है, जो Starlark नियमों के ctx
आर्ग्युमेंट के बराबर है. इसका एपीआई ज़्यादा बेहतर है, लेकिन साथ ही, इसमें 'बुरे काम™' करना आसान है. उदाहरण के लिए, ऐसा कोड लिखना जिसका समय या स्टोरेज की जटिलता क्वाड्रैटिक (या इससे भी खराब) हो, Bazel सर्वर को Java अपवाद की वजह से क्रैश करना या इनवैरिएंट का उल्लंघन करना (जैसे, Options
इंस्टेंस में गलती से बदलाव करना या कॉन्फ़िगर किए गए टारगेट को बदलने योग्य बनाना)
कॉन्फ़िगर किए गए टारगेट की डायरेक्ट डिपेंडेंसी तय करने वाला एल्गोरिदम, DependencyResolver.dependentNodeMap()
में रहता है.
कॉन्फ़िगरेशन
कॉन्फ़िगरेशन, टारगेट बनाने का तरीका है: किस प्लैटफ़ॉर्म के लिए, कमांड लाइन के कौनसे विकल्पों के साथ वगैरह.
एक ही बिल्ड में, एक ही टारगेट को कई कॉन्फ़िगरेशन के लिए बनाया जा सकता है. उदाहरण के लिए, यह तब मददगार होता है, जब एक ही कोड का इस्तेमाल, बिल्ड के दौरान चलने वाले टूल और टारगेट कोड के लिए किया जाता है. साथ ही, जब हम क्रॉस-कंपाइल कर रहे हों या कोई बड़ा Android ऐप्लिकेशन (ऐसा ऐप्लिकेशन जिसमें कई सीपीयू आर्किटेक्चर के लिए नेटिव कोड शामिल होता है) बना रहे हों
कॉन्फ़िगरेशन, BuildOptions
इंस्टेंस होता है. हालांकि, आम तौर पर BuildOptions
को BuildConfiguration
में रैप किया जाता है, जो कई अन्य फ़ंक्शन उपलब्ध कराता है. यह डिपेंडेंसी ग्राफ़ के ऊपर से नीचे तक
जाती है. अगर यह बदलता है, तो बिल्ड का फिर से विश्लेषण करना होगा.
इस वजह से, गड़बड़ियां होती हैं. उदाहरण के लिए, अगर अनुरोध किए गए टेस्ट रन की संख्या में बदलाव होता है, तो पूरे बिल्ड का फिर से विश्लेषण करना पड़ता है. भले ही, इसका असर सिर्फ़ टेस्ट टारगेट पर पड़ता हो. हम कॉन्फ़िगरेशन को "छोटा" करने की योजना बना रहे हैं, ताकि ऐसा न हो. हालांकि, यह सुविधा अभी तैयार नहीं है.
जब किसी नियम को लागू करने के लिए कॉन्फ़िगरेशन का कोई हिस्सा ज़रूरी होता है, तो उसे RuleClass.Builder.requiresConfigurationFragments()
का इस्तेमाल करके इसकी परिभाषा में एलान करना पड़ता है. इनके ज़रिए, गलतियों (जैसे कि Java फ़्रैगमेंट का इस्तेमाल करने वाले Python के नियम) से बचने और कॉन्फ़िगरेशन में काट-छांट करने की सुविधा मिलती है. इससे Python के विकल्पों में बदलाव होने पर, C++ टारगेट का फिर से विश्लेषण करने की ज़रूरत नहीं पड़ती.
यह ज़रूरी नहीं है कि किसी नियम का कॉन्फ़िगरेशन उसके "पैरंट" नियम जैसा ही हो. डिपेंडेंसी एज में कॉन्फ़िगरेशन को बदलने की प्रोसेस को "कॉन्फ़िगरेशन ट्रांज़िशन" कहा जाता है. ऐसा दो जगहों पर हो सकता है:
- डिपेंडेंसी किनारे पर. ये ट्रांज़िशन
Attribute.Builder.cfg()
में बताए गए हैं. येRule
(जहां ट्रांज़िशन होता है) औरBuildOptions
(ओरिजनल कॉन्फ़िगरेशन) से एक या एक से ज़्यादाBuildOptions
(आउटपुट कॉन्फ़िगरेशन) तक के फ़ंक्शन होते हैं. - कॉन्फ़िगर किए गए टारगेट के किसी भी इनकमिंग एज पर. इनकी जानकारी
RuleClass.Builder.cfg()
में दी गई है.
TransitionFactory
और ConfigurationTransition
कक्षाएं काम की हैं.
कॉन्फ़िगरेशन ट्रांज़िशन का इस्तेमाल इनके लिए किया जाता है:
- यह बताने के लिए कि किसी खास डिपेंडेंसी का इस्तेमाल बिल्ड के दौरान किया जाता है और इसलिए, इसे एक्सीक्यूशन आर्किटेक्चर में बनाया जाना चाहिए
- यह बताने के लिए कि किसी खास डिपेंडेंसी को कई आर्किटेक्चर के लिए बनाया जाना चाहिए. जैसे, फ़ैट Android APKs में नेटिव कोड के लिए
अगर कॉन्फ़िगरेशन ट्रांज़िशन के नतीजे में एक से ज़्यादा कॉन्फ़िगरेशन मिलते हैं, तो इसे स्प्लिट ट्रांज़िशन कहा जाता है.
कॉन्फ़िगरेशन ट्रांज़िशन को Starlark में भी लागू किया जा सकता है (दस्तावेज़ यहां)
ट्रांसिटिव जानकारी देने वाली कंपनियां
ट्रांज़िटिव जानकारी देने वाले टूल, कॉन्फ़िगर किए गए टारगेट के लिए एक तरीका (और _एकमात्र _तरीका) हैं. इनकी मदद से, कॉन्फ़िगर किए गए उन टारगेट के बारे में जानकारी हासिल की जा सकती है जिन पर वे निर्भर हैं. साथ ही, इनकी मदद से कॉन्फ़िगर किए गए उन टारगेट को अपने बारे में जानकारी दी जा सकती है जिन पर वे निर्भर हैं. इनके नाम में "ट्रांसीटिव" इसलिए है, क्योंकि आम तौर पर यह कॉन्फ़िगर किए गए टारगेट के ट्रांसीटिव क्लोज़र का एक तरह का रोल-अप होता है.
आम तौर पर, Java के ट्रांज़िशन की जानकारी देने वाले एपीआई और Starlark के ट्रांज़िशन की जानकारी देने वाले एपीआई के बीच 1:1 का अनुपात होता है. हालांकि, DefaultInfo
को छोड़कर, ऐसा सभी एपीआई के लिए नहीं होता. DefaultInfo
, FileProvider
, FilesToRunProvider
, और RunfilesProvider
का एक मिला-जुला एपीआई है. ऐसा इसलिए है, क्योंकि इस एपीआई को Java के एपीआई से सीधे ट्रांसलिटरेट करने के बजाय, Starlark के तौर पर ज़्यादा इस्तेमाल किया जाता है.
इनमें से कोई एक चीज़, इनकी कुंजी होती है:
- Java क्लास ऑब्जेक्ट. यह सुविधा सिर्फ़ उन सेवा देने वाली कंपनियों के लिए उपलब्ध है जिन्हें Starlark से ऐक्सेस नहीं किया जा सकता. ये सेवा देने वाली कंपनियां,
TransitiveInfoProvider
की सबक्लास होती हैं. - एक स्ट्रिंग. यह लेगसी तरीका है और इसका सुझाव नहीं दिया जाता. इसकी वजह यह है कि नामों में टकराव हो सकता है. ट्रांज़िटिव जानकारी देने वाली ऐसी कंपनियां,
build.lib.packages.Info
की डायरेक्ट सबक्लास होती हैं. - सेवा देने वाली कंपनी का सिंबल. इसे Starlark में
provider()
फ़ंक्शन का इस्तेमाल करके बनाया जा सकता है. यह नए प्रोवाइडर बनाने का सुझाया गया तरीका है. इस सिंबल को Java मेंProvider.Key
इंस्टेंस से दिखाया जाता है.
Java में लागू किए गए नए प्रोवाइडर, BuiltinProvider
का इस्तेमाल करके लागू किए जाने चाहिए.
NativeProvider
का इस्तेमाल नहीं किया जा सकता (हमने इसे अब तक हटाया नहीं है) और
TransitiveInfoProvider
सबक्लास को Starlark से ऐक्सेस नहीं किया जा सकता.
कॉन्फ़िगर किए गए टारगेट
कॉन्फ़िगर किए गए टारगेट, RuleConfiguredTargetFactory
के तौर पर लागू किए जाते हैं. Java में लागू किए गए हर नियम क्लास के लिए एक सबक्लास होता है. Starlark के ज़रिए कॉन्फ़िगर किए गए टारगेट, StarlarkRuleConfiguredTargetUtil.buildRule()
के ज़रिए बनाए जाते हैं.
कॉन्फ़िगर की गई टारगेट फ़ैक्ट्री को अपनी रिटर्न वैल्यू तय करने के लिए, RuleConfiguredTargetBuilder
का इस्तेमाल करना चाहिए. इसमें ये चीज़ें शामिल होती हैं:
- उनका
filesToBuild
, "इस नियम के तहत आने वाली फ़ाइलों के सेट" का धुंधला कॉन्सेप्ट. ये वे फ़ाइलें होती हैं जो तब बनाई जाती हैं, जब कॉन्फ़िगर किया गया टारगेट, कमांड लाइन पर या जेनरूल के सोर्स में होता है. - उनकी रनफ़ाइलें, नियमित, और डेटा.
- उनके आउटपुट ग्रुप. ये "फ़ाइलों के अन्य सेट" हैं, जिन्हें नियम से बनाया जा सकता है. इन्हें BUILD में filegroup नियम के output_group एट्रिब्यूट का इस्तेमाल करके और Java में
OutputGroupInfo
प्रोवाइडर का इस्तेमाल करके ऐक्सेस किया जा सकता है.
रनफ़ाइलें
कुछ बाइनरी को चलाने के लिए डेटा फ़ाइलों की ज़रूरत होती है. इसका एक उदाहरण, ऐसे टेस्ट हैं जिनमें इनपुट फ़ाइलों की ज़रूरत होती है. Bazel में इसे "रनफ़ाइल" के कॉन्सेप्ट से दिखाया जाता है. "रनफ़ाइल ट्री", किसी खास बाइनरी के लिए डेटा फ़ाइलों की डायरेक्ट्री ट्री होती है. इसे फ़ाइल सिस्टम में एक सिमलिंक ट्री के तौर पर बनाया जाता है. इसमें सोर्स या आउटपुट ट्री में मौजूद फ़ाइलों की ओर अलग-अलग सिमलिंक होते हैं.
रनफ़ाइल के सेट को Runfiles
इंस्टेंस के तौर पर दिखाया जाता है. यह सैद्धांतिक तौर पर रनफ़ाइल ट्री में फ़ाइल के पाथ से Artifact
इंस्टेंस तक का मैप होता है, जो इसे दिखाता है. यह दो वजहों से, एक Map
से थोड़ा ज़्यादा पेचीदा है:
- ज़्यादातर मामलों में, किसी फ़ाइल का runfiles पाथ और execpath एक ही होता है. हम इसका इस्तेमाल, कुछ रैम बचाने के लिए करते हैं.
- रनफ़ाइल ट्री में, लेगसी टाइप की कई एंट्री होती हैं. इन्हें भी दिखाना ज़रूरी है.
रनफ़ाइलों को RunfilesProvider
का इस्तेमाल करके इकट्ठा किया जाता है: इस क्लास का एक इंस्टेंस, कॉन्फ़िगर किए गए टारगेट (जैसे, लाइब्रेरी) और उसके ट्रांज़िशन क्लोज़र की ज़रूरतों की रनफ़ाइलों को दिखाता है. साथ ही, इन्हें नेस्ट किए गए सेट की तरह इकट्ठा किया जाता है. असल में, इन्हें नेस्ट किए गए सेट का इस्तेमाल करके लागू किया जाता है: हर टारगेट, अपनी डिपेंडेंसी की रनफ़ाइलों को जोड़ता है और कुछ अपनी रनफ़ाइलें जोड़ता है. इसके बाद, वह नतीजे वाले सेट को डिपेंडेंसी ग्राफ़ में ऊपर की ओर भेजता है. किसी RunfilesProvider
इंस्टेंस में दो Runfiles
इंस्टेंस होते हैं. पहला, "डेटा" एट्रिब्यूट के ज़रिए नियम पर निर्भर होने पर और दूसरा, आने वाली हर तरह की अन्य डिपेंडेंसी के लिए. ऐसा इसलिए होता है, क्योंकि डेटा एट्रिब्यूट के ज़रिए किसी टारगेट पर निर्भर होने पर, कभी-कभी अलग-अलग रनफ़ाइलें दिखती हैं. यह एक गड़बड़ी है, जिसे हम अब तक ठीक नहीं कर पाए हैं.
बाइनरी वाली रनफ़ाइल को RunfilesSupport
के इंस्टेंस के तौर पर दिखाया जाता है. यह
Runfiles
से अलग है, क्योंकि RunfilesSupport
के पास असल में बनाए जाने
की क्षमता है (Runfiles
के उलट, जो सिर्फ़ एक मैपिंग है). इसके लिए, इन अतिरिक्त कॉम्पोनेंट की ज़रूरत होती है:
- इनपुट रनफ़ाइल मेनिफ़ेस्ट. यह रनफ़ाइल ट्री का क्रम के मुताबिक ब्यौरा है. इसका इस्तेमाल रनफ़ाइल ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. साथ ही, Ba बैंक यह मानता है कि रनफ़ाइल ट्री में बदलाव सिर्फ़ तब ही होगा, जब मेनिफ़ेस्ट के कॉन्टेंट में बदलाव हुआ हो.
- आउटपुट रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल, रनटाइम लाइब्रेरी करती हैं, जो रनफ़ाइल ट्री को मैनेज करती हैं. खास तौर पर, Windows पर, जो कभी-कभी सिंबल लिंक के साथ काम नहीं करता.
- रनफ़ाइल मिडलमैन. रनफ़ाइल ट्री के मौजूद रहने के लिए, किसी व्यक्ति को सिमलिंक ट्री और वह आर्टफ़ैक्ट बनाना होता है, जिस पर वह सिमलिंक ले जाता है. डिपेंडेंसी एज की संख्या कम करने के लिए, इन सभी को दिखाने के लिए, रनफ़ाइल मिडलमैन का इस्तेमाल किया जा सकता है.
- उस बाइनरी को चलाने के लिए कमांड लाइन आर्ग्युमेंट जिसका इस्तेमाल करके,
RunfilesSupport
ऑब्जेक्ट का रनफ़ाइल बनाया जाता है.
आसपेक्ट
ऐसेट, "डिपेंडेंसी ग्राफ़ में कैलकुलेशन को नीचे तक भेजने" का एक तरीका है. Bazel का इस्तेमाल करने वाले लोगों के लिए, इनके बारे में यहां बताया गया है. ऐसा करने का एक अच्छा उदाहरण प्रोटोकॉल बफ़र है: proto_library
नियम को किसी खास भाषा के बारे में नहीं पता होना चाहिए, लेकिन किसी भी प्रोग्रामिंग भाषा में प्रोटोकॉल बफ़र मैसेज (प्रोटोकॉल बफ़र की "बेसिक यूनिट") को लागू करने की प्रोसेस proto_library
नियम के हिसाब से होनी चाहिए, ताकि अगर एक ही भाषा के दो टारगेट एक ही प्रोटोकॉल बफ़र पर निर्भर हों, तो उसे सिर्फ़ एक बार बनाया जाए.
कॉन्फ़िगर किए गए टारगेट की तरह ही, इन्हें Skyframe में SkyValue
के तौर पर दिखाया जाता है. साथ ही, इन्हें बनाने का तरीका भी कॉन्फ़िगर किए गए टारगेट बनाने के तरीके से काफ़ी मिलता-जुलता है: इनमें ConfiguredAspectFactory
नाम की फ़ैक्ट्री क्लास होती है, जिसके पास RuleContext
का ऐक्सेस होता है. हालांकि, कॉन्फ़िगर किए गए टारगेट फ़ैक्ट्री के उलट, यह उस कॉन्फ़िगर किए गए टारगेट और उसके प्रोवाइडर के बारे में भी जानती है जिससे यह जुड़ी होती है.
डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजे गए आसपेक्ट का सेट, Attribute.Builder.aspects()
फ़ंक्शन का इस्तेमाल करके हर एट्रिब्यूट के लिए तय किया जाता है. इस प्रोसेस में हिस्सा लेने वाली कुछ क्लास के नाम भ्रमित करने वाले हैं:
AspectClass
, इस एस्पेक्ट को लागू करने का तरीका है. यह Java (इस मामले में यह एक सबक्लास है) या Starlark (इस मामले में यहStarlarkAspectClass
का एक इंस्टेंस है) में हो सकता है. यहRuleConfiguredTargetFactory
के जैसा ही है.AspectDefinition
, एस्पेक्ट की परिभाषा है. इसमें, ज़रूरी सेवा देने वाली कंपनियां और सेवा देने वाली कंपनियां शामिल होती हैं. साथ ही, इसमें लागू करने का रेफ़रंस भी होता है, जैसे कि सहीAspectClass
इंस्टेंस. यहRuleClass
के जैसा है.AspectParameters
, डिपेंडेंसी ग्राफ़ में नीचे की ओर प्रोपैगेट किए जाने वाले किसी पहलू को पैरामीटर करने का एक तरीका है. फ़िलहाल, यह स्ट्रिंग से स्ट्रिंग का मैप है. प्रोटोकॉल बफ़र के काम के होने का एक अच्छा उदाहरण: अगर किसी भाषा में एक से ज़्यादा एपीआई हैं, तो यह जानकारी कि प्रोटोकॉल बफ़र किस एपीआई के लिए बनाए जाने चाहिए, उसे डिपेंडेंसी ग्राफ़ में भेजा जाना चाहिए.Aspect
उस डेटा को दिखाता है जो डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजे जाने वाले किसी पहलू का हिसाब लगाने के लिए ज़रूरी है. इसमें आसपेक्ट क्लास, उसकी परिभाषा, और उसके पैरामीटर शामिल होते हैं.RuleAspect
एक ऐसा फ़ंक्शन है जो यह तय करता है कि किसी खास नियम के किन पहलुओं को प्रॉपगेट करना चाहिए. यहRule
->Aspect
फ़ंक्शन है.
एक समस्या यह है कि एस्पेक्ट, दूसरे एस्पेक्ट से जुड़े हो सकते हैं. उदाहरण के लिए, किसी Java IDE के क्लासपाथ को इकट्ठा करने वाले एस्पेक्ट को क्लासपाथ पर मौजूद सभी .jar फ़ाइलों के बारे में जानना होगा. हालांकि, उनमें से कुछ प्रोटोकॉल बफ़र हैं. ऐसे में, IDE का ऐस्पेक्ट, (proto_library
नियम + Java प्रोटो ऐस्पेक्ट) पेयर से जुड़ना चाहेगा.
अलग-अलग पहलुओं की जटिलता को क्लास
AspectCollection
में कैप्चर किया जाता है.
प्लैटफ़ॉर्म और टूलचेन
Bazel, एक से ज़्यादा प्लैटफ़ॉर्म के लिए बने बिल्ड के साथ काम करता है. इसका मतलब है कि ऐसे बिल्ड जिनमें एक से ज़्यादा आर्किटेक्चर हो सकते हैं, जहां बिल्ड ऐक्शन चलते हैं, और एक से ज़्यादा आर्किटेक्चर जिनके लिए कोड बनाया जाता है. इन आर्किटेक्चर को बेज़ेल पार्सल में प्लैटफ़ॉर्म कहा जाता है. (पूरा दस्तावेज़ यहां) दिया गया है
किसी प्लैटफ़ॉर्म के बारे में, सीमा सेटिंग (जैसे, "सीपीयू आर्किटेक्चर" का कॉन्सेप्ट) से सीमा की वैल्यू (जैसे, x86_64 जैसा कोई सीपीयू) तक की की-वैल्यू मैपिंग से बताया जाता है. हमारे पास @platforms
रिपॉज़िटरी में, सबसे ज़्यादा इस्तेमाल की जाने वाली पाबंदी की सेटिंग और वैल्यू की "डिक्शनरी" है.
टूलचेन का कॉन्सेप्ट इस बात पर आधारित है कि कौनसे प्लैटफ़ॉर्म पर बिल्ड चल रहा है और कौनसे प्लैटफ़ॉर्म टारगेट किए जा रहे हैं. इसके आधार पर, आपको अलग-अलग कंपाइलर का इस्तेमाल करना पड़ सकता है. उदाहरण के लिए, कोई खास C++ टूलचेन किसी खास ओएस पर चल सकता है और कुछ अन्य ओएस को टारगेट कर सकता है. Bazel को यह तय करना होगा कि सेट किए गए एक्सीक्यूशन और टारगेट प्लैटफ़ॉर्म के आधार पर, C++ के किस कंपाइलर का इस्तेमाल किया जाए. टूलचेन के दस्तावेज़ यहां देखे जा सकते हैं.
ऐसा करने के लिए, टूलचेन को एक्ज़ीक्यूशन के सेट और उनके साथ काम करने वाले टारगेट प्लैटफ़ॉर्म की कंस्ट्रेंट के साथ एनोटेट किया जाता है. ऐसा करने के लिए, टूलचेन की परिभाषा को दो हिस्सों में बांटा गया है:
toolchain()
नियम, जो किसी टूलचेन के साथ काम करने वाले एक्ज़ीक्यूशन और टारगेट की सीमाओं के सेट के बारे में बताता है. साथ ही, यह भी बताता है कि यह किस तरह का टूलचेन है, जैसे कि C++ या Java. टूलचेन के टाइप के बारे मेंtoolchain_type()
नियम से पता चलता है- भाषा के हिसाब से बना नियम, जिसमें असल टूलचेन के बारे में बताया गया हो (जैसे कि
cc_toolchain()
)
ऐसा इसलिए किया जाता है, क्योंकि हमें टूलचेन रिज़ॉल्यूशन और किसी खास भाषा के हिसाब से काम करने के लिए, हर टूलचेन की सीमाओं के बारे में जानना होता है.
*_toolchain()
नियमों में इससे ज़्यादा जानकारी होती है, इसलिए उन्हें लोड होने में ज़्यादा समय लगता है.
एक्सीक्यूशन प्लैटफ़ॉर्म को इनमें से किसी एक तरीके से तय किया जाता है:
- MODULE.bazel फ़ाइल में,
register_execution_platforms()
फ़ंक्शन का इस्तेमाल करके - --extra_execution_platforms कमांड लाइन विकल्प का इस्तेमाल करके कमांड लाइन पर
उपलब्ध एक्सीक्यूशन प्लैटफ़ॉर्म का सेट, RegisteredExecutionPlatformsFunction
में कैलकुलेट किया जाता है.
कॉन्फ़िगर किए गए टारगेट के लिए टारगेट प्लैटफ़ॉर्म,
PlatformOptions.computeTargetPlatform()
से तय होता है . यह प्लैटफ़ॉर्म की सूची है, क्योंकि हम एक से ज़्यादा टारगेट प्लैटफ़ॉर्म के साथ काम करना चाहते हैं. हालांकि, फ़िलहाल इसे लागू नहीं किया गया है.
कॉन्फ़िगर किए गए टारगेट के लिए इस्तेमाल किए जाने वाले टूलचेन का सेट, ToolchainResolutionFunction
से तय होता है. यह इनका फ़ंक्शन है:
- रजिस्टर किए गए टूलचेन का सेट (MODULE.bazel फ़ाइल और कॉन्फ़िगरेशन में)
- कॉन्फ़िगरेशन में, पसंद के मुताबिक एक्ज़ीक्यूशन और टारगेट प्लैटफ़ॉर्म
- कॉन्फ़िगर किए गए टारगेट के लिए ज़रूरी टूलचैन टाइप का सेट (
UnloadedToolchainContextKey)
में UnloadedToolchainContextKey
में, कॉन्फ़िगर किए गए टारगेट (exec_compatible_with
एट्रिब्यूट) और कॉन्फ़िगरेशन (--experimental_add_exec_constraints_to_targets
) के लिए, प्लैटफ़ॉर्म पर लागू होने वाली पाबंदियों का सेट
इसका नतीजा UnloadedToolchainContext
होता है. यह असल में, टूलचेन टाइप (ToolchainTypeInfo
इंस्टेंस के तौर पर दिखाया गया) से चुने गए टूलचेन के लेबल तक का मैप होता है. इसे "अनलोड किया गया" इसलिए कहा जाता है, क्योंकि इसमें टूलचेन नहीं होते, सिर्फ़ उनके लेबल होते हैं.
इसके बाद, टूलचेन ResolvedToolchainContext.load()
का इस्तेमाल करके लोड किए जाते हैं और कॉन्फ़िगर किए गए उस टारगेट के लागू होने पर इस्तेमाल किए जाते हैं जिसने उनका अनुरोध किया था.
हमारे पास एक लेगसी सिस्टम भी है जो एक ही "होस्ट" कॉन्फ़िगरेशन और टारगेट कॉन्फ़िगरेशन पर निर्भर करता है. यह कॉन्फ़िगरेशन अलग-अलग कॉन्फ़िगरेशन के फ़्लैग की मदद से दिखाया जाता है, जैसे कि --cpu
. हम धीरे-धीरे ऊपर बताए गए सिस्टम पर स्विच कर रहे हैं. ऐसे मामलों को हैंडल करने के लिए जहां लोग लेगसी कॉन्फ़िगरेशन वैल्यू पर भरोसा करते हैं, हमने प्लैटफ़ॉर्म मैपिंग लागू की है, ताकि लेगसी फ़्लैग और नए स्टाइल के प्लैटफ़ॉर्म की पाबंदियों के बीच अनुवाद किया जा सके.
उनका कोड PlatformMappingFunction
में है और इसमें Starlark के बजाय किसी दूसरी "लिटल लैंग्वेज" का इस्तेमाल किया गया है.
कंस्ट्रेंट
कभी-कभी किसी टारगेट को सिर्फ़ कुछ प्लैटफ़ॉर्म के साथ काम करने वाला तय करना होता है. माफ़ करें, Baज़र के पास इस मकसद को पूरा करने के लिए कई तरीके हैं:
- नियम के हिसाब से पाबंदियां
environment_group()
/environment()
- प्लैटफ़ॉर्म के कंट्रोल
नियम के हिसाब से पाबंदियों का इस्तेमाल, ज़्यादातर Google में Java नियमों के लिए किया जाता है. ये पाबंदियां अब बंद होने वाली हैं और ये Bazel में उपलब्ध नहीं हैं. हालांकि, सोर्स कोड में इनका रेफ़रंस हो सकता है. इसे कंट्रोल करने वाले एट्रिब्यूट को
constraints=
कहा जाता है.
environment_group() और environment()
ये नियम, लेगसी सिस्टम के तहत काम करते हैं और इनका ज़्यादातर इस्तेमाल नहीं किया जाता.
सभी बिल्ड रूल से यह बताया जा सकता है कि उन्हें किस "एनवायरमेंट" के लिए बनाया जा सकता है और जहां "एनवायरमेंट", environment()
नियम का एक इंस्टेंस है.
किसी नियम के लिए, इस्तेमाल किए जा सकने वाले एनवायरमेंट की जानकारी देने के कई तरीके हैं:
restricted_to=
एट्रिब्यूट की मदद से. यह जानकारी देने का सबसे सीधा तरीका है. इसमें उन सभी एनवायरमेंट के बारे में बताया जाता है जिन पर नियम काम करता है.compatible_with=
एट्रिब्यूट की मदद से. इससे उन एनवायरमेंट के बारे में पता चलता है जिन पर नियम काम करता है. इनमें, डिफ़ॉल्ट रूप से काम करने वाले "स्टैंडर्ड" एनवायरमेंट भी शामिल हैं.- पैकेज-लेवल एट्रिब्यूट
default_restricted_to=
औरdefault_compatible_with=
की मदद से. environment_group()
नियमों में डिफ़ॉल्ट स्पेसिफ़िकेशन के ज़रिए. हर एनवायरमेंट, विषय के हिसाब से मिलते-जुलते पीयर के ग्रुप से जुड़ा होता है. जैसे, "सीपीयू आर्किटेक्चर", "JDK वर्शन" या "मोबाइल ऑपरेटिंग सिस्टम". किसी एनवायरमेंट ग्रुप की परिभाषा में यह शामिल होता है कि इनमें से किस एनवायरमेंट के लिए "डिफ़ॉल्ट" सेट किया जाना चाहिए. ऐसा तब होता है, जबrestricted_to=
/environment()
एट्रिब्यूट के ज़रिए कोई अन्य जानकारी न दी गई हो. बिना एट्रिब्यूट वाले नियम के सभी डिफ़ॉल्ट एट्रिब्यूट इनहेरिट किए जाते हैं.- नियम क्लास के डिफ़ॉल्ट तौर पर. इससे, दिए गए नियम की क्लास के सभी उदाहरणों के लिए, ग्लोबल डिफ़ॉल्ट बदल जाते हैं. उदाहरण के लिए, इसका इस्तेमाल करके सभी
*_test
नियमों की जांच की जा सकती है. इसके लिए, हर इंस्टेंस को इस सुविधा के बारे में साफ़ तौर पर बताने की ज़रूरत नहीं होती.
environment()
को सामान्य नियम के तौर पर लागू किया जाता है, जबकि environment_group()
, Target
का सबक्लास है. हालांकि, यह Rule
(EnvironmentGroup
) का सबक्लास नहीं है. साथ ही, यह Starlark (StarlarkLibrary.environmentGroup()
) में डिफ़ॉल्ट रूप से उपलब्ध एक फ़ंक्शन है, जो आखिर में एक ही नाम वाला टारगेट बनाता है. ऐसा इसलिए किया जाता है, ताकि एक-दूसरे पर निर्भरता की समस्या न हो. यह समस्या इसलिए होती है, क्योंकि हर एनवायरमेंट को यह बताना होता है कि वह किस एनवायरमेंट ग्रुप से जुड़ा है. साथ ही, हर एनवायरमेंट ग्रुप को अपने डिफ़ॉल्ट एनवायरमेंट के बारे में बताना होता है.
--target_environment
कमांड लाइन विकल्प की मदद से, किसी बिल्ड को
किसी चुनिंदा एनवायरमेंट में प्रतिबंधित किया जा सकता है.
कंस्ट्रेंट जांच को लागू करने की प्रोसेस, RuleContextConstraintSemantics
और TopLevelConstraintSemantics
में की गई है.
प्लैटफ़ॉर्म से जुड़ी पाबंदियां
यह बताने का मौजूदा "आधिकारिक" तरीका, उन प्लैटफ़ॉर्म के साथ काम करता है जिनका इस्तेमाल टूलचेन और प्लैटफ़ॉर्म के लिए किया जाता है. इसे पुल करने के अनुरोध #10945 में लागू किया गया था.
किसको दिखे
अगर आपको Google जैसे बड़े डेवलपर के साथ बड़े कोडबेस पर काम करना है, तो आपको यह ध्यान रखना होगा कि कोई भी व्यक्ति आपके कोड पर अपनी मर्ज़ी से काम न कर पाए. अगर ऐसा नहीं है, तो हाइरम के कानून के मुताबिक, लोग उन व्यवहारों पर भरोसा करेंगे जिन्हें आपने लागू करने से जुड़ी जानकारी माना है.
Bazel, किसको दिखे नाम के मकैनिजम की मदद से ऐसा करता है: किसको दिखे एट्रिब्यूट का इस्तेमाल करके, यह तय किया जा सकता है कि कौनसे टारगेट किसी खास टारगेट पर निर्भर कर सकते हैं. यह एट्रिब्यूट कुछ खास होता है, क्योंकि इसमें लेबल की एक सूची होती है. हालांकि, ये लेबल किसी खास टारगेट के पॉइंटर के बजाय, पैकेज के नामों पर पैटर्न बना सकते हैं. (हां, यह डिज़ाइन में गड़बड़ी है.)
इसे इन जगहों पर लागू किया जाता है:
RuleVisibility
इंटरफ़ेस, कॉन्टेंट दिखने की जानकारी दिखाता है. यह एक कॉन्स्टेंट (पूरी तरह से सार्वजनिक या पूरी तरह से निजी) या लेबल की सूची हो सकती है.- लेबल, पैकेज ग्रुप (पैकेज की पहले से तय सूची), पैकेज (
//pkg:__pkg__
) या पैकेज के सबसे छोटे उपवृक्ष (//pkg:__subpackages__
) में से किसी एक का रेफ़रंस दे सकते हैं. यह कमांड-लाइन सिंटैक्स से अलग है, जिसमें//pkg:*
या//pkg/...
का इस्तेमाल किया जाता है. - पैकेज ग्रुप को अपने टारगेट (
PackageGroup
) और कॉन्फ़िगर किए गए टारगेट (PackageGroupConfiguredTarget
) के तौर पर लागू किया जाता है. अगर हम चाहें, तो इनके बजाय आसान नियमों का इस्तेमाल किया जा सकता है. इनका लॉजिक इनकी मदद से लागू किया जाता है:PackageSpecification
, जो//pkg/...
जैसे किसी एक पैटर्न से जुड़ा होता है;PackageGroupContents
, जो किसी एकpackage_group
केpackages
एट्रिब्यूट से जुड़ा होता है; औरPackageSpecificationProvider
, जोpackage_group
और उसके ट्रांसिशन एट्रिब्यूटincludes
पर एग्रीगेट करता है. - 'किसको दिखे' लेबल वाली सूचियों को डिपेंडेंसी से डिपेंडेंसी में तब बदलाव किया जाता है, जब आप
DependencyResolver.visitTargetVisibility
और कई दूसरी अलग-अलग जगहों पर हों. - असल जांच
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
में की जाती है
नेस्ट किए गए सेट
आम तौर पर, कॉन्फ़िगर किया गया टारगेट, अपनी डिपेंडेंसी से फ़ाइलों का एक सेट इकट्ठा करता है, अपनी फ़ाइलें जोड़ता है, और एग्रीगेट किए गए सेट को ट्रांज़िशनरी जानकारी देने वाले प्रोवाइडर में रैप करता है, ताकि उस पर निर्भर कॉन्फ़िगर किए गए टारगेट भी ऐसा कर सकें. उदाहरण:
- बिल्ड के लिए इस्तेमाल की जाने वाली C++ हेडर फ़ाइलें
- ऑब्जेक्ट फ़ाइलें, जो
cc_library
के ट्रांसिशन क्लोज़र को दिखाती हैं - .jar फ़ाइलों का सेट, जो किसी Java नियम को संकलित या चलाने के लिए क्लासपाथ पर होना चाहिए
- Python नियम के ट्रांसीटिव क्लोज़र में मौजूद Python फ़ाइलों का सेट
अगर हमने List
या Set
का इस्तेमाल करके यह आसान तरीका अपनाया है, तो मेमोरी के क्वाड्रेटिक इस्तेमाल की संख्या बढ़ जाएगी: अगर N नियमों की एक चेन है और हर नियम फ़ाइल जोड़ता है, तो कलेक्शन में 1+2+...+N सदस्य जुड़ जाएंगे.
इस समस्या से निपटने के लिए, हमने NestedSet
का कॉन्सेप्ट लिया है. यह एक ऐसा डेटा स्ट्रक्चर है जो अन्य NestedSet
के इंस्टेंस और अपने कुछ सदस्यों से बना होता है. इससे सेट का डायरेक्टेड ऐसाइक्लिक ग्राफ़ बनता है. इनमें बदलाव नहीं किया जा सकता और इनके सदस्यों को बार-बार दोहराया जा सकता है. हम एक से ज़्यादा इटरेशन ऑर्डर (NestedSet.Order
) के बारे में बताते हैं: पहले से ऑर्डर करना, पोस्टऑर्डर, टोपोलॉजिकल
(एक नोड हमेशा अपने पहले वाले लोगों के बाद आता है) और "ध्यान न दें, लेकिन यह हर बार एक ही होना चाहिए".
स्टारलार्क में इसी डेटा स्ट्रक्चर को depset
कहा जाता है.
आर्टफ़ैक्ट और कार्रवाइयां
असल बिल्ड में कमांड का एक सेट होता है, जिसे उपयोगकर्ता की पसंद का आउटपुट देने के लिए चलाया जाना चाहिए. निर्देशों को क्लास Action
के इंस्टेंस के तौर पर दिखाया जाता है और फ़ाइलों को क्लास Artifact
के इंस्टेंस के तौर पर दिखाया जाता है. इन्हें दो हिस्सों में बांटा गया है. साथ ही, इनमें निर्देश और असाइकलिक ग्राफ़ होते हैं. इन्हें "ऐक्शन ग्राफ़" कहा जाता है.
आर्टफ़ैक्ट दो तरह के होते हैं: सोर्स आर्टफ़ैक्ट (वे आर्टफ़ैक्ट जो Bazel के शुरू होने से पहले उपलब्ध होते हैं) और डेरिव्ड आर्टफ़ैक्ट (वे आर्टफ़ैक्ट जिन्हें बनाना ज़रूरी होता है). डेरिव्ड आर्टफ़ैक्ट कई तरह के हो सकते हैं:
- **सामान्य आर्टफ़ैक्ट. **इनके चेकसम का हिसाब लगाकर, इन्हें अप-टू-डेट रखने की जांच की जाती है. इसके लिए, शॉर्टकट के तौर पर mtime का इस्तेमाल किया जाता है. अगर फ़ाइल का समय नहीं बदलता है, तो हम फ़ाइल को चेकसम नहीं करते.
- समाधान नहीं किए गए सिंबललिंक आर्टफ़ैक्ट. readlink() को कॉल करके, इनके अप-टू-डेट होने की जांच की जाती है. सामान्य आर्टफ़ैक्ट के मुकाबले, ये डैंगलिंग स्लिंक्स हो सकते हैं. आम तौर पर, इसका इस्तेमाल उन मामलों में किया जाता है जहां कुछ फ़ाइलों को किसी तरह के संग्रह में पैक किया जाता है.
- ट्री आर्टफ़ैक्ट. ये एक फ़ाइल नहीं, बल्कि डायरेक्ट्री ट्री हैं. इनकी जांच करके यह पता लगाया जाता है कि वे अप-टू-डेट हैं या नहीं. इसके लिए, इनमें मौजूद फ़ाइलों और उनके कॉन्टेंट की जांच की जाती है. इन्हें
TreeArtifact
के तौर पर दिखाया जाता है. - मेटाडेटा के लगातार आर्टफ़ैक्ट. इन आर्टफ़ैक्ट में बदलाव करने पर, फिर से बनाने की प्रोसेस ट्रिगर नहीं होती. इसका इस्तेमाल सिर्फ़ बिल्ड स्टैंप की जानकारी के लिए किया जाता है: हम सिर्फ़ इसलिए फिर से बिल्ड नहीं करना चाहते, क्योंकि मौजूदा समय बदल गया है.
सोर्स आर्टफ़ैक्ट, ट्री आर्टफ़ैक्ट या हल नहीं किए गए सिंबललिंक आर्टफ़ैक्ट क्यों नहीं हो सकते, इसकी कोई बुनियादी वजह नहीं है. ऐसा इसलिए है, क्योंकि हमने इसे अब तक लागू नहीं किया है. हालांकि, हमें इसे लागू करना चाहिए -- BUILD
फ़ाइल में सोर्स डायरेक्ट्री का रेफ़रंस देना, Bazel की कुछ ऐसी गड़बड़ियों में से एक है जो लंबे समय से मौजूद हैं. हमारे पास इस तरह के काम करने वाला एक लागू तरीका है, जिसे BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM प्रॉपर्टी से चालू किया जाता है
Artifact
के एक खास तरह के उदाहरण हैं, मध्यस्थ. इन्हें Artifact
इंस्टेंस से दिखाया जाता है, जो MiddlemanAction
के आउटपुट होते हैं. इनका इस्तेमाल एक खास मामले में किया जाता है:
- Runfiles मिडलमैन का इस्तेमाल, Runfiles ट्री की मौजूदगी की पुष्टि करने के लिए किया जाता है, ताकि किसी को आउटपुट मेनिफ़ेस्ट और Runfiles ट्री के रेफ़रंस वाले हर आर्टफ़ैक्ट पर अलग से निर्भर न होना पड़े.
ऐक्शन को सबसे अच्छी तरह से एक ऐसे निर्देश के तौर पर समझा जा सकता है जिसे चलाने के लिए, एक खास तरह के एनवायरमेंट की ज़रूरत होती है. साथ ही, यह भी ज़रूरी है कि यह निर्देश, एक खास तरह के आउटपुट जनरेट करता हो. किसी कार्रवाई की जानकारी के मुख्य कॉम्पोनेंट, यहां दी गई हैं:
- वह कमांड लाइन जिसे चलाना है
- इसके लिए ज़रूरी इनपुट आर्टफ़ैक्ट
- वे एनवायरमेंट वैरिएबल जिन्हें सेट करने की ज़रूरत है
- ऐसी व्याख्या जो आस-पास के माहौल (जैसे कि प्लैटफ़ॉर्म) के बारे में बताती है, जहां इसे चलाना ज़रूरी है \
कुछ और खास मामले भी हैं, जैसे कि ऐसी फ़ाइल लिखना जिसका कॉन्टेंट, Bazel को पता हो. ये AbstractAction
की सब-क्लास हैं. ज़्यादातर कार्रवाइयां SpawnAction
या StarlarkAction
होती हैं (यही भी होती है कि ये अलग-अलग क्लास नहीं होनी चाहिए). हालांकि, Java और C++ के ऐक्शन टाइप (JavaCompileAction
, CppCompileAction
, और CppLinkAction
) हैं.
हम आखिर में सभी चीज़ों को SpawnAction
पर ले जाना चाहते हैं; JavaCompileAction
काफ़ी करीब है, लेकिन .d फ़ाइल को पार्स करने और शामिल करने की स्कैनिंग की वजह से, C++ थोड़ा खास मामला है.
ऐक्शन ग्राफ़ को ज़्यादातर Skyframe ग्राफ़ में "एम्बेड" किया जाता है: कॉन्सेप्ट के हिसाब से, किसी ऐक्शन को लागू करने को ActionExecutionFunction
को कॉल करने के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से Skyframe डिपेंडेंसी एज की मैपिंग के बारे में ActionExecutionFunction.getInputDeps()
और Artifact.key()
में बताया गया है. इसमें Skyframe एज की संख्या कम रखने के लिए, कुछ ऑप्टिमाइज़ेशन किए गए हैं:
- हासिल किए गए आर्टफ़ैक्ट के अपने
SkyValue
नहीं होते हैं. इसके बजाय,Artifact.getGeneratingActionKey()
का इस्तेमाल, उसे जनरेट करने वाले ऐक्शन की कुंजी का पता लगाने के लिए किया जाता है - नेस्ट किए गए सेट की अपनी Skyframe कुंजी होती है.
शेयर की गई कार्रवाइयां
कुछ कार्रवाइयां, कॉन्फ़िगर किए गए कई टारगेट से जनरेट होती हैं. Starlark नियमों के दायरे में ज़्यादा कार्रवाइयां नहीं आती हैं, क्योंकि उन्हें सिर्फ़ अपने कॉन्फ़िगरेशन और पैकेज के हिसाब से तय की गई डायरेक्ट्री में, डेरिव्ड ऐक्शन डालने की अनुमति होती है. हालांकि, एक ही पैकेज में मौजूद नियमों में अंतर हो सकता है. वहीं, Java में लागू किए गए नियमों से, डेरिव्ड आर्टफ़ैक्ट को कहीं भी डाला जा सकता है.
इसे गड़बड़ी माना जाता है, लेकिन इसे हटाना काफ़ी मुश्किल है. ऐसा इसलिए, क्योंकि इससे प्रोसेस करने में लगने वाले समय में काफ़ी बचत होती है. उदाहरण के लिए, जब किसी सोर्स फ़ाइल को किसी तरह से प्रोसेस करना होता है और उस फ़ाइल का रेफ़रंस कई नियमों (हैंडवाइब-हैंडवाइब) से मिलता है. हालांकि, इसके लिए कुछ रैम की ज़रूरत होती है: शेयर की गई कार्रवाई के हर उदाहरण को मेमोरी में अलग से सेव करना पड़ता है.
अगर दो कार्रवाइयां एक ही आउटपुट फ़ाइल जनरेट करती हैं, तो वे एक जैसी होनी चाहिए:
उनमें एक जैसे इनपुट, एक जैसे आउटपुट होने चाहिए और एक ही कमांड लाइन को चलाना चाहिए. यह समानता संबंध Actions.canBeShared()
में लागू किया जाता है और हर कार्रवाई को देखकर, विश्लेषण और निष्पादन के चरणों के बीच इसकी पुष्टि की जाती है.
इसे SkyframeActionExecutor.findAndStoreArtifactConflicts()
में लागू किया गया है और यह Bazel की उन कुछ जगहों में से एक है जहां बिल्ड के "ग्लोबल" व्यू की ज़रूरत होती है.
एक्ज़ीक्यूशन का चरण
इसके बाद, Bazel असल में बिल्ड ऐक्शन चलाना शुरू करता है. जैसे, आउटपुट देने वाले कमांड.
विश्लेषण के बाद, Bazel सबसे पहले यह तय करता है कि कौनसे आर्टफ़ैक्ट बनाने हैं. इसके लिए लॉजिक, TopLevelArtifactHelper
में कोड में बदला गया है. यह कमांड लाइन पर कॉन्फ़िगर किए गए टारगेट का filesToBuild
और खास आउटपुट ग्रुप का कॉन्टेंट है. इसका मकसद, "अगर यह टारगेट कमांड लाइन पर है, तो इन आर्टफ़ैक्ट को बनाएं" को साफ़ तौर पर बताना है.
अगला चरण, एक्सीक्यूशन रूट बनाना है. Bazel के पास फ़ाइल सिस्टम (--package_path
) में मौजूद अलग-अलग जगहों से सोर्स पैकेज पढ़ने का विकल्प होता है. इसलिए, इसे पूरे सोर्स ट्री के साथ, लोकल तौर पर की जाने वाली कार्रवाइयां उपलब्ध करानी होती हैं. इसे क्लास SymlinkForest
मैनेज करता है. यह विश्लेषण के फ़ेज़ में इस्तेमाल किए गए हर टारगेट को ध्यान में रखकर काम करता है. साथ ही, एक डायरेक्ट्री ट्री बनाता है, जो हर पैकेज को इस्तेमाल किए गए टारगेट के साथ उसकी असल जगह से लिंक करता है. इसका दूसरा विकल्प यह है कि आप निर्देशों के लिए सही पाथ भेजें (--package_path
को ध्यान में रखते हुए).
ऐसा नहीं करना चाहिए, क्योंकि:
- जब किसी पैकेज को पैकेज पाथ की एक एंट्री से दूसरी एंट्री में ले जाया जाता है, तो यह ऐक्शन कमांड लाइन बदल देता है. आम तौर पर, ऐसा होता रहता है
- अगर कोई कार्रवाई, स्थानीय तौर पर की जाती है, तो इससे अलग कमांड लाइन बनती हैं. हालांकि, अगर कार्रवाई को रिमोट तौर पर किया जाता है, तो इससे अलग कमांड लाइन बनती हैं
- इसके लिए, इस्तेमाल किए जा रहे टूल के हिसाब से कमांड लाइन ट्रांसफ़ॉर्मेशन की ज़रूरत होती है (जैसे, Java क्लासपाथ और C++ शामिल पाथ के बीच का अंतर)
- किसी ऐक्शन की कमांड लाइन बदलने पर, उसकी ऐक्शन कैश मेमोरी एंट्री अमान्य हो जाती है
--package_path
के इस्तेमाल पर धीरे-धीरे रोक लगाई जा रही है
इसके बाद, Bazel ऐक्शन ग्राफ़ (ऐक्शन और उनके इनपुट और आउटपुट आर्टफ़ैक्ट से बना, दो हिस्सों वाला डायरेक्टेड ग्राफ़) और ऐक्शन चलाना शुरू करता है.
हर कार्रवाई को SkyValue
क्लास ActionExecutionValue
के इंस्टेंस से दिखाया जाता है.
कोई कार्रवाई करने में ज़्यादा समय लगता है. इसलिए, हमारे पास कैश मेमोरी से जुड़ी कुछ लेयर हैं, जिन्हें Skyframe के पीछे से हिट किया जा सकता है:
ActionExecutionFunction.stateMap
मेंActionExecutionFunction
के Skyframe को फिर से शुरू करने की लागत कम करने के लिए डेटा शामिल है- लोकल ऐक्शन कैश मेमोरी में, फ़ाइल सिस्टम की स्थिति के बारे में डेटा मौजूद होता है
- रिमोट एक्ज़िक्यूशन सिस्टम में आम तौर पर खुद की कैश मेमोरी भी होती है
लोकल ऐक्शन कैश मेमोरी
यह कैश मेमोरी एक और लेयर है, जो Skyframe में मौजूद है. अगर Skyframe में कोई कार्रवाई फिर से की जाती है, तब भी वह लोकल ऐक्शन कैश में हिट हो सकती है. यह लोकल फ़ाइल सिस्टम की स्थिति दिखाता है और यह डिस्क में सीरियल होता है. इसका मतलब है कि जब कोई नया बेज़ल सर्वर शुरू करता है, तो स्काईफ़्रेम ग्राफ़ खाली होने पर भी उसे लोकल ऐक्शन कैश हिट मिल सकते हैं.
इस कैश मेमोरी की जांच, ActionCacheChecker.getTokenIfNeedToExecute()
तरीके का इस्तेमाल करके हिट के लिए की जाती है .
नाम के उलट, यह मैप, डेरिव्ड आर्टफ़ैक्ट के पाथ से उस ऐक्शन तक का मैप होता है जिसने उसे उत्सर्जित किया है. इस कार्रवाई का ब्यौरा है:
- इनपुट और आउटपुट फ़ाइलों का सेट और उनका चेकसम
- इसकी "ऐक्शन बटन", आम तौर पर वह कमांड लाइन होती है जिसे चलाया गया था. हालांकि, आम तौर पर यह उन सभी चीज़ों को दिखाता है जिन्हें इनपुट फ़ाइलों के चेकसम से कैप्चर नहीं किया गया है. जैसे,
FileWriteAction
के लिए, यह लिखे गए डेटा का चेकसम है
एक और "टॉप-डाउन ऐक्शन कैश" है, जो अभी तक डेवलपमेंट के तहत है. यह कैश मेमोरी में कई बार जाने से बचने के लिए, ट्रांज़िशन हैश का इस्तेमाल करता है.
इनपुट की खोज और इनपुट को छोटा करना
कुछ कार्रवाइयां सिर्फ़ इनपुट का सेट रखने से ज़्यादा मुश्किल होती हैं. किसी कार्रवाई के इनपुट के सेट में बदलाव दो तरह के होते हैं:
- कोई कार्रवाई, लागू होने से पहले नए इनपुट ढूंढ सकती है या यह तय कर सकती है कि उसके कुछ इनपुट ज़रूरी नहीं हैं. C++ का उदाहरण लें, जहां यह अनुमान लगाना बेहतर होता है कि C++ फ़ाइल, ट्रांज़िटिव क्लोज़र से कौनसी हेडर फ़ाइलों का इस्तेमाल करती है, ताकि हमें हर फ़ाइल को रिमोट एक्सीक्यूटर को भेजने की ज़रूरत न पड़े. इसलिए, हमारे पास हर हेडर फ़ाइल को "इनपुट" के तौर पर रजिस्टर न करने का विकल्प है. हालांकि, ट्रांज़िटिव तौर पर शामिल किए गए हेडर के लिए सोर्स फ़ाइल को स्कैन करें और सिर्फ़ उन हेडर फ़ाइलों को इनपुट के तौर पर मार्क करें जिनके बारे में
#include
स्टेटमेंट में बताया गया है. हम ज़्यादा अनुमान लगाते हैं, ताकि हमें C प्रोसेसर्वर को पूरी तरह से लागू करने की ज़रूरत न पड़े. फ़िलहाल, यह विकल्प Bazel में "गलत" के तौर पर हार्ड-वाइर्ड है और इसका इस्तेमाल सिर्फ़ Google में किया जाता है. - किसी कार्रवाई को पूरा करने के दौरान, हो सकता है कि कुछ फ़ाइलों का इस्तेमाल न किया गया हो. C++ में, इसे ".d फ़ाइलें" कहा जाता है: कंपाइलर बताता है कि कौनसी हेडर फ़ाइलों का इस्तेमाल किया गया था. Make की तुलना में, बेहतर इंक्रीमेंटलिटी पाने के लिए, Bazel इस फ़ैक्ट का इस्तेमाल करता है. यह शामिल करने वाले स्कैनर की तुलना में बेहतर अनुमान देता है, क्योंकि यह कंपाइलर पर निर्भर करता है.
इन्हें ऐक्शन के तरीकों का इस्तेमाल करके लागू किया जाता है:
Action.discoverInputs()
को कॉल किया जाता है. इससे, ऐसे आर्टफ़ैक्ट का नेस्ट किया गया सेट दिखना चाहिए जिन्हें ज़रूरी माना गया है. ये सोर्स आर्टफ़ैक्ट होने चाहिए, ताकि ऐक्शन ग्राफ़ में कोई ऐसा डिपेंडेंसी एज न हो जिसका कॉन्फ़िगर किया गया टारगेट ग्राफ़ में कोई मिलता-जुलता एज न हो.Action.execute()
को कॉल करके यह कार्रवाई की जाती है.Action.execute()
के आखिर में, ऐक्शनAction.updateInputs()
को कॉल कर सकता है, ताकि Bazel को यह बताया जा सके कि उसके सभी इनपुट ज़रूरी नहीं थे. अगर इस्तेमाल किए गए इनपुट को इस्तेमाल नहीं किए गए के तौर पर रिपोर्ट किया जाता है, तो इससे गलत इंक्रीमेंटल बिल्ड बन सकते हैं.
जब कोई ऐक्शन कैश मेमोरी, किसी नए ऐक्शन इंस्टेंस (जैसे, सर्वर को रीस्टार्ट करने के बाद बनाया गया) पर हिट दिखाती है, तो Bazel खुद updateInputs()
को कॉल करता है, ताकि इनपुट का सेट, इनपुट की खोज और पहले की गई छंटाई का नतीजा दिखा सके.
Starlark ऐक्शन, ctx.actions.run()
के unused_inputs_list=
आर्ग्युमेंट का इस्तेमाल करके, कुछ इनपुट को इस्तेमाल न किए गए के तौर पर घोषित करने के लिए, इस सुविधा का इस्तेमाल कर सकते हैं.
कार्रवाइयां करने के अलग-अलग तरीके: रणनीतियां/ActionContexts
कुछ कार्रवाइयां अलग-अलग तरीकों से चलाई जा सकती हैं. उदाहरण के लिए, एक कमांड लाइन को स्थानीय तौर पर, स्थानीय तौर पर, लेकिन कई तरह के सैंडबॉक्स में या रिमोट तरीके से चलाया जा सकता है. इस कॉन्सेप्ट को ActionContext
(या Strategy
, क्योंकि हमने नाम बदलने की प्रोसेस को सिर्फ़ आधा पूरा किया है...) कहा जाता है
किसी ऐक्शन कॉन्टेक्स्ट का लाइफ़ साइकल इस तरह होता है:
- जब एक्ज़ीक्यूशन का फ़ेज़ शुरू होता है, तब
BlazeModule
इंस्टेंस से पूछा जाता है कि वे कार्रवाई के संदर्भ क्या हैं. यहExecutionTool
के कन्स्ट्रक्टर में होता है. ऐक्शन कॉन्टेक्स्ट टाइप की पहचान, JavaClass
के किसी ऐसे इंस्टेंस से की जाती है जोActionContext
के किसी सब-इंटरफ़ेस को रेफ़र करता है. साथ ही, यह भी तय करता है कि ऐक्शन कॉन्टेक्स्ट को किस इंटरफ़ेस को लागू करना चाहिए. - कार्रवाई के सही संदर्भ को उपलब्ध विकल्पों में से चुना जाता है. इसके बाद, उसे
ActionExecutionContext
औरBlazeExecutor
पर भेज दिया जाता है. - कार्रवाइयां,
ActionExecutionContext.getContext()
औरBlazeExecutor.getStrategy()
का इस्तेमाल करके कॉन्टेक्स्ट का अनुरोध करती हैं (इसके लिए, सिर्फ़ एक तरीका होना चाहिए…)
रणनीतियां, अपनी भूमिका निभाने के लिए दूसरी रणनीतियों को कॉल कर सकती हैं. उदाहरण के लिए, डाइनैमिक रणनीति में, स्थानीय और रिमोट, दोनों तरह से कार्रवाइयां शुरू की जाती हैं. इसके बाद, पहले पूरी होने वाली कार्रवाइयों का इस्तेमाल किया जाता है.
एक अहम रणनीति, लगातार चलने वाली वर्क प्रोसेस (WorkerSpawnStrategy
) को लागू करना है. इसका मकसद यह है कि कुछ टूल को शुरू होने में ज़्यादा समय लगता है. इसलिए, हर कार्रवाई के लिए नए सिरे से शुरू करने के बजाय, कार्रवाई के बीच में ही उनका फिर से इस्तेमाल किया जाना चाहिए. हालांकि, इससे सही तरीके से काम करने से जुड़ी समस्या हो सकती है, क्योंकि Bazel, वर्क प्रोसेस के इस वादे पर भरोसा करता है कि वह अलग-अलग अनुरोधों के बीच में, निगरानी की जा सकने वाली स्थिति को नहीं ले जाता
टूल बदलने पर, वर्कर प्रोसेस को फिर से शुरू करना होगा. किसी वर्कफ़्लो का फिर से इस्तेमाल किया जा सकता है या नहीं, यह तय करने के लिए WorkerFilesHash
का इस्तेमाल करके, इस्तेमाल किए गए टूल का चेकसम कैलकुलेट किया जाता है. इससे यह पता चलता है कि कार्रवाई के कौनसे इनपुट, टूल का हिस्सा हैं और कौनसे इनपुट इनपुट के बारे में हैं. इसे कार्रवाई बनाने वाला तय करता है: Spawn.getToolFiles()
और Spawn
की रनफ़ाइल को टूल का हिस्सा माना जाता है.
रणनीतियों (या कार्रवाई से जुड़े कॉन्टेक्स्ट!) के बारे में ज़्यादा जानकारी:
- कार्रवाइयां चलाने के लिए अलग-अलग रणनीतियों के बारे में जानकारी यहां उपलब्ध है.
- डाइनैमिक रणनीति के बारे में जानकारी, जिसमें हम स्थानीय और रिमोट, दोनों तरह से कार्रवाई करते हैं, ताकि यह देखा जा सके कि कौनसी कार्रवाई पहले पूरी होती है. इस बारे में ज़्यादा जानकारी यहां दी गई है.
- स्थानीय तौर पर कार्रवाइयां करने की बारीकियों के बारे में जानकारी यहां उपलब्ध है.
लोकल रिसोर्स मैनेजर
Baze एक साथ कई कार्रवाइयां कर सकता है. एक साथ चलाई जाने वाली लोकल कार्रवाइयों की संख्या, हर कार्रवाई के हिसाब से अलग-अलग होती है: कार्रवाई के लिए जितने ज़्यादा संसाधनों की ज़रूरत होगी, लोकल मशीन को ओवरलोड होने से बचाने के लिए कम इंस्टेंस एक साथ चलने चाहिए.
इसे ResourceManager
क्लास में लागू किया गया है: हर कार्रवाई के लिए, ResourceSet
इंस्टेंस (सीपीयू और रैम) के तौर पर, ज़रूरी लोकल संसाधनों के अनुमान के साथ एनोटेट करना होगा. इसके बाद, जब कार्रवाई से जुड़े कॉन्टेक्स्ट कुछ ऐसा करते हैं जिसके लिए स्थानीय संसाधनों की ज़रूरत होती है, तो वे ResourceManager.acquireResources()
को कॉल करते हैं. साथ ही, जब तक ज़रूरी संसाधन उपलब्ध नहीं होते, तब तक उन्हें ब्लॉक किया जाता है.
लोकल रिसॉर्स मैनेजमेंट के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
आउटपुट डायरेक्ट्री का स्ट्रक्चर
हर कार्रवाई के लिए आउटपुट डायरेक्ट्री में एक अलग जगह होनी चाहिए, जहां वह आउटपुट को रखती है. आम तौर पर, डेरिव्ड आर्टफ़ैक्ट की जगह इस तरह होती है:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
किसी खास कॉन्फ़िगरेशन से जुड़ी डायरेक्ट्री का नाम कैसे तय किया जाता है? ऐसी दो प्रॉपर्टी हैं जो एक-दूसरे से मेल नहीं खातीं:
- अगर एक ही बिल्ड में दो कॉन्फ़िगरेशन हो सकते हैं, तो उनके पास अलग-अलग डायरेक्ट्री होनी चाहिए, ताकि दोनों के पास एक ही ऐक्शन का अपना वर्शन हो. अगर ऐसा नहीं है और दोनों कॉन्फ़िगरेशन एक ही आउटपुट फ़ाइल बनाने वाले ऐक्शन की कमांड लाइन के बारे में अलग-अलग हैं, तो Bazel को यह नहीं पता होता कि कौनसा ऐक्शन चुनना है. इसे "ऐक्शन का विरोध" कहा जाता है
- अगर दो कॉन्फ़िगरेशन "लगभग" एक ही चीज़ को दिखाते हैं, तो उनका नाम एक ही होना चाहिए, ताकि कमांड लाइन मैच होने पर, एक में की गई कार्रवाइयों का फिर से इस्तेमाल किया जा सके: उदाहरण के लिए, Java कंपाइलर के कमांड लाइन विकल्पों में किए गए बदलावों की वजह से, C++ कंपाइल करने की कार्रवाइयां फिर से नहीं चलाई जानी चाहिए.
अभी तक हम इस समस्या को हल करने का कोई ऐसा सैद्धांतिक तरीका नहीं ढूंढ पाए हैं जो कॉन्फ़िगरेशन में काट-छांट करने की समस्या से मिलता-जुलता हो. विकल्पों के बारे में ज़्यादा जानकारी यहां उपलब्ध है. समस्या वाले मुख्य हिस्से, Starlark नियम (जिनके लेखक आम तौर पर Bazel के बारे में अच्छी तरह से नहीं जानते) और ऐसेपेक्ट हैं. ये ऐसेपेक्ट, उन चीज़ों के स्पेस में एक और डाइमेंशन जोड़ते हैं जिनसे "एक ही" आउटपुट फ़ाइल बन सकती है.
फ़िलहाल, कॉन्फ़िगरेशन के लिए पाथ सेगमेंट <CPU>-<compilation mode>
है. इसमें अलग-अलग सफ़िक्स जोड़े गए हैं, ताकि Java में लागू किए गए कॉन्फ़िगरेशन ट्रांज़िशन से ऐक्शन में कोई विरोध न हो. इसके अलावा, Starlark कॉन्फ़िगरेशन के ट्रांज़िशन के सेट का एक चेकसम जोड़ा गया है, ताकि उपयोगकर्ताओं के सामने कोई कार्रवाई न हो. यह पूरी तरह से सही नहीं है. इसे OutputDirectories.buildMnemonic()
में लागू किया गया है. यह हर कॉन्फ़िगरेशन फ़्रैगमेंट पर निर्भर करता है, जो आउटपुट डायरेक्ट्री के नाम में अपना हिस्सा जोड़ता है.
जांच
Bazel में टेस्ट चलाने के लिए, कई सुविधाएं उपलब्ध हैं. यह इनके साथ काम करता है:
- रिमोट तौर पर टेस्ट चलाना (अगर रिमोट तौर पर टेस्ट चलाने की सुविधा उपलब्ध है)
- एक साथ कई बार टेस्ट चलाना (डेटा इकट्ठा करने या समय का डेटा इकट्ठा करने के लिए)
- टेस्ट को अलग-अलग हिस्सों में बांटना (तेज़ी से टेस्ट करने के लिए, एक ही टेस्ट में टेस्ट केस को कई प्रोसेस में बांटना)
- काम न करने वाले टेस्ट फिर से चलाना
- टेस्ट को टेस्ट सुइट में ग्रुप करना
टेस्ट, नियमित तौर पर कॉन्फ़िगर किए गए टारगेट होते हैं, जिनमें TestProvider की सुविधा होती है. इससे पता चलता है कि टेस्ट को कैसे चलाया जाना चाहिए:
- ऐसे आर्टफ़ैक्ट जिनकी बिल्डिंग के नतीजे में टेस्ट चल रहा है. यह एक "कैश मेमोरी का स्टेटस" फ़ाइल है, जिसमें
TestResultData
मैसेज को सीरियलाइज़ किया गया है - टेस्ट को कितनी बार चलाना चाहिए
- उन शार्ड की संख्या जिन्हें टेस्ट में बांटा जाना चाहिए
- टेस्ट को कैसे चलाया जाना चाहिए, इस बारे में कुछ पैरामीटर (जैसे, टेस्ट टाइम आउट)
यह तय करना कि कौनसे टेस्ट किए जाएं
यह तय करना कि कौनसे टेस्ट चलाए जाएं, एक जटिल प्रोसेस है.
पहला, टारगेट पैटर्न पार्स करने के दौरान, टेस्ट सुइट को बार-बार बड़ा किया जाता है. TestsForTargetPatternFunction
में, ज़्यादा क्रिएटर्स के लिए उपलब्ध कराने की सुविधा लागू की गई है. एक तरह से यह आश्चर्य की बात है कि अगर किसी टेस्ट सुइट में कोई टेस्ट नहीं है, तो इसका मतलब है कि उसके पैकेज में मौजूद हर टेस्ट के लिए ऐसा है. इसे Package.beforeBuild()
में लागू किया गया है. इसके लिए, टेस्ट सुइट के नियमों में $implicit_tests
नाम का एक इंप्लिसिट एट्रिब्यूट जोड़ा गया है.
इसके बाद, कमांड लाइन के विकल्पों के हिसाब से, टेस्ट को साइज़, टैग, टाइम आउट, और भाषा के हिसाब से फ़िल्टर किया जाता है. इसे TestFilter
में लागू किया गया है. टारगेट पार्स करते समय TargetPatternPhaseFunction.determineTests()
से इसे कॉल किया जाता है. साथ ही, इसका नतीजा TargetPatternPhaseValue.getTestsToRunLabels()
में डाला जाता है. नियम के जिन एट्रिब्यूट के लिए फ़िल्टर किया जा सकता है उन्हें कॉन्फ़िगर नहीं किया जा सकता, इसलिए विश्लेषण के चरण से पहले ऐसा होता है. इसलिए, कॉन्फ़िगरेशन उपलब्ध नहीं है.
इसके बाद, इसे BuildView.createResult()
में प्रोसेस किया जाता है: ऐसे टारगेट को फ़िल्टर करके बाहर कर दिया जाता है जिनकी जांच नहीं हो सकी. साथ ही, टेस्ट को एक्सक्लूज़िव और नॉन-एक्सक्लूज़िव टेस्ट में बांट दिया जाता है. इसके बाद, उसे AnalysisResult
में डाला जाता है. इससे, ExecutionTool
को पता चलता है कि कौनसे टेस्ट करने हैं.
इस पूरी प्रोसेस को ज़्यादा पारदर्शी बनाने के लिए, tests()
क्वेरी ऑपरेटर (TestsFunction
में लागू किया गया) उपलब्ध है. इससे यह पता चलता है कि कमांड लाइन पर किसी खास टारगेट के बताए जाने पर कौनसे टेस्ट चलाए जाते हैं. यह अफ़सोस की बात है कि
इसे फिर से लागू किया गया है. इसलिए, हो सकता है कि यह ऊपर दी गई बातों से अलग-अलग तरीकों से अलग हो.
टेस्ट चलाना
कैश मेमोरी की स्थिति के आर्टफ़ैक्ट का अनुरोध करके, टेस्ट चलाए जाते हैं. इसके बाद, TestRunnerAction
को लागू किया जाता है. यह --test_strategy
कमांड लाइन विकल्प से चुने गए TestActionContext
को कॉल करता है. TestActionContext
, टेस्ट को अनुरोध किए गए तरीके से चलाता है.
टेस्ट, एक खास प्रोटोकॉल के हिसाब से चलाए जाते हैं. यह प्रोटोकॉल, एनवायरमेंट वैरिएबल का इस्तेमाल करके, टेस्ट को यह बताता है कि उनसे क्या उम्मीद की जा रही है. Bazel के लिए टेस्ट से क्या उम्मीद की जा सकती है और टेस्ट के लिए Bazel से क्या उम्मीद की जा सकती है, इस बारे में ज़्यादा जानकारी यहां दी गई है. सबसे आसान तरीके से, 0 वाले एक्सिट कोड का मतलब है कि प्रोसेस पूरी हो गई है. किसी भी अन्य कोड का मतलब है कि प्रोसेस पूरी नहीं हुई है.
कैश मेमोरी की स्थिति वाली फ़ाइल के अलावा, हर जांच प्रोसेस से कई अन्य फ़ाइलें बनती हैं. इन्हें "टेस्ट लॉग डायरेक्ट्री" में डाला जाता है. यह टारगेट कॉन्फ़िगरेशन की आउटपुट डायरेक्ट्री की सबडायरेक्ट्री होती है, जिसे testlogs
कहा जाता है:
test.xml
, JUnit स्टाइल वाली एक्सएमएल फ़ाइल, जिसमें टेस्ट स्HARD में मौजूद अलग-अलग टेस्ट केस की जानकारी होती हैtest.log
, टेस्ट का कंसोल आउटपुट. stdout और stderr को अलग नहीं किया गया है.test.outputs
, "बिना एलान की गई आउटपुट डायरेक्ट्री"; इसका इस्तेमाल उन टेस्ट के लिए किया जाता है जो टर्मिनल पर प्रिंट करने के अलावा, फ़ाइलों को भी आउटपुट करना चाहते हैं.
टेस्ट एक्ज़ीक्यूशन के दौरान दो चीज़ें हो सकती हैं, जो सामान्य टारगेट बनाते समय नहीं हो सकती: खास टेस्ट एक्ज़ीक्यूशन और आउटपुट स्ट्रीमिंग.
कुछ टेस्ट को एक्सक्लूज़िव मोड में चलाना ज़रूरी होता है. उदाहरण के लिए, इन्हें अन्य टेस्ट के साथ नहीं चलाना चाहिए. इसे जांच के नियम में tags=["exclusive"]
जोड़कर या --test_strategy=exclusive
के साथ जांच चलाकर पाया जा सकता है . हर एक्सक्लूज़िव जांच, Skyframe के अलग-अलग इनवोकेशन से चलाई जाती है. यह "मुख्य" बिल्ड के बाद, जांच को चलाने का अनुरोध करता है. इसे SkyframeExecutor.runExclusiveTest()
में लागू किया गया है.
सामान्य कार्रवाइयों के उलट, जिनके टर्मिनल आउटपुट को कार्रवाई खत्म होने पर डंप कर दिया जाता है. हालांकि, उपयोगकर्ता टेस्ट के आउटपुट को स्ट्रीम करने का अनुरोध कर सकता है, ताकि उन्हें लंबे समय तक चलने वाले टेस्ट की प्रोग्रेस के बारे में जानकारी मिल सके. इसकी जानकारी, --test_output=streamed
कमांड लाइन विकल्प से मिलती है. इससे, खास टेस्ट को चलाने का मतलब है, ताकि अलग-अलग टेस्ट के आउटपुट एक-दूसरे में न मिलें.
इसे StreamedTestOutput
क्लास में लागू किया गया है. यह काम, टेस्ट की test.log
फ़ाइल में हुए बदलावों को पोल करके करता है. साथ ही, Bazel के नियमों वाले टर्मिनल में नए बाइट डालता है.
चलाए गए टेस्ट के नतीजे, इवेंट बस पर उपलब्ध होते हैं. इसके लिए, TestAttempt
, TestResult
या TestingCompleteEvent
जैसे अलग-अलग इवेंट को देखा जाता है. इन्हें बिल्ड इवेंट प्रोटोकॉल में डाला जाता है और AggregatingTestListener
की मदद से कंसोल में भेजा जाता है.
कवरेज कलेक्शन
कवरेज की रिपोर्ट, bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
फ़ाइलों में LCOV फ़ॉर्मैट में टेस्ट के ज़रिए दी जाती है .
कवरेज इकट्ठा करने के लिए, हर टेस्ट को collect_coverage.sh
नाम की स्क्रिप्ट में रैप किया जाता है.
यह स्क्रिप्ट, कवरेज इकट्ठा करने की सुविधा चालू करने के लिए, टेस्ट का एनवायरमेंट सेट अप करती है. साथ ही, यह तय करती है कि कवरेज रनटाइम(रनटाइम) से कवरेज फ़ाइलें कहां लिखी जाती हैं. इसके बाद, यह जांच करता है. एक टेस्ट में कई सबप्रोसेस चल सकती हैं. साथ ही, इसमें कई अलग-अलग प्रोग्रामिंग भाषाओं में लिखे गए हिस्से हो सकते हैं. इनमें अलग-अलग कवरेज कलेक्शन रनटाइम होते हैं. रैपर स्क्रिप्ट, ज़रूरत पड़ने पर नतीजों वाली फ़ाइलों को LCOV फ़ॉर्मैट में बदलती है और उन्हें एक फ़ाइल में मर्ज करती है.
collect_coverage.sh
को टेस्ट की रणनीतियों के ज़रिए इंटरपोज़ किया जाता है. इसके लिए, collect_coverage.sh
को टेस्ट के इनपुट पर होना ज़रूरी है. यह काम, एलिमेंट के लिए डिफ़ॉल्ट तौर पर लागू होने वाले एट्रिब्यूट :coverage_support
की मदद से किया जाता है. इस एट्रिब्यूट की वैल्यू, कॉन्फ़िगरेशन फ़्लैग --coverage_support
की वैल्यू पर सेट होती है (TestConfiguration.TestOptions.coverageSupport
देखें)
कुछ भाषाएं ऑफ़लाइन इंस्ट्रुमेंटेशन करती हैं, जिसका मतलब है कि कवरेज इंस्ट्रुमेंटेशन को कंपाइल करते समय (जैसे कि C++) जोड़ा जाता है और कुछ ऑनलाइन इंस्ट्रुमेंटेशन करते हैं. इसका मतलब है कि कवरेज इंस्ट्रुमेंटेशन को एक्ज़ीक्यूशन के समय जोड़ा जाता है.
बेसलाइन कवरेज एक और मुख्य कॉन्सेप्ट है. यह किसी लाइब्रेरी, बिनेरी या टेस्ट की कवरेज है. इससे पता चलता है कि उसमें कोई कोड नहीं चलाया गया था. यह समस्या हल करता है कि अगर आपको किसी बाइनरी के लिए टेस्ट कवरेज का हिसाब लगाना है, तो सभी टेस्ट की कवरेज को मर्ज करना काफ़ी नहीं है. ऐसा इसलिए, क्योंकि बाइनरी में ऐसा कोड हो सकता है जो किसी भी टेस्ट से लिंक न हो. इसलिए, हम हर बाइनरी के लिए एक कवरेज फ़ाइल जनरेट करते हैं. इसमें सिर्फ़ वे फ़ाइलें शामिल होती हैं जिनके लिए हम कवरेज इकट्ठा करते हैं. इनमें ऐसी कोई लाइन नहीं होती जिसकी कवरेज ली गई हो. किसी टारगेट के लिए बेसलाइन कवरेज फ़ाइल, bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
पर मौजूद है . अगर 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
को सबक्लास करके, क्वेरी के अन्य फ़ंक्शन जोड़े जा सकते हैं. क्वेरी के नतीजों को स्ट्रीम करने की अनुमति देने के लिए, उन्हें कुछ डेटा स्ट्रक्चर में इकट्ठा करने के बजाय, QueryFunction
को query2.engine.Callback
पास किया जाता है. यह ऐसे नतीजों के लिए कॉल करता है जिन्हें वह दिखाना चाहता है.
क्वेरी का नतीजा कई तरीकों से दिखाया जा सकता है: लेबल, लेबल और नियम वाली क्लास, एक्सएमएल, प्रोटोबस वगैरह. इन्हें OutputFormatter
की सब-क्लास के तौर पर लागू किया जाता है.
कुछ क्वेरी आउटपुट फ़ॉर्मैट (proto, definitely) की एक खास ज़रूरत यह है कि Bazel को पैकेज लोड करने से मिलने वाली _पूरी _जानकारी को एमिट करना होगा, ताकि कोई व्यक्ति आउटपुट की तुलना कर सके और यह पता लगा सके कि किसी खास टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को क्रम से लगाया जाना चाहिए. इसलिए, ऐसे कुछ ही एट्रिब्यूट टाइप हैं जिनमें स्टारलार्क की मुश्किल वैल्यू वाले एट्रिब्यूट नहीं हैं. आम तौर पर, किसी लेबल का इस्तेमाल करके, उस लेबल के साथ नियम में जटिल जानकारी अटैच की जाती है. यह समस्या हल करने का एक अच्छा तरीका नहीं है और इस शर्त को हटाना अच्छा होगा.
मॉड्यूल सिस्टम
Basel में मॉड्यूल जोड़कर उसे बढ़ाया जा सकता है. हर मॉड्यूल को BlazeModule
सब-क्लास करना होगा. यह नाम Baze के इतिहास का हिस्सा है, जब इसे Blaze कहा जाता था. साथ ही, किसी निर्देश को एक्ज़ीक्यूट करने के दौरान होने वाले अलग-अलग इवेंट के बारे में जानकारी मिल जाती है.
इनका इस्तेमाल ज़्यादातर, "नॉन-कोर" फ़ंक्शन के अलग-अलग हिस्सों को लागू करने के लिए किया जाता है. इन फ़ंक्शन की ज़रूरत, Bazel के कुछ वर्शन (जैसे, Google में इस्तेमाल होने वाले वर्शन) को होती है:
- रिमोट एक्ज़ीक्यूशन सिस्टम के इंटरफ़ेस
- नए निर्देश
एक्सटेंशन पॉइंट BlazeModule
के ऑफ़र का सेट कुछ हद तक गड़बड़ी वाला है. इसका इस्तेमाल, डिज़ाइन के अच्छे सिद्धांतों के उदाहरण के तौर पर न करें.
इवेंट बस
BlazeModules, बाकी Bazel के साथ इवेंट बस (EventBus
) के ज़रिए मुख्य रूप से कम्यूनिकेट करते हैं: हर बिल्ड के लिए एक नया इंस्टेंस बनाया जाता है. Bazel के अलग-अलग हिस्से, उसमें इवेंट पोस्ट कर सकते हैं. साथ ही, मॉड्यूल उन इवेंट के लिए लिसनर रजिस्टर कर सकते हैं जिनमें उनकी दिलचस्पी है. उदाहरण के लिए, नीचे दी गई चीज़ें इवेंट के तौर पर दिखाई जाती हैं:
- बनाए जाने वाले बिल्ड टारगेट की सूची तय कर दी गई है
(
TargetParsingCompleteEvent
) - टॉप-लेवल कॉन्फ़िगरेशन तय कर दिए गए हैं
(
BuildConfigurationEvent
) - टारगेट बनाया गया या नहीं (
TargetCompleteEvent
) - कोई टेस्ट चलाया गया (
TestAttempt
,TestSummary
)
इनमें से कुछ इवेंट, बिल्ड इवेंट प्रोटोकॉल में, Bazel के बाहर दिखाए जाते हैं. ये BuildEvent
होते हैं. इससे न सिर्फ़ BlazeModule
, बल्कि Bazel प्रोसेस के बाहर की चीज़ें भी बिल्ड को मॉनिटर कर सकती हैं. इन्हें या तो ऐसी फ़ाइल के तौर पर ऐक्सेस किया जा सकता है जिसमें
प्रोटोकॉल मैसेज होते हैं या Ba बैंक, इवेंट स्ट्रीम करने के लिए किसी सर्वर (जिसे Build इवेंट सेवा कहा जाता है) से कनेक्ट कर सकता है.
इसे 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()
स्टेटमेंट तक इसका आकलन किया जाता है.
डेटा स्टोर करने की जगहें फ़ेच करना
Basel के लिए, डेटा स्टोर करने की जगह का कोड उपलब्ध होने से पहले, उसे
फ़ेच किया जाना होगा. इससे Bazel, $OUTPUT_BASE/external/<repository name>
के नीचे एक डायरेक्ट्री बनाता है.
रिपॉज़िटरी को फ़ेच करने के लिए, यह तरीका अपनाएं:
PackageLookupFunction
को पता चलता है कि उसे एक रिपॉज़िटरी की ज़रूरत है और वहSkyKey
के तौर परRepositoryName
बनाता है, जोRepositoryLoaderFunction
को ट्रिगर करता हैRepositoryLoaderFunction
,RepositoryDelegatorFunction
को अनुरोध भेजता है. हालांकि, इसकी वजह साफ़ तौर पर नहीं बताई गई है. कोड के मुताबिक, Skyframe के रीस्टार्ट होने पर, चीज़ों को फिर से डाउनलोड करने से बचने के लिए ऐसा किया जाता है. हालांकि, यह वजह सही नहीं हैRepositoryDelegatorFunction
, डेटा स्टोर करने की उस जगह का पता लगा लेता है जिसे फ़ेच करने के लिए कहा जाता है. इसके लिए, वर्कस्पेस फ़ाइल के अलग-अलग हिस्सों को तब तक दोहराना होता है, जब तक कि अनुरोध की गई डेटा स्टोर करने की जगह नहीं मिल जाती- सही
RepositoryFunction
पाया गया है जो रिपॉज़िटरी को लागू करता है; यह या तो डेटा स्टोर करने की जगह का Starlark लागू करता है या Java में डेटा स्टोर करने की जगहों के लिए हार्ड कोड किया गया मैप होता है.
कैश मेमोरी की कई लेयर होती हैं, क्योंकि रिपॉज़िटरी फ़ेच करना बहुत महंगा हो सकता है:
- डाउनलोड की गई फ़ाइलों के लिए एक कैश मेमोरी होती है, जिसे उनके चेकसम (
RepositoryCache
) से कंट्रोल किया जाता है. इसके लिए, ज़रूरी है कि चेकसम, WORKSPACE फ़ाइल में उपलब्ध हो. हालांकि, यह किसी भी तरह से सुरक्षित है. इसे एक ही वर्कस्टेशन पर इस्तेमाल करने वाले सभी बेज़ेल सर्वर इंस्टेंस के ज़रिए शेयर किया जाता है. भले ही, वे किसी भी वर्कस्पेस या आउटपुट बेस पर चल रहे हों. $OUTPUT_BASE/external
में मौजूद हर रिपॉज़िटरी के लिए एक "मार्कर फ़ाइल" लिखी जाती है. इसमें उस नियम का चेकसम होता है जिसका इस्तेमाल उसे फ़ेच करने के लिए किया गया था. अगर Basel सर्वर रीस्टार्ट होता है, लेकिन चेकसम में बदलाव नहीं होता है, तो उसे फिर से फ़ेच नहीं किया जाता है. इसेRepositoryDelegatorFunction.DigestWriter
में लागू किया गया है.--distdir
कमांड लाइन विकल्प, एक और कैश मेमोरी तय करता है. इसका इस्तेमाल, डाउनलोड किए जाने वाले आर्टफ़ैक्ट को खोजने के लिए किया जाता है. यह एंटरप्राइज़ सेटिंग में काम आता है, जहां Bazel को इंटरनेट से कोई भी चीज़ फ़ेच नहीं करनी चाहिए. इसेDownloadManager
ने लागू किया है .
डेटा स्टोर करने की जगह को डाउनलोड करने के बाद, उसमें मौजूद आर्टफ़ैक्ट को सोर्स आर्टफ़ैक्ट के तौर पर माना जाता है. इससे समस्या होती है, क्योंकि आम तौर पर Bazel, सोर्स आर्टफ़ैक्ट के अप-टू-डेट होने की जांच करने के लिए, उन पर stat() को कॉल करता है. साथ ही, इन आर्टफ़ैक्ट को अमान्य भी कर दिया जाता है, जब वे जिस रिपॉज़िटरी में मौजूद होते हैं उसकी परिभाषा में बदलाव होता है. इसलिए,
किसी बाहरी डेटा स्टोर करने की जगह में मौजूद आर्टफ़ैक्ट के FileStateValue
को उसके बाहरी डेटा स्टोर करने की जगह पर निर्भर होना चाहिए. इसे ExternalFilesHelper
मैनेज करता है.
रिपॉज़िटरी मैपिंग
ऐसा हो सकता है कि एक से ज़्यादा रिपॉज़िटरी, एक ही रिपॉज़िटरी पर निर्भर करना चाहें. हालांकि, ऐसा अलग-अलग वर्शन में हो सकता है (यह "डायमंड डिपेंडेंसी से जुड़ी समस्या" का एक उदाहरण है). उदाहरण के लिए, अगर बिल्ड में अलग-अलग रिपॉज़िटरी में मौजूद दो बाइनरी को Guava पर निर्भर करना है, तो हो सकता है कि दोनों Guava को @guava//
से शुरू होने वाले लेबल के साथ रेफ़र करें. साथ ही, यह उम्मीद करें कि इसका मतलब इसके अलग-अलग वर्शन से है.
इसलिए, Basel को किसी बाहरी रिपॉज़िटरी लेबल को फिर से मैप करने की अनुमति मिलती है. इससे @guava//
स्ट्रिंग, एक बाइनरी डेटा स्टोर करने की जगह (जैसे कि @guava1//
) का इस्तेमाल कर सकती है. साथ ही, दूसरी Guava रिपॉज़िटरी (जैसे, @guava2//
) में से किसी दूसरी जगह के डेटा को स्टोर कर सकती है.
इसके अलावा, इसका इस्तेमाल डायमंड को जॉइन करने के लिए भी किया जा सकता है. अगर कोई रिपॉज़िटरी @guava1//
पर निर्भर करती है और दूसरी @guava2//
पर निर्भर करती है, तो रिपॉज़िटरी मैपिंग, दोनों डेटा स्टोर करने की जगहों को फिर से मैप करने की अनुमति देती है. इससे, किसी कैननिकल @guava//
रिपॉज़िटरी का इस्तेमाल किया जा सकता है.
मैपिंग को WORKSPACE फ़ाइल में, अलग-अलग रिपॉज़िटरी की परिभाषाओं के repo_mapping
एट्रिब्यूट के तौर पर बताया गया है. इसके बाद, यह Skyframe में WorkspaceFileValue
के सदस्य के तौर पर दिखता है. यहां इसे इनसे कनेक्ट किया जाता है:
Package.Builder.repositoryMapping
का इस्तेमाल, पैकेज में मौजूद नियमों के लेबल की वैल्यू वाले एट्रिब्यूट कोRuleClass.populateRuleAttributeValues()
से बदलने के लिए किया जाता हैPackage.repositoryMapping
का इस्तेमाल विश्लेषण के फ़ेज़ में किया जाता है. इससे$(location)
जैसी समस्याओं को हल किया जा सकता है, जिन्हें लोड करने के फ़ेज़ में पार्स नहीं किया जाता- load() स्टेटमेंट में लेबल का समाधान करने के लिए
BzlLoadFunction
JNI बिट
Bazel का सर्वर ज़्यादातर 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
वहां _not _दिखते हैं). ये ExtendedEventHandler
के लागू होने के तरीके हैं. इनका मुख्य इस्तेमाल, कैश मेमोरी में सेव किए गए EventBus
इवेंट को फिर से चलाने के लिए किया जाता है. ये सभी EventBus
इवेंट, Postable
को लागू करते हैं. हालांकि, EventBus
पर पोस्ट की गई हर चीज़ को ज़रूरी रूप से यह इंटरफ़ेस लागू नहीं करता. सिर्फ़ ExtendedEventHandler
से कैश मेमोरी में सेव की गई चीज़ें ही लागू होती हैं. हालांकि, ऐसा करना अच्छा होता है और ज़्यादातर चीज़ें ऐसा करती हैं, लेकिन इसे लागू करना ज़रूरी नहीं है
टर्मिनल आउटपुट ज़्यादातर UiEventHandler
के ज़रिए दिखाया जाता है. यह आउटपुट को फ़ॉर्मैट करने और प्रोग्रेस रिपोर्ट करने के लिए ज़रूरी है. इसके दो इनपुट होते हैं:
- इवेंट बस
- इवेंट स्ट्रीम, रिपोर्टर के ज़रिए इसमें शामिल हुई
क्लाइंट की आरपीसी स्ट्रीम से, कमांड को लागू करने वाली मशीन (उदाहरण के लिए, बाज़ल का बाकी हिस्सा) का सीधा कनेक्शन सिर्फ़ Reporter.getOutErr()
के ज़रिए होता है. इससे इन स्ट्रीम को सीधे ऐक्सेस किया जा सकता है. इसका इस्तेमाल सिर्फ़ तब किया जाता है, जब किसी कमांड को bazel query
जैसे बहुत ज़्यादा बाइनरी डेटा को डंप करना हो.
Bazel की प्रोफ़ाइल बनाना
Bazel तेज़ काम करता है. Bazel भी धीमा है, क्योंकि बिल्ड की संख्या तब तक बढ़ती रहती है, जब तक कि वह ज़्यादा से ज़्यादा नहीं हो जाती. इस वजह से, Bazel में एक प्रोफ़ाइलर शामिल होता है. इसका इस्तेमाल, बिल्ड और Bazel की प्रोफ़ाइल बनाने के लिए किया जा सकता है. इसे Profiler
नाम की क्लास में लागू किया गया है. यह सुविधा डिफ़ॉल्ट रूप से चालू रहती है. हालांकि, यह सिर्फ़ कम डेटा रिकॉर्ड करती है, ताकि इसका ओवरहेड कम से कम हो. कमांड लाइन --record_full_profiler_data
की मदद से, यह ज़्यादा से ज़्यादा डेटा रिकॉर्ड कर सकती है.
इससे एक ऐसी प्रोफ़ाइल बनती है जो Chrome के प्रोफ़ाइलर फ़ॉर्मैट में होती है. यह Chrome में सबसे अच्छी तरह दिखती है. यह डेटा मॉडल, टास्क स्टैक का है: इसमें, टास्क शुरू किए जा सकते हैं और टास्क खत्म किए जा सकते हैं. साथ ही, इन्हें एक-दूसरे के अंदर अच्छे से नेस्ट किया जाना चाहिए. हर Java थ्रेड का अपना टास्क स्टैक होता है. TODO: यह कार्रवाइयों और लगातार पास होने के तरीके के साथ कैसे काम करता है?
प्रोफ़ाइलर, BlazeRuntime.initProfiler()
और BlazeRuntime.afterCommand()
में बंद हो गया है और चालू हो गया है. साथ ही, यह ज़्यादा से ज़्यादा समय तक लाइव रहने की कोशिश करता है, ताकि हम हर चीज़ की प्रोफ़ाइल बना सकें. प्रोफ़ाइल में कुछ जोड़ने के लिए,
Profiler.instance().profile()
को कॉल करें. यह Closeable
दिखाता है. इसके क्लोज़र का मतलब है कि टास्क पूरा हो गया है. इसका सबसे अच्छा इस्तेमाल, संसाधनों की मदद से कोशिश करने वाले
स्टेटमेंट के साथ किया जाता है.
हम MemoryProfiler
में, मेमोरी की बुनियादी प्रोफ़ाइलिंग भी करते हैं. यह हमेशा चालू रहता है और ज़्यादातर हीप साइज़ और जीसी ऐक्शन को रिकॉर्ड करता है.
Bazel की जांच करना
Bazel में दो तरह के मुख्य टेस्ट होते हैं: एक ऐसा टेस्ट जो Bazel को "ब्लैक बॉक्स" के तौर पर देखता है और दूसरा ऐसा टेस्ट जो सिर्फ़ विश्लेषण का फ़ेज़ चलाता है. हम पहले वाले "इंटिग्रेशन टेस्ट" और बाद वाले "यूनिट टेस्ट" को "यूनिट टेस्ट" कहते हैं. हालांकि, वे इंटिग्रेशन टेस्ट की तरह होते हैं, जो कम इंटिग्रेट किए जाते हैं. हमारे पास कुछ यूनिट टेस्ट भी हैं, जो ज़रूरी होने पर किए जाते हैं.
इंटिग्रेशन टेस्ट दो तरह के होते हैं:
- एक ऐसा प्रोग्राम जिसे
src/test/shell
के तहत, बहुत अच्छे बैश टेस्ट फ़्रेमवर्क का इस्तेमाल करके लागू किया गया हो - Java में लागू किए गए. इन्हें
BuildIntegrationTestCase
के सबक्लास के तौर पर लागू किया जाता है
BuildIntegrationTestCase
, इंटिग्रेशन टेस्टिंग के लिए सबसे ज़्यादा इस्तेमाल किया जाने वाला फ़्रेमवर्क है, क्योंकि यह ज़्यादातर टेस्टिंग स्थितियों के लिए बेहतर तरीके से तैयार है. यह एक Java फ़्रेमवर्क है, इसलिए यह कई सामान्य डेवलपमेंट टूल के साथ डीबग करने की सुविधा और आसान इंटिग्रेशन देता है. Bazel रिपॉज़िटरी में BuildIntegrationTestCase
क्लास के कई उदाहरण हैं.
विश्लेषण टेस्ट, BuildViewTestCase
के सबक्लास के तौर पर लागू किए जाते हैं. इसमें एक स्क्रैच फ़ाइल सिस्टम होता है, जिसका इस्तेमाल BUILD
फ़ाइलें लिखने के लिए किया जा सकता है. इसके बाद, अलग-अलग सहायक तरीके, कॉन्फ़िगर किए गए टारगेट का अनुरोध कर सकते हैं, कॉन्फ़िगरेशन में बदलाव कर सकते हैं, और विश्लेषण के नतीजे के बारे में अलग-अलग बातें बता सकते हैं.