यह दस्तावेज़, कोडबेस के बारे में जानकारी देता है. साथ ही, यह भी बताता है कि Basel की प्रोसेस कैसे हुई है. यह उन लोगों के लिए है जो Bazel में योगदान देना चाहते हैं, न कि आम उपयोगकर्ताओं के लिए.
परिचय
बेज़ेल का कोडबेस बड़ा है (~350KLOC प्रोडक्शन कोड और ~260 KLOC टेस्ट कोड) और कोई भी पूरे लैंडस्केप से वाकिफ़ नहीं है: सभी लोग अपनी खास घाटी को अच्छी तरह से जानते हैं, लेकिन कुछ ही लोगों को यह पता है कि हर दिशा में पहाड़ियों पर क्या मौजूद है.
इस दस्तावेज़ में कोडबेस के बारे में खास जानकारी दी गई है, ताकि लोग इस पर काम करना शुरू कर सकें. इससे, उन्हें बीच में किसी भी तरह की समस्या का सामना नहीं करना पड़ेगा.
Bazel के सोर्स कोड का सार्वजनिक वर्शन, GitHub पर github.com/bazelbuild/bazel पर मौजूद है. यह "सच का सोर्स" नहीं है, इसे Google के इंटरनल सोर्स ट्री से लिया गया है. इसमें ऐसे अन्य फ़ंक्शन शामिल हैं जो Google के बाहर काम के नहीं हैं. लंबे समय के लक्ष्य के तौर पर, GitHub को सटीक जानकारी का सोर्स बनाना है.
योगदानों को GitHub के सामान्य पुल रिक्वेस्ट मैकेनिज्म की मदद से स्वीकार किया जाता है. साथ ही, Googler उन्हें मैन्युअल तरीके से इंटरनल सोर्स ट्री में इंपोर्ट करता है. इसके बाद, उन्हें GitHub पर फिर से एक्सपोर्ट किया जाता है.
क्लाइंट/सर्वर आर्किटेक्चर
Bazel का ज़्यादातर हिस्सा, सर्वर प्रोसेस में मौजूद होता है. यह प्रोसेस, बिल्ड के बीच RAM में रहती है. इससे Bazel, बिल्ड के बीच स्टेटस बनाए रख पाता है.
इसलिए, Bazel कमांड-लाइन में दो तरह के विकल्प होते हैं: स्टार्टअप और कमांड. इस तरह की कमांड लाइन में:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
कुछ विकल्प (--host_jvm_args=
), चलाए जाने वाले कमांड के नाम से पहले और कुछ उसके बाद (-c opt
) होते हैं. पहले तरह के विकल्प को "स्टार्टअप विकल्प" कहा जाता है और यह पूरी सर्वर प्रोसेस पर असर डालता है. वहीं, दूसरे तरह के विकल्प, यानी "कमांड विकल्प" सिर्फ़ एक कमांड पर असर डालते हैं.
हर सर्वर इंस्टेंस में एक अलग फ़ाइल फ़ोल्डर होता है (सोर्स ट्री का कलेक्शन, जिसे "डेटा स्टोर करने की जगह" कहा जाता है) और हर वर्कस्पेस में आम तौर पर एक ऐक्टिव सर्वर इंस्टेंस होता है. कस्टम आउटपुट बेस तय करके, इस समस्या को हल किया जा सकता है. ज़्यादा जानकारी के लिए, "डायरेक्ट्री लेआउट" सेक्शन देखें.
Bazel को एक ELF एक्ज़ीक्यूटेबल के तौर पर डिस्ट्रिब्यूट किया जाता है. यह एक मान्य .zip फ़ाइल भी है.
bazel
टाइप करने पर, C++ में लागू किए गए ऊपर दिए गए ELF executable ("क्लाइंट") को कंट्रोल मिल जाता है. यह नीचे दिए गए चरणों का इस्तेमाल करके सही सर्वर प्रोसेस सेट अप करता है:
- यह जांचता है कि क्या यह पहले से ही एक्सट्रैक्ट हो चुका है. अगर नहीं, तो फिर से अपलोड की जा सकती है. यहीं से सर्वर लागू किया जाता है.
- यह जांच करता है कि कोई चालू सर्वर इंस्टेंस काम कर रहा है या नहीं: वह चल रहा है,
उसमें स्टार्टअप के सही विकल्प हैं, और वह सही वर्कस्पेस डायरेक्ट्री का इस्तेमाल करता है. यह
$OUTPUT_BASE/server
डायरेक्ट्री में जाकर, चल रहे सर्वर को ढूंढता है. इस डायरेक्ट्री में, उस पोर्ट की लॉक फ़ाइल होती है जिस पर सर्वर सुन रहा होता है. - ज़रूरत पड़ने पर, पुरानी सर्वर प्रोसेस को बंद कर देता है
- ज़रूरत पड़ने पर, नई सर्वर प्रोसेस शुरू करता है
सही सर्वर प्रोसेस तैयार होने के बाद, जिस निर्देश को चलाना है उसे gRPC इंटरफ़ेस के ज़रिए भेजा जाता है. इसके बाद, Bazel का आउटपुट टर्मिनल पर वापस भेजा जाता है. एक ही समय पर सिर्फ़ एक निर्देश चल सकता है. इसे
C++ में पार्ट और
Java में पार्ट के साथ, बहुत अच्छे लॉकिंग तरीके का इस्तेमाल करके लागू किया गया है. एक साथ कई कमांड चलाने के लिए, कुछ बुनियादी ढांचा मौजूद है, क्योंकि bazel version
को किसी दूसरे कमांड के साथ चलाने में कुछ परेशानी होती है. मुख्य समस्या, BlazeModule
s के लाइफ़ साइकल और BlazeRuntime
में कुछ स्टेटस है.
किसी निर्देश के खत्म होने पर, Basel का सर्वर उस एग्ज़िट कोड को ट्रांसमिट करता है जिसे क्लाइंट को वापस करना चाहिए. bazel run
को लागू करना एक दिलचस्प बात है: इस कमांड का काम, हाल ही में Bazel से बनाए गए कुछ कोड को चलाना है. हालांकि, यह सर्वर प्रोसेस से ऐसा नहीं कर सकता, क्योंकि इसमें टर्मिनल नहीं है. इसलिए, यह क्लाइंट को बताता है कि उसे किस बाइनरी को ujexec()
करना चाहिए और किन आर्ग्युमेंट के साथ.
जब कोई Ctrl-C दबाता है, तो क्लाइंट इसे gRPC कनेक्शन पर रद्द करें कॉल में बदल देता है, जो जितनी जल्दी हो सके कमांड को खत्म करने की कोशिश करता है. तीसरे Ctrl-C के बाद, क्लाइंट सर्वर को SIGKILL भेजता है.
क्लाइंट का सोर्स कोड src/main/cpp
में है और सर्वर से बातचीत करने के लिए इस्तेमाल किया जाने वाला प्रोटोकॉल src/main/protobuf/command_server.proto
में है .
सर्वर का मुख्य एंट्री पॉइंट BlazeRuntime.main()
है और क्लाइंट से आने वाले gRPC कॉल को GrpcServerImpl.run()
मैनेज करता है.
डायरेक्ट्री का लेआउट
Bazel, बिल्ड के दौरान डायरेक्ट्री का एक ऐसा सेट बनाता है जो थोड़ा मुश्किल होता है. आउटपुट डायरेक्ट्री लेआउट में पूरी जानकारी उपलब्ध है.
"मुख्य रिपॉज़िटरी", वह सोर्स ट्री है जिसमें Bazel को चलाया जाता है. आम तौर पर, यह उस फ़ाइल से जुड़ा होता है जिसे आपने सोर्स कंट्रोल से चेक आउट किया है. इस डायरेक्ट्री के रूट को "वर्कस्पेस रूट" कहा जाता है.
Bazel अपना सारा डेटा "आउटपुट उपयोगकर्ता रूट" में डालता है. आम तौर पर, यह $HOME/.cache/bazel/_bazel_${USER}
होता है. हालांकि, --output_user_root
स्टार्टअप विकल्प का इस्तेमाल करके इसे बदला जा सकता है.
"install base" वह जगह है जहां Bazel को निकाला जाता है. यह अपने-आप होता है और हर Bazel वर्शन को, इंस्टॉल बेस में उसके चेकसम के आधार पर एक सबडायरेक्ट्री मिलती है. यह डिफ़ॉल्ट रूप से $OUTPUT_USER_ROOT/install
पर सेट होता है. इसे --install_base
कमांड लाइन विकल्प का इस्तेमाल करके बदला जा सकता है.
"आउटपुट बेस" वह जगह है जहां किसी खास वर्कस्पेस से अटैच किए गए Baze इंस्टेंस,
उस ईमेल पते के बारे में जानकारी देते हैं. हर आउटपुट बेस में, किसी भी समय ज़्यादा से ज़्यादा एक Bazel सर्वर इंस्टेंस चल रहा होता है. आम तौर पर, यह $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
में बदलने जा रहे हैं. हालांकि, यह एक लंबी अवधि का प्लान है, क्योंकि यह बहुत ही असंगत बदलाव है. - बिल्ड के दौरान बनाई गई फ़ाइलें.
किसी निर्देश को लागू करने की प्रोसेस
जब बेज़ल सर्वर कंट्रोल हो जाता है और उसे किसी निर्देश के बारे में पता चल जाता है, तो इवेंट का यह क्रम होता है:
BlazeCommandDispatcher
को नए अनुरोध के बारे में सूचना दी जाती है. यह तय करता है कि निर्देश को चलाने के लिए, वर्कस्पेस की ज़रूरत है या नहीं. यह ज़रूरी नहीं है कि हर निर्देश के लिए वर्कस्पेस की ज़रूरत हो. जैसे, वर्शन या मदद जैसे निर्देशों के लिए वर्कस्पेस की ज़रूरत नहीं होती. साथ ही, यह भी तय करता है कि कोई दूसरा निर्देश चल रहा है या नहीं.सही निर्देश मिल गया है. हर कमांड में इंटरफ़ेस
BlazeCommand
लागू होना चाहिए और उसमें@Command
एनोटेशन होना चाहिए. यह एक तरह का एंटीपैटर्न है. यह अच्छा होगा, अगर किसी कमांड के लिए ज़रूरी सभी मेटाडेटा कोBlazeCommand
पर मौजूद तरीकों से बताया गया होकमांड-लाइन के विकल्पों को पार्स किया जाता है. हर कमांड में अलग-अलग कमांड लाइन विकल्प होते हैं. इन विकल्पों के बारे में
@Command
एनोटेशन में बताया गया है.एक इवेंट बस बनाई जाती है. इवेंट बस, उन इवेंट के लिए एक स्ट्रीम है जो बिल्ड के दौरान होते हैं. इनमें से कुछ को, बिल्ड इवेंट प्रोटोकॉल के तहत Bazel से बाहर एक्सपोर्ट किया जाता है, ताकि दुनिया को यह पता चल सके कि बिल्ड कैसे हुआ.
निर्देश को कंट्रोल मिल जाता है. सबसे दिलचस्प निर्देश वे होते हैं जो किसी प्रोग्राम को बने हुए कोड में बदलते हैं: बने हुए कोड में बदलना, जांच करना, चलाना, कवरेज वगैरह: यह सुविधा
BuildTool
से लागू की जाती है.कमांड लाइन पर टारगेट पैटर्न का सेट पार्स किया जाता है और
//pkg:all
और//pkg/...
जैसे वाइल्डकार्ड हल किए जाते हैं. इसेAnalysisPhaseRunner.evaluateTargetPatterns()
में लागू किया गया है और Skyframe मेंTargetPatternPhaseValue
के तौर पर फिर से बनाया गया है.लोडिंग/विश्लेषण का फ़ेज़, ऐक्शन ग्राफ़ बनाने के लिए चलाया जाता है. यह कमांड का एक डायरेक्टेड एकाइक्लिक ग्राफ़ होता है, जिसे बिल्ड के लिए एक्ज़ीक्यूट किया जाना चाहिए.
प्रोग्राम चलाने का चरण शुरू हो जाता है. इसका मतलब है कि अनुरोध किए गए टॉप-लेवल टारगेट बनाने के लिए, ज़रूरी हर कार्रवाई को चलाया जाता है.
कमांड लाइन के विकल्प
Bazel को कॉल करने के लिए कमांड-लाइन के विकल्पों के बारे में, OptionsParsingResult
ऑब्जेक्ट में बताया गया है. इसमें "option
classes" से विकल्पों की वैल्यू तक का मैप होता है. "विकल्प क्लास", OptionsBase
की एक सबक्लास होती है. साथ ही, यह एक-दूसरे से जुड़े कमांड लाइन विकल्पों को एक साथ ग्रुप करती है. उदाहरण के लिए:
- प्रोग्रामिंग भाषा (
CppOptions
याJavaOptions
) से जुड़े विकल्प. येFragmentOptions
के सबक्लास होने चाहिए और आखिर में इन्हेंBuildOptions
ऑब्जेक्ट में रैप कर दिया जाता है. - Baze, कार्रवाइयों को लागू करने के तरीके से जुड़े विकल्प (
ExecutionOptions
)
इन विकल्पों को विश्लेषण के चरण में इस्तेमाल करने के लिए डिज़ाइन किया गया है. इन्हें Java में RuleContext.getFragment()
या Starlark में ctx.fragments
के ज़रिए इस्तेमाल किया जा सकता है.
इनमें से कुछ विकल्पों को, प्रोग्राम के लागू होने के दौरान पढ़ा जाता है. उदाहरण के लिए, C++ में शामिल स्कैनिंग की सुविधा का इस्तेमाल करना है या नहीं. हालांकि, इसके लिए हमेशा साफ़ तौर पर प्लंबिंग की ज़रूरत होती है, क्योंकि उस समय BuildConfiguration
उपलब्ध नहीं होता. ज़्यादा जानकारी के लिए, "कॉन्फ़िगरेशन" सेक्शन देखें.
चेतावनी: हम यह दिखाना चाहते हैं कि OptionsBase
इंस्टेंस में बदलाव नहीं किया जा सकता और उनका इस्तेमाल उसी तरह किया जा सकता है जैसे SkyKeys
का हिस्सा. हालांकि, ऐसा नहीं है. इनमें बदलाव करने से, Bazel को ऐसे तरीके से गड़बड़ किया जा सकता है जिसे डीबग करना मुश्किल होता है. माफ़ करें, उन्हें असल में अपरिवर्तनीय बनाना एक बड़ी चुनौती है.
(किसी FragmentOptions
को बनाने के तुरंत बाद उसमें बदलाव करना ठीक है. ऐसा तब करें, जब किसी और को उसका रेफ़रंस रखने का मौका न मिले और equals()
या hashCode()
को उस पर कॉल न किया गया हो.)
बेज़ल, विकल्प क्लास के बारे में ये जानकारी हासिल करते हैं:
- कुछ बेज़ल (
CommonCommandOptions
) में हार्ड वायर वाली हैं - हर Bazel कमांड पर मौजूद
@Command
एनोटेशन से ConfiguredRuleClassProvider
से (ये अलग-अलग प्रोग्रामिंग भाषाओं से जुड़े कमांड लाइन विकल्प हैं)- Starlark के नियम अपने विकल्प भी तय कर सकते हैं (यहां देखें)
हर विकल्प (Starlark से तय किए गए विकल्पों को छोड़कर), FragmentOptions
सबक्लास का एक सदस्य वैरिएबल होता है. इसमें @Option
एनोटेशन होता है, जो कुछ सहायता टेक्स्ट के साथ-साथ कमांड लाइन विकल्प का नाम और टाइप बताता है.
कमांड लाइन के विकल्प की वैल्यू का Java टाइप आम तौर पर आसान होता है (जैसे, कोई स्ट्रिंग, कोई पूर्णांक, कोई बूलियन, कोई लेबल वगैरह). हालांकि, हम ज़्यादा मुश्किल टाइप के विकल्पों के साथ भी काम करते हैं. इस मामले में, कमांड लाइन स्ट्रिंग को डेटा टाइप में बदलने का काम, com.google.devtools.common.options.Converter
को लागू करने पर होता है.
Bazel को दिखने वाला सोर्स ट्री
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
नाम की फ़ाइल में दी जाती है. अगर दोनों मौजूद हैं, तो Bazel BUILD.bazel
को प्राथमिकता देता है. BUILD
फ़ाइलों को अब भी इसलिए स्वीकार किया जाता है, क्योंकि Bazel के पूर्वज Blaze ने इस फ़ाइल के नाम का इस्तेमाल किया था. हालांकि, यह आम तौर पर इस्तेमाल किया जाने वाला पाथ सेगमेंट है. खास तौर पर, Windows पर, जहां फ़ाइल के नाम केस-इन्सेंसिव होते हैं.
पैकेज एक-दूसरे से अलग होते हैं: किसी पैकेज की BUILD
फ़ाइल में बदलाव करने से, दूसरे पैकेज में बदलाव नहीं होता. BUILD
फ़ाइलों को जोड़ना या हटाना,
दूसरे पैकेज को बदल सकता है _can _बदलें, क्योंकि बार-बार होने वाले ग्लोब, पैकेज की सीमाओं पर रुक जाते हैं.
इसलिए, BUILD
फ़ाइल की मौजूदगी से बार-बार होने वाला इवेंट नहीं होता.
BUILD
फ़ाइल का आकलन करने की प्रोसेस को "पैकेज लोड करना" कहा जाता है. इसे PackageFactory
क्लास में लागू किया गया है. यह Starlark इंटरप्रेटर को कॉल करके काम करता है. साथ ही, इसके लिए उपलब्ध नियम क्लास के सेट के बारे में जानकारी ज़रूरी है. पैकेज लोड करने का नतीजा, एक Package
ऑब्जेक्ट होता है. यह ज़्यादातर स्ट्रिंग (टारगेट का नाम) से लेकर टारगेट तक का मैप होता है.
पैकेज लोड करने के दौरान, ग्लोबिंग की वजह से समस्याएं आती हैं: Bazel को हर सोर्स फ़ाइल को साफ़ तौर पर सूची में शामिल करने की ज़रूरत नहीं होती. इसके बजाय, यह ग्लोब (जैसे, glob(["**/*.java"])
) चला सकता है. शेल के विपरीत, यह बार-बार होने वाली ग्लोबिंग के साथ काम करता है, जो सब-डायरेक्ट्री में जाती है (लेकिन सब-पैकेज में नहीं). इसके लिए, फ़ाइल सिस्टम का ऐक्सेस ज़रूरी है. यह प्रोसेस धीमी हो सकती है. इसलिए, हम इसे एक साथ और बेहतर तरीके से चलाने के लिए, सभी तरह की तरकीबें अपनाते हैं.
ग्लोबिंग इन क्लास में लागू किया जाता है:
LegacyGlobber
, एक तेज़ और खुशी से Skyframe के बारे में अनजान globberSkyframeHybridGlobber
, यह एक ऐसा वर्शन है जो Skyframe का इस्तेमाल करता है और "Skyframe रीस्टार्ट" (इसके बारे में नीचे बताया गया है) से बचने के लिए, लेगसी globber पर वापस आ जाता है
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
Bazel के तहत काम करने वाले आकलन फ़्रेमवर्क को Skyframe कहा जाता है. इसका मॉडल यह है कि किसी भी बिल्ड के दौरान, जो भी चीज़ें बनाई जानी हैं उन्हें एक डायरेक्टेड ऐसाइक्लिक ग्राफ़ में व्यवस्थित किया जाता है. इस ग्राफ़ में, डेटा के किसी भी हिस्से से उसकी डिपेंडेंसी पर जाने वाले किनारे होते हैं. डिपेंडेंसी, डेटा के ऐसे अन्य हिस्से होते हैं जिनके बारे में जानने के बाद ही, डेटा का पूरा हिस्सा बनाया जा सकता है.
ग्राफ़ में मौजूद नोड को SkyValue
कहा जाता है और उनके नाम को
SkyKey
कहा जाता है. दोनों में बदलाव नहीं किया जा सकता. इनमें सिर्फ़ ऐसे ऑब्जेक्ट को ऐक्सेस किया जा सकता है जिनमें बदलाव नहीं किया जा सकता. यह इनवैरिएंट, ज़्यादातर मामलों में लागू होता है. अगर ऐसा नहीं होता है, तो हम पूरी कोशिश करते हैं कि इनमें बदलाव न किया जाए या फिर सिर्फ़ ऐसे बदलाव किए जाएं जो बाहर से न दिखें. जैसे, अलग-अलग विकल्पों की क्लास BuildOptions
, जो BuildConfigurationValue
और उसकी SkyKey
की सदस्य है.
इससे यह पता चलता है कि Skyframe में कॉन्फ़िगर किए गए टारगेट जैसे सभी चीज़ों में बदलाव नहीं किया जा सकता.
Skyframe ग्राफ़ को देखने का सबसे आसान तरीका है, bazel dump
--skyframe=deps
को चलाना. इससे ग्राफ़, हर लाइन में एक SkyValue
के तौर पर डंप हो जाता है. ऐसा छोटे बिल्ड के लिए करना सबसे अच्छा होता है, क्योंकि यह बहुत बड़ा हो सकता है.
Skyframe, com.google.devtools.build.skyframe
पैकेज में मौजूद है. इसी तरह के नाम वाले पैकेज com.google.devtools.build.lib.skyframe
में, Skyframe के ऊपर बेज़ल लागू किया गया है. 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()
का इस्तेमाल कर रही है या ऐड-हॉक स्टैटिक कैश मेमोरी "Sस्काईफ़्रेम के पिछले हिस्से के पीछे" रख रही है.
आम तौर पर, हमें इस तरह के तरीके अपनाने की ज़रूरत होती है, क्योंकि हमारे पास आम तौर पर, सैकड़ों हज़ार इन-फ़्लाइट Skyframe नोड होते हैं. साथ ही, Java में लाइटवेट थ्रेड काम नहीं करते.
Starlark
Starlark, डोमेन के लिए खास तौर पर इस्तेमाल की जाने वाली भाषा है. लोग इस भाषा का इस्तेमाल करके, बैजल को कॉन्फ़िगर करने और उसका दायरा बढ़ाने के लिए इस्तेमाल करते हैं. इसे Python के सीमित सबसेट के तौर पर माना जाता है, जिसमें बहुत कम टाइप होते हैं. साथ ही, कंट्रोल फ़्लो पर ज़्यादा पाबंदियां होती हैं. सबसे अहम बात यह है कि एक साथ कई फ़ाइलें पढ़ने की सुविधा चालू करने के लिए, डेटा में बदलाव न होने की गारंटी दी जाती है. यह ट्यूरिंग-कंप्लीट नहीं है. इस वजह से, कुछ (सभी नहीं) उपयोगकर्ता इस भाषा में सामान्य प्रोग्रामिंग टास्क पूरा करने से बचते हैं.
Starlark को net.starlark.java
पैकेज में लागू किया गया है.
इसे यहां, Go से भी अलग से लागू किया जा सकता है. फ़िलहाल, Bazel में इस्तेमाल किया जा रहा Java, एक इंटरप्रेटर है.
Starlark का इस्तेमाल कई कामों के लिए किया जाता है. जैसे:
BUILD
फ़ाइलें. यहां नए बिल्ड टारगेट तय किए जाते हैं. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास, सिर्फ़BUILD
फ़ाइल के कॉन्टेंट और उससे लोड की गई.bzl
फ़ाइलों का ऐक्सेस होता है.MODULE.bazel
फ़ाइल. बाहरी डिपेंडेंसी यहां तय की जाती है. इस संदर्भ में चल रहे Starlark कोड के पास, पहले से तय किए गए कुछ निर्देशों का ऐक्सेस बहुत सीमित होता है..bzl
फ़ाइलें. यहां नए बिल्ड नियम, repo नियम, मॉड्यूल के एक्सटेंशन तय किए जाते हैं. यहां मौजूद Starlark कोड, नए फ़ंक्शन तय कर सकता है और अन्य.bzl
फ़ाइलों से लोड हो सकता है.
BUILD
और .bzl
फ़ाइलों के लिए उपलब्ध बोलियाँ थोड़ी अलग होती हैं, क्योंकि इनमें अलग-अलग चीज़ें बताई जाती हैं. इनके बीच के अंतर की सूची यहां दी गई है.
Starlark के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
लोडिंग/विश्लेषण का चरण
लोड करने/विश्लेषण करने के चरण में, Bazel यह तय करता है कि किसी खास नियम को बनाने के लिए कौनसी कार्रवाइयां ज़रूरी हैं. इसकी बुनियादी यूनिट, "कॉन्फ़िगर किया गया टारगेट" है, जो (टारगेट, कॉन्फ़िगरेशन) पेयर है.
इसे "लोडिंग/विश्लेषण का फ़ेज़" कहा जाता है, क्योंकि इसे दो अलग-अलग हिस्सों में बांटा जा सकता है. पहले इन हिस्सों को क्रम से चलाया जाता था, लेकिन अब ये एक-दूसरे के साथ ओवरलैप हो सकते हैं:
- पैकेज लोड करना, यानी
BUILD
फ़ाइलों को उनPackage
ऑब्जेक्ट में बदलना जो उन्हें दिखाते हैं - कॉन्फ़िगर किए गए टारगेट का विश्लेषण करना. इसका मतलब है कि ऐक्शन ग्राफ़ बनाने के लिए, नियमों को लागू करना
कमांड लाइन पर अनुरोध किए गए कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में, कॉन्फ़िगर किए गए हर टारगेट का विश्लेषण नीचे से किया जाना चाहिए. इसका मतलब है कि पहले लीफ़ नोड का विश्लेषण किया जाना चाहिए. इसके बाद, कमांड लाइन पर मौजूद टारगेट तक का विश्लेषण किया जाना चाहिए. कॉन्फ़िगर किए गए एक टारगेट के विश्लेषण के लिए, इनपुट इस तरह हैं:
- कॉन्फ़िगरेशन. ("वह नियम कैसे बनाया जाए; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म के साथ-साथ कमांड-लाइन के विकल्पों जैसी चीज़ें भी, जिन्हें उपयोगकर्ता C++ कंपाइलर में भेजना चाहता है)
- डायरेक्ट डिपेंडेंसी. डेटा देने वाली ऐसी कंपनियां, जिनके पास फ़िलहाल COVID-19 की जानकारी है, वे विश्लेषण किए जा रहे नियम के लिए उपलब्ध होते हैं. इन्हें इस तरह इसलिए कहा जाता है, क्योंकि ये कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र में जानकारी का "रोल-अप" उपलब्ध कराते हैं. जैसे, क्लासपाथ पर मौजूद सभी .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
, "इस नियम के तहत आने वाली फ़ाइलों के सेट" का धुंधला कॉन्सेप्ट. ये ऐसी फ़ाइलें होती हैं जो तब बनती हैं, जब कॉन्फ़िगर किया गया टारगेट कमांड लाइन पर या genrule के srcs में होता है. - उनकी रनफ़ाइलें, नियमित, और डेटा.
- उनके आउटपुट ग्रुप. ये "फ़ाइलों के अन्य सेट" हैं, जिन्हें नियम से बनाया जा सकता है. इन्हें BUILD में फ़ाइलग्रुप नियम के asset_group एट्रिब्यूट का इस्तेमाल करके और Java में
OutputGroupInfo
प्रोवाइडर की मदद से ऐक्सेस किया जा सकता है.
रनफ़ाइलें
कुछ बाइनरी को चलने के लिए डेटा फ़ाइलों की ज़रूरत होती है. इसका एक उदाहरण, ऐसे टेस्ट हैं जिनमें इनपुट फ़ाइलों की ज़रूरत होती है. Bazel में इसे "रनफ़ाइल" के कॉन्सेप्ट से दिखाया जाता है. "रनफ़ाइल ट्री", किसी खास बाइनरी के लिए डेटा फ़ाइलों की डायरेक्ट्री ट्री होती है. इसे फ़ाइल सिस्टम में सिमलिंक ट्री के तौर पर बनाया जाता है. इसमें अलग-अलग सिमलिंक होते हैं, जो आउटपुट ट्री के सोर्स में मौजूद फ़ाइलों पर ले जाते हैं.
रनफ़ाइलों के सेट को Runfiles
इंस्टेंस के तौर पर दिखाया जाता है. यह सैद्धांतिक तौर पर रनफ़ाइल ट्री में फ़ाइल के पाथ से Artifact
इंस्टेंस तक का मैप होता है, जो इसे दिखाता है. यह दो वजहों से, एक Map
से थोड़ा ज़्यादा पेचीदा है:
- ज़्यादातर मामलों में, किसी फ़ाइल का रनफ़ाइल पाथ उसके एक्ज़ीकपाथ के जैसा ही होता है. हम इसका इस्तेमाल, कुछ रैम बचाने के लिए करते हैं.
- रनफ़ाइल ट्री में कई तरह की लेगसी एंट्री हैं, जिन्हें दिखाना भी ज़रूरी है.
रनफ़ाइल RunfilesProvider
का इस्तेमाल करके इकट्ठा की जाती हैं: इस क्लास का एक इंस्टेंस, कॉन्फ़िगर किए गए टारगेट (जैसे कि लाइब्रेरी) और इसकी ट्रांज़िटिव क्लोज़ ज़रूरतों को दिखाता है. साथ ही, इन्हें नेस्ट किए गए सेट की तरह इकट्ठा किया जाता है (वास्तव में, इन्हें कवर के तहत नेस्ट किए गए सेट का इस्तेमाल करके लागू किया जाता है): हर टारगेट यूनियन अपनी डिपेंडेंसी के रनफ़ाइल को जोड़ता है, फिर डिपेंडेंसी ग्राफ़ में ऊपर की ओर सेट करता है. RunfilesProvider
इंस्टेंस में दो Runfiles
इंस्टेंस होते हैं. पहला, यह तब होता है, जब नियम "data" एट्रिब्यूट के ज़रिए तय किया जाता है और दूसरा हर तरह के इनकमिंग डिपेंडेंसी के लिए होता है. ऐसा इसलिए होता है, क्योंकि
कभी-कभी टारगेट, अलग-अलग रनफ़ाइल दिखाता है. ऐसा तब होता है, जब
डेटा एट्रिब्यूट के ज़रिए इस पर निर्भर किया जाता है, जबकि अन्य के बजाय
यह डेटा एट्रिब्यूट की मदद से तय होता है. यह एक गड़बड़ी है, जिसे हम अब तक ठीक नहीं कर पाए हैं.
बाइनरी वाली रनफ़ाइल को RunfilesSupport
के इंस्टेंस के तौर पर दिखाया जाता है. यह Runfiles
से अलग है, क्योंकि RunfilesSupport
को असल में बनाया जा सकता है. Runfiles
सिर्फ़ एक मैपिंग है. इसके लिए, इन अतिरिक्त कॉम्पोनेंट की ज़रूरत होती है:
- रनफ़ाइल मेनिफ़ेस्ट इनपुट. यह रनफ़ाइल ट्री का क्रम के मुताबिक ब्यौरा है. इसका इस्तेमाल, रनफ़ाइल्स ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. साथ ही, Bazel यह मानता है कि रनफ़ाइल्स ट्री में सिर्फ़ तब बदलाव होता है, जब मेनिफ़ेस्ट के कॉन्टेंट में बदलाव होता है.
- आउटपुट के लिए, रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल, रनटाइम लाइब्रेरी करती हैं, जो रनफ़ाइल ट्री को मैनेज करती हैं. खास तौर पर, Windows पर, जो कभी-कभी सिंबल लिंक के साथ काम नहीं करता.
- Runfiles मिडलमैन. रनफ़ाइल ट्री मौजूद होने के लिए, सिमलिंक ट्री और सिमलिंक जिस आर्टफ़ैक्ट पर ले जाते हैं उसे बनाना ज़रूरी है. डिपेंडेंसी किनारों की संख्या को कम करने के लिए, रनफ़ाइल मिडलमैन का इस्तेमाल इन सभी को दिखाने के लिए किया जा सकता है.
- उस बाइनरी को चलाने के लिए कमांड लाइन आर्ग्युमेंट जिसका इस्तेमाल करके,
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, एक से ज़्यादा प्लैटफ़ॉर्म के लिए बने बिल्ड के साथ काम करता है. इसका मतलब है कि ऐसे बिल्ड जिनमें एक से ज़्यादा आर्किटेक्चर हो सकते हैं, जहां बिल्ड ऐक्शन चलते हैं, और एक से ज़्यादा आर्किटेक्चर जिनके लिए कोड बनाया जाता है. Bazel में इन आर्किटेक्चर को प्लैटफ़ॉर्म कहा जाता है. इनके बारे में पूरी जानकारी यहां दी गई है
कंस्ट्रेंट सेटिंग (जैसे कि "सीपीयू आर्किटेक्चर का सिद्धांत") से लेकर कंस्ट्रेंट वैल्यू (जैसे कि एक खास सीपीयू, जैसे कि x86_64) तक की-वैल्यू मैपिंग की मदद से, प्लैटफ़ॉर्म के बारे में बताया जाता है. हमारे पास @platforms
रिपॉज़िटरी में, सबसे ज़्यादा इस्तेमाल की जाने वाली पाबंदी की सेटिंग और वैल्यू की "डिक्शनरी" है.
टूलचेन का कॉन्सेप्ट इस बात पर आधारित है कि कौनसे प्लैटफ़ॉर्म पर बिल्ड चल रहा है और कौनसे प्लैटफ़ॉर्म टारगेट किए जा रहे हैं. इसके आधार पर, आपको अलग-अलग कंपाइलर का इस्तेमाल करना पड़ सकता है. उदाहरण के लिए, कोई खास C++ टूलचेन किसी खास ओएस पर चल सकता है और कुछ अन्य ओएस को टारगेट कर सकता है. बेज़ल को C++ कंपाइलर तय करना चाहिए, जिसका इस्तेमाल सेट किए गए एक्ज़ीक्यूशन और टारगेट प्लैटफ़ॉर्म के आधार पर किया गया हो. टूलचेन के लिए दस्तावेज़ यहां दिए गए हैं.
ऐसा करने के लिए, टूलचेन को उन प्लैटफ़ॉर्म की सीमाओं के सेट के साथ एनोटेट किया जाता है जिन पर वे काम करते हैं. ऐसा करने के लिए, टूलचेन की परिभाषा को दो हिस्सों में बांटा गया है:
toolchain()
नियम, जो किसी टूलचेन के साथ काम करने वाले एक्ज़ीक्यूशन और टारगेट की सीमाओं के सेट के बारे में बताता है. साथ ही, यह भी बताता है कि यह किस तरह का टूलचेन है, जैसे कि C++ या Java. टूलचेन के टाइप के बारे मेंtoolchain_type()
नियम से पता चलता है- भाषा के हिसाब से एक नियम, जो असल टूलचेन के बारे में बताता है (जैसे कि
cc_toolchain()
)
ऐसा इसलिए किया जाता है, क्योंकि हमें टूलचेन रिज़ॉल्यूशन और किसी खास भाषा के हिसाब से काम करने के लिए, हर टूलचेन की सीमाओं के बारे में जानना होता है.
*_toolchain()
नियमों में इससे ज़्यादा जानकारी होती है, इसलिए उन्हें लोड होने में ज़्यादा समय लगता है.
एक्सीक्यूशन प्लैटफ़ॉर्म को इनमें से किसी एक तरीके से तय किया जाता है:
register_execution_platforms()
फ़ंक्शन का इस्तेमाल करके MODULE.bagel फ़ाइल में- कमांड लाइन पर, --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 के बजाय किसी दूसरी "लिटल लैंग्वेज" का इस्तेमाल किया गया है.
कंस्ट्रेंट
कभी-कभी किसी टारगेट को सिर्फ़ कुछ प्लैटफ़ॉर्म के साथ काम करने वाला तय करना होता है. अफ़सोस की बात है कि Bazel में, इस काम के लिए कई तरीके हैं:
- नियम के हिसाब से पाबंदियां
environment_group()
/environment()
- प्लैटफ़ॉर्म से जुड़ी पाबंदियां
नियम के हिसाब से पाबंदियों का इस्तेमाल, ज़्यादातर Google में Java नियमों के लिए किया जाता है. ये पाबंदियां अब बंद होने वाली हैं और ये Bazel में उपलब्ध नहीं हैं. हालांकि, सोर्स कोड में इनका रेफ़रंस हो सकता है. इसे कंट्रोल करने वाले एट्रिब्यूट को
constraints=
कहा जाता है .
एनवायरमेंट_ग्रुप() और एनवायरमेंट()
ये नियम, लेगसी प्रोसेस के तहत आते हैं. साथ ही, इनका ज़्यादा इस्तेमाल नहीं किया जाता.
सभी बिल्ड रूल से यह बताया जा सकता है कि उन्हें किस "एनवायरमेंट" के लिए बनाया जा सकता है और जहां "एनवायरमेंट", 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, visibility नाम के तंत्र की मदद से ऐसा करता है: आपके पास यह बताने का विकल्प होता है कि किसी खास टारगेट पर सिर्फ़ visibility एट्रिब्यूट का इस्तेमाल करके निर्भर किया जा सकता है. यह एट्रिब्यूट थोड़ा खास है, क्योंकि इसमें लेबल की सूची होती है. हालांकि, ये लेबल किसी खास टारगेट के पॉइंटर के बजाय, पैकेज के नामों पर पैटर्न को कोड में बदल सकते हैं. (हां, यह डिज़ाइन की एक गलती है.)
इसे इन जगहों पर लागू किया जाता है:
RuleVisibility
इंटरफ़ेस, कॉन्टेंट दिखने की जानकारी दिखाता है. यह एक कॉन्स्टेंट (पूरी तरह से सार्वजनिक या पूरी तरह से निजी) या लेबल की सूची हो सकती है.- लेबल, पैकेज ग्रुप (पैकेज की पहले से तय सूची), पैकेज (
//pkg:__pkg__
) या पैकेज के सबसे छोटे उपवृक्ष (//pkg:__subpackages__
) में से किसी एक का रेफ़रंस दे सकते हैं. यह कमांड-लाइन सिंटैक्स से अलग है, जिसमें//pkg:*
या//pkg/...
का इस्तेमाल किया जाता है. - पैकेज ग्रुप को अपने टारगेट (
PackageGroup
) और कॉन्फ़िगर किए गए टारगेट (PackageGroupConfiguredTarget
) के तौर पर लागू किया जाता है. अगर हम चाहें, तो इनके बजाय आसान नियमों का इस्तेमाल किया जा सकता है. इनका लॉजिक इनकी मदद से लागू किया जाता है:PackageSpecification
, जो//pkg/...
जैसे किसी एक पैटर्न से जुड़ा होता है;PackageGroupContents
, जो किसी एकpackage_group
केpackages
एट्रिब्यूट से जुड़ा होता है; औरPackageSpecificationProvider
, जोpackage_group
और उसके ट्रांसिशन एट्रिब्यूटincludes
पर एग्रीगेट करता है. - दिखने की सेटिंग वाले लेबल की सूचियों को डिपेंडेंसी में बदलने का काम,
DependencyResolver.visitTargetVisibility
और कुछ अन्य जगहों पर किया जाता है. - असल जांच
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
में पूरी होती है
नेस्ट किए गए सेट
आम तौर पर, कॉन्फ़िगर किया गया टारगेट, अपनी डिपेंडेंसी से फ़ाइलों का एक सेट इकट्ठा करता है, अपनी फ़ाइलें जोड़ता है, और एग्रीगेट किए गए सेट को ट्रांज़िशनरी जानकारी देने वाले प्रोवाइडर में रैप करता है, ताकि उस पर निर्भर कॉन्फ़िगर किए गए टारगेट भी ऐसा कर सकें. उदाहरण:
- बिल्ड के लिए इस्तेमाल की जाने वाली C++ हेडर फ़ाइलें
- ऑब्जेक्ट फ़ाइलें, जो
cc_library
के ट्रांसिशन क्लोज़र को दिखाती हैं - .jar फ़ाइलों का सेट, जिसे Java नियम को कंपाइल या चलाने के लिए क्लासपाथ पर होना चाहिए
- Python नियम के ट्रांसीटिव क्लोज़र में मौजूद Python फ़ाइलों का सेट
अगर हमने List
या Set
का इस्तेमाल करके, इसे आसान तरीके से किया, तो हमें ज़्यादा मेमोरी का इस्तेमाल करना पड़ेगा: अगर N नियमों की एक चेन है और हर नियम एक फ़ाइल जोड़ता है, तो हमारे पास 1+2+...+N कलेक्शन मेंबर होंगे.
इस समस्या से निपटने के लिए, हमने NestedSet
का कॉन्सेप्ट लिया है. यह डेटा स्ट्रक्चर है, जो अन्य NestedSet
इंस्टेंस और अपने कुछ सदस्यों से मिलकर बनता है. इस तरह, सेट का एक दिशा-निर्देश वाला असाइकलिक ग्राफ़ बनता है. इन्हें बदला नहीं जा सकता और सदस्यों को फिर से इस्तेमाल किया जा सकता है. हम कई क्रम (NestedSet.Order
) तय करते हैं: प्रीऑर्डर, पोस्टऑर्डर, टॉपोलॉजिकल (कोई नोड हमेशा अपने पैरंट के बाद आता है) और "इस पर ध्यान न दें, लेकिन हर बार यह एक जैसा होना चाहिए".
Starlark में, इसी डेटा स्ट्रक्चर को depset
कहा जाता है.
आर्टफ़ैक्ट और कार्रवाइयां
असल बिल्ड में, उन निर्देशों का एक सेट होता है जिन्हें उपयोगकर्ता के मनमुताबिक आउटपुट पाने के लिए चलाया जाना चाहिए. निर्देशों को क्लास Action
के इंस्टेंस के तौर पर दिखाया जाता है और फ़ाइलों को क्लास Artifact
के इंस्टेंस के तौर पर दिखाया जाता है. इन्हें दो हिस्सों में बांटा गया है. साथ ही, इनमें निर्देश और असाइकलिक ग्राफ़ होते हैं. इन्हें "ऐक्शन ग्राफ़" कहा जाता है.
आर्टफ़ैक्ट दो तरह के होते हैं: सोर्स आर्टफ़ैक्ट (वे आर्टफ़ैक्ट जो Bazel के शुरू होने से पहले उपलब्ध होते हैं) और डेरिव्ड आर्टफ़ैक्ट (वे आर्टफ़ैक्ट जिन्हें बनाना ज़रूरी होता है). डेरिव्ड आर्टफ़ैक्ट कई तरह के हो सकते हैं:
- **सामान्य आर्टफ़ैक्ट. **इन फ़ाइलों के अप-टू-डेट होने की जांच, उनके चेकसम का हिसाब लगाकर की जाती है. इसके लिए, mtime को शॉर्टकट के तौर पर इस्तेमाल किया जाता है. अगर फ़ाइल के mtime में कोई बदलाव नहीं होता है, तो हम फ़ाइल का चेकसम नहीं करते.
- समाधान नहीं किए गए सिंबललिंक आर्टफ़ैक्ट. readlink() को कॉल करके, इनके अप-टू-डेट होने की जांच की जाती है. सामान्य आर्टफ़ैक्ट के मुकाबले, ये डैंगलिंग स्लिंक्स हो सकते हैं. आम तौर पर, इसका इस्तेमाल उन मामलों में किया जाता है जहां कुछ फ़ाइलों को किसी तरह के संग्रह में पैक किया जाता है.
- ट्री आर्टफ़ैक्ट. ये एक फ़ाइल नहीं, बल्कि डायरेक्ट्री ट्री हैं. इनकी जांच करके यह पता लगाया जाता है कि वे अप-टू-डेट हैं या नहीं. इसके लिए, इनमें मौजूद फ़ाइलों और उनके कॉन्टेंट की जांच की जाती है. इन्हें
TreeArtifact
के तौर पर दिखाया जाता है. - मेटाडेटा के लगातार आर्टफ़ैक्ट. इन आर्टफ़ैक्ट में बदलाव करने पर, फिर से बनाने की प्रोसेस ट्रिगर नहीं होती. इसका इस्तेमाल सिर्फ़ बिल्ड स्टैंप की जानकारी के लिए किया जाता है: हम सिर्फ़ इसलिए फिर से बिल्ड नहीं करना चाहते, क्योंकि मौजूदा समय बदल गया है.
सोर्स आर्टफ़ैक्ट, ट्री आर्टफ़ैक्ट या हल नहीं किए गए सिंबललिंक आर्टफ़ैक्ट क्यों नहीं हो सकते, इसकी कोई बुनियादी वजह नहीं है. ऐसा इसलिए है, क्योंकि हमने इसे अब तक लागू नहीं किया है. हालांकि, हमें इसे लागू करना चाहिए -- BUILD
फ़ाइल में सोर्स डायरेक्ट्री का रेफ़रंस देना, Bazel की कुछ ऐसी गड़बड़ियों में से एक है जो लंबे समय से मौजूद हैं. हमारे पास इस तरह के काम करने वाला एक लागू तरीका है, जिसे BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM प्रॉपर्टी से चालू किया जाता है
Artifact
के एक खास तरह के उदाहरण हैं, मध्यस्थ. इन्हें Artifact
इंस्टेंस से दिखाया जाता है, जो MiddlemanAction
के आउटपुट होते हैं. इनका इस्तेमाल कुछ चीज़ों को
खास तौर पर सामने रखने के लिए किया जाता है:
- आर्टफ़ैक्ट को एक साथ ग्रुप करने के लिए, एग्रीगेट करने वाले मिडलमैन का इस्तेमाल किया जाता है. ऐसा इसलिए किया जाता है, ताकि अगर कई कार्रवाइयां इनपुट के एक ही बड़े सेट का इस्तेमाल करती हैं, तो हमारे पास N*M डिपेंडेंसी एज न हों, सिर्फ़ N+M (इन्हें नेस्ट किए गए सेट से बदला जा रहा है)
- डिपेंडेंसी मिडलमैन को शेड्यूल करने से यह पक्का होता है कि एक कार्रवाई, दूसरी कार्रवाई से पहले पूरी हो.
ज़्यादातर इसका इस्तेमाल लिंटिंग के लिए किया जाता है. हालांकि, C++ कंपाइलेशन के लिए भी इसका इस्तेमाल किया जाता है (ज़्यादा जानकारी के लिए
CcCompilationContext.createMiddleman()
देखें) - Runfiles मिडलमैन का इस्तेमाल, Runfiles ट्री की मौजूदगी की पुष्टि करने के लिए किया जाता है, ताकि किसी को आउटपुट मेनिफ़ेस्ट और Runfiles ट्री के रेफ़रंस वाले हर आर्टफ़ैक्ट पर अलग से निर्भर न होना पड़े.
ऐक्शन को सबसे अच्छी तरह से एक ऐसे निर्देश के तौर पर समझा जा सकता है जिसे चलाने के लिए, एक खास तरह के एनवायरमेंट की ज़रूरत होती है. साथ ही, यह भी ज़रूरी है कि यह निर्देश, एक खास तरह के आउटपुट जनरेट करता हो. किसी कार्रवाई के ब्यौरे के मुख्य कॉम्पोनेंट ये हैं:
- वह कमांड लाइन जिसे चलाना है
- इसके लिए ज़रूरी इनपुट आर्टफ़ैक्ट
- ऐसे एनवायरमेंट वैरिएबल जिन्हें सेट करना ज़रूरी है
- ऐसे एनोटेशन जिनमें उस प्लैटफ़ॉर्म के बारे में बताया गया हो जिस पर इसे चलाना है \
कुछ और खास मामले भी हैं, जैसे कि ऐसी फ़ाइल लिखना जिसका कॉन्टेंट, Bazel को पता हो. ये AbstractAction
के सबक्लास हैं. ज़्यादातर कार्रवाइयां SpawnAction
या StarlarkAction
होती हैं (ये एक ही हैं, इसलिए इन्हें अलग-अलग क्लास नहीं माना जाना चाहिए). हालांकि, Java और C++ के अपने ऐक्शन टाइप (JavaCompileAction
, CppCompileAction
, और CppLinkAction
) होते हैं.
आखिर में हम सब कुछ SpawnAction
में ले जाना चाहते हैं; JavaCompileAction
काफ़ी करीब है, लेकिन .d फ़ाइल पार्स करने और स्कैन करने की सुविधा की वजह से C++ एक खास केस है.
ऐक्शन ग्राफ़ को ज़्यादातर Skyframe ग्राफ़ में "एम्बेड" किया जाता है: कॉन्सेप्ट के हिसाब से, किसी ऐक्शन को लागू करने को ActionExecutionFunction
को कॉल करने के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से स्काईफ़्रेम डिपेंडेंसी एज पर मैप करने की जानकारी ActionExecutionFunction.getInputDeps()
और Artifact.key()
में दी गई है. साथ ही, स्काईफ़्रेम किनारों की संख्या को कम रखने के लिए, इसमें कुछ ऑप्टिमाइज़ेशन शामिल किए गए हैं:
- डेरिव्ड आर्टफ़ैक्ट के पास अपने
SkyValue
नहीं होते. इसके बजाय,Artifact.getGeneratingActionKey()
का इस्तेमाल, उसे जनरेट करने वाले ऐक्शन की कुंजी का पता लगाने के लिए किया जाता है - नेस्ट किए गए सेट की अपनी स्काईफ़्रेम कुंजी होती है.
शेयर की गई कार्रवाइयां
कुछ कार्रवाइयां, कॉन्फ़िगर किए गए कई टारगेट से जनरेट होती हैं. Starlark नियमों के दायरे में ज़्यादा कार्रवाइयां नहीं आती हैं, क्योंकि उन्हें सिर्फ़ अपने कॉन्फ़िगरेशन और पैकेज के हिसाब से तय की गई डायरेक्ट्री में, डेरिव्ड ऐक्शन डालने की अनुमति होती है. हालांकि, एक ही पैकेज में मौजूद नियमों में अंतर हो सकता है. वहीं, Java में लागू किए गए नियमों से, डेरिव्ड आर्टफ़ैक्ट को कहीं भी डाला जा सकता है.
इसे एक गलत सुविधा माना जाता है, लेकिन इससे छुटकारा पाना बहुत मुश्किल है, क्योंकि इससे एक्ज़ीक्यूशन में लगने वाले समय में काफ़ी बचत होती है. उदाहरण के लिए, किसी सोर्स फ़ाइल को प्रोसेस करने की ज़रूरत होती है और उस फ़ाइल का रेफ़रंस एक से ज़्यादा नियमों (हैंडवेव-हैंडवेव) से लगाया जाता है. हालांकि, इसके लिए कुछ रैम की ज़रूरत होती है: शेयर की गई कार्रवाई के हर उदाहरण को मेमोरी में अलग से सेव करना पड़ता है.
अगर दो कार्रवाइयां एक ही आउटपुट फ़ाइल जनरेट करती हैं, तो वे एक जैसी होनी चाहिए:
उनमें एक जैसे इनपुट, एक जैसे आउटपुट होने चाहिए और एक ही कमांड लाइन को चलाना चाहिए. यह समानता संबंध Actions.canBeShared()
में लागू किया जाता है और हर कार्रवाई को देखकर, विश्लेषण और निष्पादन के चरणों के बीच इसकी पुष्टि की जाती है.
इसे SkyframeActionExecutor.findAndStoreArtifactConflicts()
में लागू किया गया है और यह बेज़ल की उन कुछ जगहों में से एक है जहां बिल्ड को "ग्लोबल" व्यू की ज़रूरत होती है.
लागू करने का चरण
इसके बाद, Bazel असल में बिल्ड ऐक्शन चलाना शुरू करता है. जैसे, आउटपुट देने वाले कमांड.
विश्लेषण के चरण के बाद, Baze सबसे पहला काम यह तय करना
करता है कि कौनसी आर्टफ़ैक्ट बनाने की ज़रूरत है. इसके लिए लॉजिक, TopLevelArtifactHelper
में कोड में बदला गया है. यह कमांड लाइन पर कॉन्फ़िगर किए गए टारगेट का filesToBuild
और खास आउटपुट ग्रुप का कॉन्टेंट है. इसका मकसद, "अगर यह टारगेट कमांड लाइन पर है, तो इन आर्टफ़ैक्ट को बनाएं" को साफ़ तौर पर बताना है.
अगला चरण, एक्सीक्यूशन रूट बनाना है. Baज़ल के पास फ़ाइल सिस्टम (--package_path
) में, अलग-अलग जगहों से सोर्स पैकेज पढ़ने का विकल्प होता है. इसलिए, इसे पूरे सोर्स ट्री के साथ स्थानीय तौर पर लागू की जाने वाली कार्रवाइयों की जानकारी देनी होगी. इसे क्लास SymlinkForest
मैनेज करता है. यह विश्लेषण के फ़ेज़ में इस्तेमाल किए गए हर टारगेट को ध्यान में रखकर काम करता है. साथ ही, एक डायरेक्ट्री ट्री बनाता है, जो हर पैकेज को इस्तेमाल किए गए टारगेट के साथ उसकी असल जगह से लिंक करता है. इसके अलावा, --package_path
को ध्यान में रखते हुए, कमांड के लिए सही पाथ पास किए जा सकते हैं.
ऐसा नहीं करना चाहिए, क्योंकि:
- जब किसी पैकेज को पैकेज पाथ की एक एंट्री से दूसरी एंट्री में ले जाया जाता है, तो यह ऐक्शन कमांड लाइन बदल देता है. आम तौर पर, ऐसा होता रहता है
- अगर किसी कार्रवाई को स्थानीय तौर पर की जाने वाली कार्रवाई की तुलना में दूर से चलाया जाता है, तो इसका नतीजा अलग कमांड लाइन के तौर पर दिखता है.
- इसके लिए, इस्तेमाल किए जा रहे टूल के हिसाब से कमांड लाइन ट्रांसफ़ॉर्मेशन की ज़रूरत होती है (जैसे, Java क्लासपाथ और C++ शामिल पाथ के बीच का अंतर)
- किसी ऐक्शन की कमांड लाइन बदलने पर, उसकी ऐक्शन कैश मेमोरी एंट्री अमान्य हो जाती है
--package_path
के इस्तेमाल पर धीरे-धीरे रोक लगाई जा रही है
इसके बाद, बेज़ल ऐक्शन ग्राफ़ (दो हिस्सों में बंटे, डायरेक्ट ग्राफ़ में ऐक्शन और उनके इनपुट और आउटपुट आर्टफ़ैक्ट) को ट्रैक करना और ऐक्शन चलाना शुरू कर देता है.
हर कार्रवाई को SkyValue
क्लास ActionExecutionValue
के इंस्टेंस से दिखाया जाता है.
कोई कार्रवाई करने में ज़्यादा समय लगता है. इसलिए, हमारे पास कैश मेमोरी से जुड़ी कुछ लेयर हैं, जिन्हें Skyframe के पीछे से हिट किया जा सकता है:
ActionExecutionFunction.stateMap
मेंActionExecutionFunction
के Skyframe को फिर से शुरू करने की लागत कम करने के लिए डेटा शामिल है- लोकल ऐक्शन कैश मेमोरी में, फ़ाइल सिस्टम की स्थिति के बारे में डेटा मौजूद होता है
- रिमोट एक्ज़िक्यूशन सिस्टम में आम तौर पर खुद की कैश मेमोरी भी होती है
लोकल ऐक्शन कैश मेमोरी
यह कैश, Skyframe के पीछे मौजूद एक और लेयर है. भले ही, Skyframe में कोई कार्रवाई फिर से की गई हो, फिर भी यह स्थानीय ऐक्शन कैश में हिट हो सकती है. यह, लोकल फ़ाइल सिस्टम की स्थिति दिखाता है और इसे डिस्क पर सीरियलाइज़ किया जाता है. इसका मतलब है कि जब कोई नया Bazel सर्वर शुरू किया जाता है, तो 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
की रनफ़ाइलों को टूल के हिस्से के तौर पर गिना जाता है.
रणनीतियों (या ऐक्शन कॉन्टेक्स्ट!) के बारे में ज़्यादा जानकारी:
- कार्रवाइयां चलाने के लिए अलग-अलग रणनीतियों के बारे में जानकारी यहां उपलब्ध है.
- डाइनैमिक रणनीति के बारे में जानकारी, जिसमें हम स्थानीय और रिमोट, दोनों तरह से कार्रवाई करते हैं, ताकि यह देखा जा सके कि कौनसी कार्रवाई पहले पूरी होती है. इस बारे में ज़्यादा जानकारी यहां दी गई है.
- स्थानीय तौर पर कार्रवाइयां करने की बारीकियों के बारे में जानकारी यहां उपलब्ध है.
लोकल रिसोर्स मैनेजर
Bazel, कई कार्रवाइयों को एक साथ चल सकता है. एक साथ कई स्थानीय कार्रवाइयां चलानी चाहिए या नहीं, यह कार्रवाई के हिसाब से अलग-अलग होता है: किसी कार्रवाई के लिए ज़्यादा संसाधनों की ज़रूरत होने पर, एक ही समय पर कम इंस्टेंस चलाए जाने चाहिए, ताकि स्थानीय मशीन पर लोड कम हो.
इसे ResourceManager
क्लास में लागू किया गया है: हर कार्रवाई के लिए, ResourceSet
इंस्टेंस (सीपीयू और रैम) के तौर पर, ज़रूरी लोकल संसाधनों के अनुमान के साथ एनोटेट करना होगा. इसके बाद, जब ऐक्शन कॉन्टेक्स्ट ऐसा कुछ करते हैं जिसके लिए स्थानीय संसाधनों की ज़रूरत होती है, तो वे ResourceManager.acquireResources()
को कॉल करते हैं और ज़रूरी संसाधन उपलब्ध होने तक ब्लॉक रहते हैं.
लोकल रिसॉर्स मैनेजमेंट के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
आउटपुट डायरेक्ट्री का स्ट्रक्चर
हर ऐक्शन के लिए, आउटपुट डायरेक्ट्री में एक अलग जगह की ज़रूरत होती है, जहां वह अपने आउटपुट डालता है. आम तौर पर, डेरिव्ड आर्टफ़ैक्ट की जगह इस तरह होती है:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
किसी खास कॉन्फ़िगरेशन से जुड़ी डायरेक्ट्री का नाम कैसे तय किया जाता है? पसंद की दो प्रॉपर्टी अलग-अलग होती हैं:
- अगर एक ही बिल्ड में दो कॉन्फ़िगरेशन हो सकते हैं, तो उनके पास अलग-अलग डायरेक्ट्री होनी चाहिए, ताकि दोनों के पास एक ही ऐक्शन का अपना वर्शन हो. अगर ऐसा नहीं है और दोनों कॉन्फ़िगरेशन एक ही आउटपुट फ़ाइल बनाने वाले ऐक्शन की कमांड लाइन के बारे में अलग-अलग हैं, तो Bazel को यह नहीं पता होता कि कौनसा ऐक्शन चुनना है. इसे "ऐक्शन का विरोध" कहा जाता है
- अगर दो कॉन्फ़िगरेशन "लगभग" एक ही चीज़ को दिखाते हैं, तो उनका नाम एक ही होना चाहिए, ताकि कमांड लाइन मैच होने पर, एक में की गई कार्रवाइयों का फिर से इस्तेमाल किया जा सके: उदाहरण के लिए, Java कंपाइलर के कमांड लाइन विकल्पों में किए गए बदलावों की वजह से, C++ कंपाइल करने की कार्रवाइयां फिर से नहीं चलाई जानी चाहिए.
अब तक, हम इस समस्या को हल करने का कोई ऐसा तरीका नहीं ढूंढ पाए हैं जो कॉन्फ़िगरेशन ट्रिम करने की समस्या से मिलता-जुलता हो. विकल्पों के बारे में ज़्यादा लंबी जानकारी यहां उपलब्ध है. मुख्य समस्या वाले क्षेत्र हैं Starlark के नियम (जिनके लेखक आम तौर पर बेज़ेल के बारे में अच्छी तरह से नहीं जानते) और पहलू हैं, जो उन चीज़ों में एक और आयाम जोड़ते हैं जो "एक जैसी" आउटपुट फ़ाइल बना सकती हैं.
फ़िलहाल, कॉन्फ़िगरेशन के लिए पाथ सेगमेंट <CPU>-<compilation mode>
है. इसमें अलग-अलग सफ़िक्स जोड़े गए हैं, ताकि Java में लागू किए गए कॉन्फ़िगरेशन ट्रांज़िशन से ऐक्शन में कोई विरोध न हो. इसके अलावा, Starlark कॉन्फ़िगरेशन ट्रांज़िशन के सेट का चेकसम जोड़ा गया है, ताकि उपयोगकर्ता कार्रवाई से जुड़ी समस्याएं पैदा न कर सकें. यह पूरी तरह से सही नहीं है. इसे OutputDirectories.buildMnemonic()
में लागू किया गया है. यह हर कॉन्फ़िगरेशन फ़्रैगमेंट पर निर्भर करता है, जो आउटपुट डायरेक्ट्री के नाम में अपना हिस्सा जोड़ता है.
जांच
Bazel में टेस्ट चलाने के लिए, कई सुविधाएं उपलब्ध हैं. यह इनके साथ काम करता है:
- दूर से टेस्ट करना (अगर रिमोट एक्ज़ीक्यूशन बैकएंड उपलब्ध है)
- एक साथ कई बार टेस्ट चलाना (डेटा इकट्ठा करने या समय का डेटा इकट्ठा करने के लिए)
- टेस्ट को अलग-अलग हिस्सों में बांटना (तेज़ी से टेस्ट करने के लिए, एक ही टेस्ट में टेस्ट केस को कई प्रोसेस में बांटना)
- काम न करने वाले टेस्ट फिर से चलाना
- टेस्ट को टेस्ट सुइट में ग्रुप करना
टेस्ट, रेगुलर तौर पर कॉन्फ़िगर किए गए ऐसे टारगेट होते हैं जिनमें TestProvider होता है. इससे यह पता चलता है कि टेस्ट को कैसे चलाया जाना चाहिए:
- ऐसे आर्टफ़ैक्ट जिनकी बिल्डिंग के नतीजे में टेस्ट चल रहा है. यह एक "कैश मेमोरी का स्टेटस" फ़ाइल है, जिसमें
TestResultData
मैसेज को सीरियलाइज़ किया गया है - टेस्ट को कितनी बार चलाना चाहिए
- टेस्ट को कितने हिस्सों में बांटना है
- टेस्ट को कैसे चलाया जाना चाहिए, इस बारे में कुछ पैरामीटर (जैसे, टेस्ट टाइम आउट)
यह तय करना कि कौनसे टेस्ट चलाने हैं
यह तय करना कि कौनसे टेस्ट चलाए जाएं, एक जटिल प्रोसेस है.
सबसे पहले, टारगेट पैटर्न को पार्स करने के दौरान, टेस्ट सुइट को बार-बार बड़ा किया जाता है. TestsForTargetPatternFunction
में, ज़्यादा क्रिएटर्स के लिए उपलब्ध कराने की सुविधा लागू की गई है. एक तरह से यह आश्चर्य की बात है कि अगर किसी टेस्ट सुइट में कोई टेस्ट नहीं है, तो इसका मतलब है कि उसके पैकेज में मौजूद हर टेस्ट के लिए ऐसा है. इसे Package.beforeBuild()
में, सुइट के नियमों की जांच करने के लिए $implicit_tests
नाम का इंप्लिसिट एट्रिब्यूट जोड़कर लागू किया जाता है.
इसके बाद, कमांड लाइन के विकल्पों के हिसाब से, टेस्ट को साइज़, टैग, टाइम आउट, और भाषा के हिसाब से फ़िल्टर किया जाता है. इसे TestFilter
में लागू किया जाता है और टारगेट पार्स करने के दौरान, TargetPatternPhaseFunction.determineTests()
से इसे कॉल किया जाता है. साथ ही, नतीजे को TargetPatternPhaseValue.getTestsToRunLabels()
में डाला जाता है. फ़िल्टर किए जा सकने वाले नियम एट्रिब्यूट को कॉन्फ़िगर नहीं किया जा सकता, क्योंकि यह विश्लेषण के चरण से पहले होता है. इसलिए, कॉन्फ़िगरेशन उपलब्ध नहीं होता.
इसके बाद, इसे BuildView.createResult()
में और प्रोसेस किया जाता है: जिन टारगेट का विश्लेषण नहीं हो पाया उन्हें फ़िल्टर कर दिया जाता है और टेस्ट को एक्सक्लूज़िव और नॉन-एक्सक्लूज़िव टेस्ट में बांट दिया जाता है. इसके बाद, इसे AnalysisResult
में डाल दिया जाता है. इससे ExecutionTool
को पता चलता है कि कौनसे टेस्ट चलाने हैं.
इस पूरी प्रोसेस को ज़्यादा पारदर्शी बनाने के लिए, tests()
क्वेरी ऑपरेटर (TestsFunction
में लागू किया गया) उपलब्ध है. इससे यह पता चलता है कि कमांड लाइन पर किसी खास टारगेट के बताए जाने पर कौनसे टेस्ट चलाए जाते हैं. माफ़ करें, इसे फिर से लागू किया जा रहा है. इसलिए, हो सकता है कि यह ऊपर बताए गए तरीके से कई मायनों में अलग हो.
चल रही जांच
कैश मेमोरी की स्थिति के आर्टफ़ैक्ट का अनुरोध करके, टेस्ट चलाए जाते हैं. इसके बाद, TestRunnerAction
को लागू किया जाता है. यह --test_strategy
कमांड लाइन विकल्प से चुने गए TestActionContext
को कॉल करता है. TestActionContext
, टेस्ट को अनुरोध किए गए तरीके से चलाता है.
टेस्ट, एक खास प्रोटोकॉल के हिसाब से चलाए जाते हैं. यह प्रोटोकॉल, एनवायरमेंट वैरिएबल का इस्तेमाल करके, टेस्ट को यह बताता है कि उनसे क्या उम्मीद की जा रही है. Basel को टेस्ट से क्या उम्मीदें हैं और कौनसे टेस्ट बेज़ल से हो सकते हैं, इस बारे में ज़्यादा जानकारी यहां दी गई है. सबसे आसान तरीके से, 0 वाले एक्सिट कोड का मतलब है कि प्रोसेस पूरी हो गई है. किसी भी अन्य कोड का मतलब है कि प्रोसेस पूरी नहीं हुई है.
कैश मेमोरी की स्थिति वाली फ़ाइल के अलावा, हर जांच प्रोसेस से कई अन्य फ़ाइलें बनती हैं. इन्हें "टेस्ट लॉग डायरेक्ट्री" में डाला जाता है. यह टारगेट कॉन्फ़िगरेशन की आउटपुट डायरेक्ट्री की सबडायरेक्ट्री होती है, जिसे testlogs
कहा जाता है:
test.xml
, JUnit-शैली की एक एक्सएमएल फ़ाइल, जिसमें टेस्ट शार्ड में अलग-अलग टेस्ट केस की जानकारी दी गई हैtest.log
, टेस्ट का कंसोल आउटपुट. stdout और stderr को अलग नहीं किया गया है.test.outputs
, "बिना एलान की गई आउटपुट डायरेक्ट्री"; इसका इस्तेमाल उन टेस्ट के लिए किया जाता है जो टर्मिनल पर प्रिंट करने के अलावा, फ़ाइलों को भी आउटपुट करना चाहते हैं.
टेस्ट को लागू करने के दौरान दो चीज़ें हो सकती हैं, जो सामान्य टारगेट बनाते समय नहीं हो सकतीं: एक्सक्लूज़िव टेस्ट लागू करना और आउटपुट स्ट्रीम करना.
कुछ टेस्ट को एक्सक्लूज़िव मोड में चलाना ज़रूरी होता है. उदाहरण के लिए, इन्हें अन्य टेस्ट के साथ नहीं चलाना चाहिए. इसे जांच के नियम में tags=["exclusive"]
जोड़कर या --test_strategy=exclusive
के साथ जांच चलाकर पाया जा सकता है . हर खास जांच को एक अलग SkyFrame के ज़रिए चलाया जाता है. इसमें, "मुख्य" बिल्ड के बाद टेस्ट को लागू करने का अनुरोध किया जाता है. इसे SkyframeExecutor.runExclusiveTest()
में लागू किया गया है.
सामान्य कार्रवाइयों के उलट, जिनका टर्मिनल आउटपुट कार्रवाई पूरी होने पर डंप हो जाता है, उपयोगकर्ता टेस्ट के आउटपुट को स्ट्रीम करने का अनुरोध कर सकता है, ताकि उन्हें लंबे समय तक चलने वाले टेस्ट की प्रोग्रेस के बारे में जानकारी मिल सके. इसकी जानकारी, --test_output=streamed
कमांड लाइन विकल्प से मिलती है. इससे, खास टेस्ट को चलाने का मतलब है, ताकि अलग-अलग टेस्ट के आउटपुट एक-दूसरे में न मिलें.
इसे StreamedTestOutput
क्लास में लागू किया गया है. यह काम, टेस्ट की test.log
फ़ाइल में हुए बदलावों को पोल करके करता है. साथ ही, Bazel के नियमों वाले टर्मिनल में नए बाइट डालता है.
चलाए गए टेस्ट के नतीजे, इवेंट बस पर उपलब्ध होते हैं. इसके लिए, TestAttempt
, TestResult
या TestingCompleteEvent
जैसे अलग-अलग इवेंट को देखा जाता है. इन्हें बिल्ड इवेंट प्रोटोकॉल में डाला जाता है और AggregatingTestListener
की मदद से कंसोल में भेजा जाता है.
कवरेज कलेक्शन
कवरेज की रिपोर्ट, 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 के अलग-अलग वर्शन के बीच अलग-अलग किया जा सकता है. आम तौर पर, इन अंतरों को हटा दिया जाता है और हम इनमें से किसी एक के आधार पर काम करते हैं.
हम एक "कवरेज रिपोर्ट" भी जनरेट करते हैं जो बेज़ल इन्वेशन में हर टेस्ट के लिए इकट्ठा किए गए कवरेज को मर्ज करती है. इसे CoverageReportActionFactory
मैनेज करता है और BuildView.createResult()
से कॉल किया जाता है . यह, पहले टेस्ट के :coverage_report_generator
एट्रिब्यूट को देखकर, ज़रूरी टूल का ऐक्सेस पाता है.
क्वेरी इंजन
बेज़ल से अलग-अलग ग्राफ़ के बारे में बहुत कुछ जानने के लिए बहुत कम भाषा का इस्तेमाल किया जाता है. यहां क्वेरी के ये टाइप दिए गए हैं:
bazel query
का इस्तेमाल, टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel cquery
का इस्तेमाल, कॉन्फ़िगर किए गए टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel aquery
का इस्तेमाल, ऐक्शन ग्राफ़ की जांच करने के लिए किया जाता है
इनमें से हर सुविधा को AbstractBlazeQueryEnvironment
की सबक्लास बनाकर लागू किया जाता है.
QueryFunction
को सबक्लास करके, क्वेरी के अन्य फ़ंक्शन जोड़े जा सकते हैं. क्वेरी के नतीजों को स्ट्रीम करने की अनुमति देने के लिए, उन्हें किसी डेटा स्ट्रक्चर में इकट्ठा करने के बजाय, query2.engine.Callback
को QueryFunction
में पास किया जाता है. QueryFunction
, उन नतीजों के लिए इसे कॉल करता है जिन्हें उसे दिखाना है.
क्वेरी का नतीजा कई तरीकों से दिखाया जा सकता है: लेबल, लेबल और नियम वाली क्लास, एक्सएमएल, प्रोटोबस वगैरह. इन्हें OutputFormatter
की सब-क्लास के तौर पर लागू किया जाता है.
कुछ क्वेरी आउटपुट फ़ॉर्मैट की एक खास ज़रूरत यह है कि बेज़ल को पैकेज लोडिंग से मिलने वाली _all _जानकारी देनी होगी, ताकि लोग आउटपुट में अंतर कर सकें और यह पता लगा सकें कि किसी टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को क्रम से लगाया जाना चाहिए. इसलिए, ऐसे कुछ ही एट्रिब्यूट टाइप हैं जिनमें स्टारलार्क की मुश्किल वैल्यू वाले एट्रिब्यूट नहीं हैं. आम तौर पर, किसी लेबल का इस्तेमाल करके, उस लेबल के साथ नियम में जटिल जानकारी अटैच की जाती है. यह समाधान बहुत संतोषजनक नहीं है और इस आवश्यकता को पूरा करना बहुत अच्छा होगा.
मॉड्यूल सिस्टम
Bazel में मॉड्यूल जोड़कर, इसे बेहतर बनाया जा सकता है. हर मॉड्यूल को BlazeModule
का सबक्लास होना चाहिए (यह नाम, Bazel के इतिहास का एक हिस्सा है, जब इसे Blaze कहा जाता था). साथ ही, किसी कमांड को लागू करने के दौरान, उसे अलग-अलग इवेंट के बारे में जानकारी मिलती है.
इनका इस्तेमाल ज़्यादातर, "नॉन-कोर" फ़ंक्शन के अलग-अलग हिस्सों को लागू करने के लिए किया जाता है. इन फ़ंक्शन की ज़रूरत, Bazel के कुछ वर्शन (जैसे, Google में इस्तेमाल होने वाले वर्शन) को होती है:
- रिमोट एक्ज़ीक्यूशन सिस्टम के इंटरफ़ेस
- नए निर्देश
एक्सटेंशन पॉइंट BlazeModule
के ऑफ़र का सेट कुछ हद तक गड़बड़ी वाला है. इसका इस्तेमाल, डिज़ाइन के अच्छे सिद्धांतों के उदाहरण के तौर पर न करें.
इवेंट बस
ब्लेज़ मॉड्यूल का बाकी बेज़ल से संपर्क करने का मुख्य तरीका एक इवेंट बस (EventBus
) है: हर बिल्ड के लिए एक नया इंस्टेंस बनाया जाता है, Baज़ल के अलग-अलग पार्ट इसमें इवेंट पोस्ट कर सकते हैं, और मॉड्यूल लिसनर को अपनी पसंद के इवेंट के लिए रजिस्टर कर सकते हैं. उदाहरण के लिए, नीचे दी गई चीज़ें इवेंट के तौर पर दिखाई जाती हैं:
- बनाए जाने वाले बिल्ड टारगेट की सूची तय कर दी गई है
(
TargetParsingCompleteEvent
) - टॉप-लेवल कॉन्फ़िगरेशन तय कर दिए गए हैं
(
BuildConfigurationEvent
) - टारगेट बनाया गया या नहीं (
TargetCompleteEvent
) - कोई टेस्ट चलाया गया (
TestAttempt
,TestSummary
)
इनमें से कुछ इवेंट, बिल्ड इवेंट प्रोटोकॉल में, Bazel के बाहर दिखाए जाते हैं. ये BuildEvent
होते हैं. इससे BlazeModule
के साथ-साथ, बेज़ल प्रोसेस से बाहर की चीज़ों को भी बिल्ड के बारे में पता चल पाता है. इन्हें प्रोटोकॉल मैसेज वाली फ़ाइल के तौर पर ऐक्सेस किया जा सकता है. इसके अलावा, इवेंट स्ट्रीम करने के लिए, Bazel किसी सर्वर (जिसे बिल्ड इवेंट सेवा कहा जाता है) से कनेक्ट हो सकता है.
इसे build.lib.buildeventservice
और
build.lib.buildeventstream
Java पैकेज में लागू किया गया है.
बाहरी डेटा स्टोर करने की जगहें
Bazel को मूल रूप से, मोनोरेपो (एक सोर्स ट्री जिसमें प्रोग्राम बनाने के लिए ज़रूरी सभी चीज़ें होती हैं) में इस्तेमाल करने के लिए डिज़ाइन किया गया था. हालांकि, Bazel को अब ऐसी दुनिया में इस्तेमाल किया जा रहा है जहां यह ज़रूरी नहीं है कि वह मोनोरेपो में ही इस्तेमाल किया जाए. "एक्सटर्नल डेटा स्टोर करने की जगहें" एक संक्षेप में हैं जिसका इस्तेमाल इन दोनों दुनिया को जोड़ने के लिए किया जाता है: वे ऐसे कोड दिखाते हैं जो बिल्ड के लिए ज़रूरी है, लेकिन मुख्य सोर्स ट्री में नहीं होता.
WORKSPACE फ़ाइल
डेटा स्टोर करने की बाहरी जगहों का सेट, Workspace फ़ाइल को पार्स करके तय किया जाता है. उदाहरण के लिए, इस तरह का एलान:
local_repository(name="foo", path="/foo/bar")
@foo
नाम की रिपॉज़िटरी में नतीजे उपलब्ध हैं. हालांकि, ज़्यादा मुश्किल तब होती है, जब Starlark फ़ाइलों में डेटा स्टोर करने के नए नियम तय किए जा सकते हैं. इन नियमों का इस्तेमाल, Starlark के नए कोड को लोड करने के लिए किया जा सकता है. इसका इस्तेमाल डेटा स्टोर करने के नए नियमों को तय करने वगैरह के लिए किया जा सकता है...
इस मामले को हैंडल करने के लिए, WorkspaceFileFunction
में मौजूद WORKSPACE फ़ाइल को पार्स करने की प्रोसेस को load()
स्टेटमेंट के हिसाब से अलग-अलग हिस्सों में बांटा जाता है. चंक इंडेक्स को WorkspaceFileKey.getIndex()
से दिखाया जाता है और इंडेक्स X तक WorkspaceFileFunction
का हिसाब लगाने का मतलब है कि Xवें load()
स्टेटमेंट तक इसका आकलन किया जाता है.
डेटा स्टोर करने की जगहों के डेटा को फ़ेच किया जा रहा है
रिपॉज़िटरी का कोड, Bazel के लिए उपलब्ध होने से पहले, उसे फ़ेच करना ज़रूरी है. इसके नतीजे के तौर पर, Basel की डायरेक्ट्री $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
कमांड लाइन विकल्प, एक और कैश मेमोरी तय करता है. इसका इस्तेमाल, डाउनलोड किए जाने वाले आर्टफ़ैक्ट को खोजने के लिए किया जाता है. यह ऐसी एंटरप्राइज़ सेटिंग में काम आता है जहां Ba बैंक को इंटरनेट से रैंडम चीज़ों को फ़ेच नहीं करना चाहिए. इसेDownloadManager
लागू करता है.
किसी रिपॉज़िटरी को डाउनलोड करने के बाद, उसमें मौजूद आर्टफ़ैक्ट को सोर्स आर्टफ़ैक्ट माना जाता है. इससे समस्या होती है, क्योंकि आम तौर पर Bazel, सोर्स आर्टफ़ैक्ट के अप-टू-डेट होने की जांच करने के लिए, उन पर stat() को कॉल करता है. साथ ही, इन आर्टफ़ैक्ट को अमान्य भी कर दिया जाता है, जब वे जिस रिपॉज़िटरी में मौजूद होते हैं उसकी परिभाषा में बदलाव होता है. इसलिए,
किसी बाहरी डेटा स्टोर करने की जगह में मौजूद आर्टफ़ैक्ट के FileStateValue
को उसके बाहरी डेटा स्टोर करने की जगह पर निर्भर होना चाहिए. इसे ExternalFilesHelper
मैनेज करता है.
डेटा स्टोर करने की जगह की मैपिंग
ऐसा हो सकता है कि कई रिपॉज़िटरी, एक ही रिपॉज़िटरी पर निर्भर करना चाहें, लेकिन अलग-अलग वर्शन में. यह "डायमंड डिपेंडेंसी समस्या" का एक उदाहरण है. उदाहरण के लिए, अगर बिल्ड में अलग-अलग रिपॉज़िटरी में मौजूद दो बाइनरी को Guava पर निर्भर करना है, तो हो सकता है कि दोनों Guava को @guava//
से शुरू होने वाले लेबल के साथ रेफ़र करें. साथ ही, यह उम्मीद करें कि इसका मतलब इसके अलग-अलग वर्शन से है.
इसलिए, Bazel की मदद से बाहरी रिपॉज़िटरी के लेबल को फिर से मैप किया जा सकता है, ताकि @guava//
स्ट्रिंग, एक बाइनरी की रिपॉज़िटरी में मौजूद किसी Guava रिपॉज़िटरी (जैसे, @guava1//
) और दूसरी बाइनरी की रिपॉज़िटरी में मौजूद किसी अन्य Guava रिपॉज़िटरी (जैसे, @guava2//
) को रेफ़र कर सके.
इसके अलावा, इसका इस्तेमाल डायमंड को जॉइन करने के लिए भी किया जा सकता है. अगर कोई रिपॉज़िटरी @guava1//
पर निर्भर करता है और कोई दूसरा @guava2//
पर, तो रिपॉज़िटरी मैपिंग की मदद से, दोनों रिपॉज़िटरी को फिर से मैप किया जा सकता है, ताकि कैननिकल @guava//
रिपॉज़िटरी का इस्तेमाल किया जा सके.
मैपिंग को WORKSPACE फ़ाइल में, अलग-अलग रिपॉज़िटरी की परिभाषाओं के repo_mapping
एट्रिब्यूट के तौर पर बताया गया है. इसके बाद, यह Skyframe में WorkspaceFileValue
के सदस्य के तौर पर दिखता है. यहां इसे इनसे कनेक्ट किया जाता है:
Package.Builder.repositoryMapping
का इस्तेमाल, पैकेज में मौजूद नियमों के लेबल-वैल्यू वाले एट्रिब्यूट को बदलने के लिए किया जाता है. इसके लिए,RuleClass.populateRuleAttributeValues()
Package.repositoryMapping
जिसका इस्तेमाल विश्लेषण के चरण में किया जाता है ($(location)
जैसी उन चीज़ों को ठीक करने के लिए जिन्हें लोड करने के चरण में पार्स नहीं किया गया है)BzlLoadFunction
, load() स्टेटमेंट में लेबल को हल करने के लिए
JNI बिट
Bazel का सर्वर, ज़्यादातर Java में लिखा गया है. हालांकि, उन हिस्सों में अपवाद है जिन्हें Java खुद नहीं कर सकता या जब हमने उसे लागू किया था, तो वह खुद ऐसा नहीं कर सकता. यह मुख्य रूप से फ़ाइल सिस्टम, प्रोसेस कंट्रोल, और कई अन्य लो-लेवल चीज़ों के साथ इंटरैक्शन तक सीमित है.
C++ कोड, src/main/native में होता है और नेटिव तरीकों वाली Java क्लास इस तरह से हैं:
NativePosixFiles
औरNativePosixFileSystem
ProcessUtils
WindowsFileOperations
औरWindowsFileProcesses
com.google.devtools.build.lib.platform
कंसोल आउटपुट
कॉन्सल आउटपुट को दिखाना आसान लगता है. हालांकि, कई प्रोसेस (कभी-कभी रिमोट से) को चलाना, बेहतर कैश मेमोरी, बेहतर और रंगीन टर्मिनल आउटपुट, और लंबे समय तक चलने वाले सर्वर को मैनेज करना आसान नहीं है.
क्लाइंट से RPC कॉल आने के तुरंत बाद, stdout और stderr के लिए दो RpcOutputStream
इंस्टेंस बनाए जाते हैं, जो क्लाइंट में प्रिंट किए गए डेटा को फ़ॉरवर्ड करते हैं. इसके बाद, इन्हें OutErr
(stdout, stderr) पेयर में रैप किया जाता है. कंसोल पर प्रिंट करने के लिए, सभी चीज़ों को इन स्ट्रीम से भेजा जाता है. इसके बाद, इन स्ट्रीम को BlazeCommandDispatcher.execExclusively()
को सौंप दिया जाता है.
आउटपुट, डिफ़ॉल्ट रूप से एएनएसआई एस्केप सीक्वेंस के साथ प्रिंट होता है. जब ये ज़रूरी न हों (--color=no
), तो उन्हें AnsiStrippingOutputStream
से हटा दिया जाता है. इसके अलावा, System.out
और System.err
को इन आउटपुट स्ट्रीम पर रीडायरेक्ट किया जाता है.
ऐसा इसलिए किया जाता है, ताकि डीबग करने से जुड़ी जानकारी को System.err.println()
का इस्तेमाल करके प्रिंट किया जा सके और वह क्लाइंट के टर्मिनल आउटपुट में दिखे. यह आउटपुट, सर्वर के आउटपुट से अलग होता है. ध्यान रखा जाता है कि अगर कोई प्रोसेस bazel query --output=proto
जैसे बाइनरी आउटपुट देती है, तो स्टैंडर्ड आउटपुट में कोई बदलाव न किया जाए.
EventHandler
इंटरफ़ेस की मदद से, छोटे मैसेज (गड़बड़ियां, चेतावनियां वगैरह) दिखाए जाते हैं. ध्यान दें कि ये EventBus
में पोस्ट किए जाने वाले डेटा से अलग होते हैं. हर Event
में एक EventKind
(गड़बड़ी, चेतावनी, जानकारी वगैरह) होती है. साथ ही, उनमें एक Location
(सोर्स कोड में वह जगह जिसकी वजह से यह इवेंट हुआ) हो सकती है.
कुछ EventHandler
लागू करने की सुविधा, उन्हें मिलने वाले इवेंट को सेव करती है. इसका इस्तेमाल, कैश मेमोरी में प्रोसेस करने के अलग-अलग तरीकों की वजह से, यूज़र इंटरफ़ेस (यूआई) पर जानकारी को फिर से चलाने के लिए किया जाता है. उदाहरण के लिए, कैश मेमोरी में कॉन्फ़िगर किए गए टारगेट से मिलने वाली चेतावनियां.
कुछ EventHandler
, इवेंट पोस्ट करने की अनुमति भी देते हैं. ये इवेंट, आखिर में इवेंट बस में दिखते हैं. सामान्य Event
वहां _नहीं_ दिखते. ये ExtendedEventHandler
को लागू करने के तरीके हैं. इनका मुख्य इस्तेमाल, कैश मेमोरी में सेव किए गए EventBus
इवेंट को फिर से चलाने के लिए किया जाता है. ये सभी EventBus
इवेंट, Postable
को लागू करते हैं. हालांकि, EventBus
पर पोस्ट की गई हर चीज़ को ज़रूरी रूप से यह इंटरफ़ेस लागू नहीं करता. सिर्फ़ ExtendedEventHandler
से कैश मेमोरी में सेव की गई चीज़ें ही लागू होती हैं. हालांकि, ऐसा करना अच्छा होता है और ज़्यादातर चीज़ें ऐसा करती हैं, लेकिन इसे लागू करना ज़रूरी नहीं है
टर्मिनल आउटपुट ज़्यादातर UiEventHandler
के ज़रिए दिखाया जाता है. यह आउटपुट को फ़ॉर्मैट करने और प्रोग्रेस रिपोर्ट करने के लिए ज़रूरी है. इसमें दो इनपुट होते हैं:
- इवेंट बस
- Reporter की मदद से इसमें भेजी गई इवेंट स्ट्रीम
क्लाइंट की आरपीसी स्ट्रीम से, कमांड को लागू करने वाली मशीन (उदाहरण के लिए, बाज़ल का बाकी हिस्सा) का सीधा कनेक्शन सिर्फ़ Reporter.getOutErr()
के ज़रिए होता है. इससे इन स्ट्रीम को सीधे ऐक्सेस किया जा सकता है. इसका इस्तेमाल सिर्फ़ तब किया जाता है, जब किसी कमांड को bazel query
जैसे बहुत ज़्यादा बाइनरी डेटा को डंप करना हो.
Bazel की प्रोफ़ाइल बनाना
Bazel तेज़ काम करता है. Basel का आकार धीमा होता है, क्योंकि बिल्ड के बढ़ने की प्रक्रिया में तब तक कोई रुकावट नहीं होती,
जब तक कि वह पैदा न हो जाए. इस वजह से, Bazel में एक प्रोफ़ाइलर शामिल होता है. इसका इस्तेमाल, बिल्ड और Bazel की प्रोफ़ाइल बनाने के लिए किया जा सकता है. इसे Profiler
नाम की क्लास में लागू किया गया है. यह सुविधा डिफ़ॉल्ट रूप से चालू रहती है. हालांकि, यह सिर्फ़ कम डेटा रिकॉर्ड करती है, ताकि इसका ओवरहेड कम से कम हो. कमांड लाइन --record_full_profiler_data
की मदद से, यह ज़्यादा से ज़्यादा डेटा रिकॉर्ड कर सकती है.
यह Chrome प्रोफ़ाइलर फ़ॉर्मैट में प्रोफ़ाइल जनरेट करता है. इसे Chrome में सबसे बेहतर तरीके से देखा जा सकता है. इसका डेटा मॉडल, टास्क स्टैक का होता है: इसमें टास्क शुरू और खत्म किए जा सकते हैं. साथ ही, ये एक-दूसरे के अंदर व्यवस्थित तरीके से नेस्ट होने चाहिए. हर Java थ्रेड को अपना टास्क स्टैक मिलता है. TODO: यह कार्रवाइयों और कंटिन्यूएशन-पासिंग स्टाइल के साथ कैसे काम करता है?
प्रोफ़ाइलर को BlazeRuntime.initProfiler()
में शुरू और BlazeRuntime.afterCommand()
में बंद किया जाता है. साथ ही, इसे ज़्यादा से ज़्यादा समय तक लाइव रखने की कोशिश की जाती है, ताकि हम हर चीज़ की प्रोफ़ाइल बना सकें. प्रोफ़ाइल में कुछ जोड़ने के लिए,
Profiler.instance().profile()
को कॉल करें. यह Closeable
दिखाता है. इसके क्लोज़र का मतलब है कि टास्क पूरा हो गया है. इसका इस्तेमाल, try-with-resources के स्टेटमेंट के साथ करना सबसे अच्छा होता है.
हम MemoryProfiler
में, मेमोरी की बुनियादी प्रोफ़ाइलिंग भी करते हैं. यह भी हमेशा चालू रहता है और ज़्यादातर हेप साइज़ और जीसी के व्यवहार को रिकॉर्ड करता है.
Bazel की जांच करना
Bazel में दो तरह के मुख्य टेस्ट होते हैं: एक ऐसा टेस्ट जो Bazel को "ब्लैक बॉक्स" के तौर पर देखता है और दूसरा ऐसा टेस्ट जो सिर्फ़ विश्लेषण का फ़ेज़ चलाता है. हम पहले को "इंटिग्रेशन टेस्ट" और दूसरे को "यूनिट टेस्ट" कहते हैं. हालांकि, ये इंटिग्रेशन टेस्ट की तरह ही होते हैं, लेकिन इनमें इंटिग्रेशन कम होता है. हमारे पास कुछ यूनिट टेस्ट भी हैं, जो ज़रूरी होने पर किए जाते हैं.
इंटिग्रेशन टेस्ट दो तरह के होते हैं:
- एक ऐसा प्रोग्राम जिसे
src/test/shell
के तहत, बहुत अच्छे बैश टेस्ट फ़्रेमवर्क का इस्तेमाल करके लागू किया गया हो - Java में लागू किए गए. इन्हें
BuildIntegrationTestCase
के सबक्लास के तौर पर लागू किया जाता है
BuildIntegrationTestCase
, इंटिग्रेशन टेस्टिंग के लिए सबसे ज़्यादा इस्तेमाल किया जाने वाला फ़्रेमवर्क है, क्योंकि यह ज़्यादातर टेस्टिंग स्थितियों के लिए बेहतर तरीके से तैयार है. यह एक Java फ़्रेमवर्क है. इसलिए, इसमें डीबग करने की सुविधा मिलती है. साथ ही, इसे कई सामान्य डेवलपमेंट टूल के साथ आसानी से इंटिग्रेट किया जा सकता है. Bazel रिपॉज़िटरी में BuildIntegrationTestCase
क्लास के कई उदाहरण हैं.
विश्लेषण टेस्ट, BuildViewTestCase
के सबक्लास के तौर पर लागू किए जाते हैं. इसमें एक स्क्रैच फ़ाइल सिस्टम होता है, जिसका इस्तेमाल BUILD
फ़ाइलें लिखने के लिए किया जा सकता है. इसके बाद, अलग-अलग सहायक तरीके, कॉन्फ़िगर किए गए टारगेट का अनुरोध कर सकते हैं, कॉन्फ़िगरेशन में बदलाव कर सकते हैं, और विश्लेषण के नतीजे के बारे में अलग-अलग बातें बता सकते हैं.