इस दस्तावेज़ में, कोडबेस और Bazel के स्ट्रक्चर के बारे में बताया गया है. यह सुविधा उन लोगों के लिए है जो Bazel में योगदान देना चाहते हैं, न कि उन लोगों के लिए जो इसका इस्तेमाल करते हैं.
परिचय
Bazel का कोडबेस बहुत बड़ा है. इसमें ~350KLOC प्रोडक्शन कोड और ~260 KLOC टेस्ट कोड है. किसी को भी पूरे कोडबेस के बारे में जानकारी नहीं है. हर व्यक्ति को अपने हिस्से के कोड के बारे में अच्छी तरह से पता है, लेकिन कुछ ही लोगों को यह पता है कि हर दिशा में पहाड़ियों के ऊपर क्या है.
इस दस्तावेज़ में कोडबेस की खास जानकारी दी गई है, ताकि जो लोग इस प्रोसेस के बीच में हैं उन्हें सीधे रास्ते के बारे में पता चल सके. इससे उन्हें इस पर काम शुरू करने में आसानी होगी.
Bazel के सोर्स कोड का सार्वजनिक वर्शन, GitHub पर github.com/bazelbuild/bazel पर उपलब्ध है. यह "भरोसेमंद सोर्स" नहीं है. यह Google के इंटरनल सोर्स ट्री से लिया गया है. इसमें ऐसी अतिरिक्त सुविधाएं शामिल हैं जो Google के बाहर काम की नहीं हैं. हमारा मुख्य लक्ष्य, GitHub को भरोसेमंद सोर्स बनाना है.
GitHub के पुल अनुरोध की सामान्य प्रोसेस के ज़रिए योगदान स्वीकार किए जाते हैं. इसके बाद, Google कर्मचारी उन्हें मैन्युअल तरीके से इंटरनल सोर्स ट्री में इंपोर्ट करता है. इसके बाद, उन्हें वापस GitHub पर एक्सपोर्ट किया जाता है.
क्लाइंट/सर्वर आर्किटेक्चर
Bazel का ज़्यादातर हिस्सा, सर्वर प्रोसेस में होता है. यह प्रोसेस, बिल्ड के बीच RAM में रहती है. इससे Bazel को बिल्ड के बीच स्थिति बनाए रखने की अनुमति मिलती है.
इसलिए, Bazel कमांड लाइन में दो तरह के विकल्प होते हैं: स्टार्टअप और कमांड. इस तरह की कमांड लाइन में:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
कुछ विकल्प (--host_jvm_args=
) कमांड के नाम से पहले होते हैं
और कुछ बाद में (-c opt
). पहले वाले विकल्प को "स्टार्टअप विकल्प" कहा जाता है. यह
सर्वर प्रोसेस को पूरी तरह से प्रभावित करता है. वहीं, बाद वाले विकल्प को "कमांड
विकल्प" कहा जाता है. यह सिर्फ़ एक कमांड को प्रभावित करता है.
हर सर्वर इंस्टेंस से एक Workspace जुड़ा होता है. Workspace, सोर्स ट्री का कलेक्शन होता है. इसे "रिपॉज़िटरी" कहा जाता है. आम तौर पर, हर Workspace में एक चालू सर्वर इंस्टेंस होता है. कस्टम आउटपुट बेस तय करके, इस समस्या से बचा जा सकता है. ज़्यादा जानकारी के लिए, "डायरेक्ट्री लेआउट" सेक्शन देखें.
Bazel को एक ही ELF एक्ज़ीक्यूटेबल के तौर पर डिस्ट्रिब्यूट किया जाता है. यह एक मान्य .zip फ़ाइल भी है.
bazel
टाइप करने पर, C++ में लागू किया गया ऊपर दिया गया ELF एक्ज़ीक्यूटेबल (क्लाइंट) कंट्रोल में आ जाता है. यह कुकी, यहां दिया गया तरीका अपनाकर सही सर्वर प्रोसेस सेट अप करती है:
- यह कुकी देखती है कि क्या यह पहले ही खुद को एक्सट्रैक्ट कर चुकी है. अगर ऐसा नहीं है, तो ऐसा होता है. सर्वर का इस्तेमाल यहां से शुरू होता है.
- यह कुकी यह जांच करती है कि कोई ऐसा सर्वर इंस्टेंस चालू है या नहीं जो काम करता हो: वह चल रहा हो,
उसमें स्टार्टअप के सही विकल्प हों, और वह सही वर्कस्पेस डायरेक्ट्री का इस्तेमाल करता हो. यह
$OUTPUT_BASE/server
डायरेक्ट्री में मौजूद लॉक फ़ाइल के ज़रिए, चालू सर्वर का पता लगाता है. इस फ़ाइल में वह पोर्ट होता है जिस पर सर्वर काम कर रहा होता है. - ज़रूरत पड़ने पर, पुराने सर्वर प्रोसेस को बंद कर देता है
- ज़रूरत पड़ने पर, नई सर्वर प्रोसेस शुरू करता है
जब सर्वर की प्रोसेस पूरी हो जाती है, तब gRPC इंटरफ़ेस के ज़रिए उस कमांड के बारे में बताया जाता है जिसे चलाना है. इसके बाद, Bazel का आउटपुट वापस टर्मिनल पर भेज दिया जाता है. एक समय में सिर्फ़ एक निर्देश दिया जा सकता है. इसे C++ और Java में मौजूद हिस्सों के साथ, जटिल लॉकिंग मेकेनिज़्म का इस्तेमाल करके लागू किया जाता है. एक साथ कई कमांड चलाने के लिए कुछ इन्फ़्रास्ट्रक्चर है, क्योंकि किसी अन्य कमांड के साथ bazel version
को एक साथ नहीं चलाया जा सकता. BlazeModule
का लाइफ़साइकल और BlazeRuntime
में मौजूद कुछ स्थितियां, मुख्य रुकावटें हैं.
कमांड के आखिर में, Bazel सर्वर, क्लाइंट को वह एक्ज़िट कोड भेजता है जिसे क्लाइंट को वापस भेजना चाहिए. bazel run
को लागू करने में एक दिलचस्प समस्या यह है कि इस कमांड का काम, Bazel की ओर से अभी-अभी बनाए गए किसी प्रोग्राम को चलाना है. हालांकि, यह सर्वर प्रोसेस से ऐसा नहीं कर सकता, क्योंकि इसके पास टर्मिनल नहीं है. इसलिए, यह क्लाइंट को बताता है कि उसे कौनसी बाइनरी exec()
करनी चाहिए और किन तर्कों के साथ.
जब कोई व्यक्ति Ctrl-C दबाता है, तो क्लाइंट इसे gRPC कनेक्शन पर Cancel कॉल में बदल देता है. इससे कमांड को जल्द से जल्द बंद करने की कोशिश की जाती है. तीसरे Ctrl-C के बाद, क्लाइंट सर्वर को SIGKILL भेजता है.
क्लाइंट का सोर्स कोड src/main/cpp
में है. साथ ही, सर्वर से कम्यूनिकेट करने के लिए इस्तेमाल किया गया प्रोटोकॉल src/main/protobuf/command_server.proto
में है.
सर्वर का मुख्य एंट्री पॉइंट BlazeRuntime.main()
है. साथ ही, क्लाइंट से मिले gRPC कॉल को GrpcServerImpl.run()
हैंडल करता है.
डायरेक्ट्री का लेआउट
Bazel, बिल्ड के दौरान डायरेक्ट्री का एक मुश्किल सेट बनाता है. पूरी जानकारी आउटपुट डायरेक्ट्री लेआउट में दी गई है.
"main repo" वह सोर्स ट्री होता है जिसमें Bazel को चलाया जाता है. आम तौर पर, यह उस चीज़ से मेल खाता है जिसे आपने सोर्स कंट्रोल से चेक आउट किया है. इस डायरेक्ट्री के रूट को "वर्कस्पेस रूट" कहा जाता है.
Bazel अपना सारा डेटा "output user root" में रखता है. आम तौर पर, यह $HOME/.cache/bazel/_bazel_${USER}
होता है. हालांकि, --output_user_root
स्टार्टअप विकल्प का इस्तेमाल करके इसे बदला जा सकता है.
"इंस्टॉल बेस" वह जगह होती है जहां Bazel को एक्सट्रैक्ट किया जाता है. यह प्रोसेस अपने-आप होती है. साथ ही, हर Bazel वर्शन को इंस्टॉल बेस में उसके चेकसम के आधार पर एक सबडायरेक्ट्री मिलती है. यह डिफ़ॉल्ट रूप से $OUTPUT_USER_ROOT/install
पर सेट होता है. इसे --install_base
कमांड लाइन विकल्प का इस्तेमाल करके बदला जा सकता है.
"आउटपुट बेस" वह जगह होती है जहां किसी खास वर्कस्पेस से जुड़ा Bazel इंस्टेंस लिखता है. हर आउटपुट बेस में, किसी भी समय ज़्यादा से ज़्यादा एक Bazel सर्वर इंस्टेंस चल रहा होता है. यह आम तौर पर $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
बजे होता है. इसे --output_base
स्टार्टअप विकल्प का इस्तेमाल करके बदला जा सकता है. यह विकल्प, अन्य चीज़ों के साथ-साथ इस सीमा को पार करने के लिए भी काम आता है कि किसी भी समय किसी भी वर्कस्पेस में सिर्फ़ एक Bazel इंस्टेंस चल सकता है.
आउटपुट डायरेक्ट्री में ये चीज़ें शामिल होती हैं:
$OUTPUT_BASE/external
पर फ़ेच की गई बाहरी रिपॉज़िटरी.- एक्ज़ेक रूट, एक डायरेक्ट्री होती है. इसमें मौजूदा बिल्ड के सभी सोर्स कोड के लिए सिंबल लिंक होते हैं. यह
$OUTPUT_BASE/execroot
में मौजूद है. बिल्ड के दौरान, वर्किंग डायरेक्ट्री$EXECROOT/<name of main repository>
होती है. हम इसे$EXECROOT
में बदलने का प्लान बना रहे हैं. हालांकि, यह एक लंबी अवधि का प्लान है, क्योंकि यह एक ऐसा बदलाव है जो काम नहीं करता. - बिल्ड के दौरान बनाई गई फ़ाइलें.
किसी निर्देश को पूरा करने की प्रोसेस
Bazel सर्वर को कंट्रोल मिलने और उसे किसी कमांड के बारे में सूचना मिलने के बाद, ये इवेंट इस क्रम में होते हैं:
BlazeCommandDispatcher
को नए अनुरोध के बारे में सूचना दी जाती है. यह तय करता है कि कमांड को चलाने के लिए किसी वर्कस्पेस की ज़रूरत है या नहीं. सोर्स कोड से जुड़ी कमांड को छोड़कर, लगभग हर कमांड के लिए वर्कस्पेस की ज़रूरत होती है. जैसे, वर्शन या मदद. यह भी तय करता है कि कोई दूसरी कमांड चल रही है या नहीं.सही कमांड मिल गई है. हर कमांड को इंटरफ़ेस
BlazeCommand
लागू करना होगा. साथ ही, उसमें@Command
एनोटेशन होना चाहिए. यह थोड़ा एंटीपैटर्न है. अगर किसी कमांड के लिए ज़रूरी सभी मेटाडेटा कोBlazeCommand
के तरीकों से बताया जाए, तो यह बेहतर होगाकमांड लाइन के विकल्पों को पार्स किया जाता है. हर कमांड के लिए, कमांड लाइन के अलग-अलग विकल्प होते हैं. इनके बारे में
@Command
एनोटेशन में बताया गया है.इवेंट बस बनाई जाती है. इवेंट बस, बिल्ड के दौरान होने वाले इवेंट की स्ट्रीम होती है. इनमें से कुछ को Bazel से बाहर एक्सपोर्ट किया जाता है. ऐसा Build Event Protocol के तहत किया जाता है, ताकि दुनिया को यह बताया जा सके कि बिल्ड कैसे काम करता है.
निर्देश को कंट्रोल मिल जाता है. सबसे दिलचस्प कमांड वे होती हैं जो बिल्ड करती हैं: बिल्ड, टेस्ट, रन, कवरेज वगैरह. इस सुविधा को
BuildTool
लागू करता है.कमांड लाइन पर टारगेट पैटर्न के सेट को पार्स किया जाता है. साथ ही,
//pkg:all
और//pkg/...
जैसे वाइल्डकार्ड को हल किया जाता है. इसेAnalysisPhaseRunner.evaluateTargetPatterns()
में लागू किया जाता है और Skyframe मेंTargetPatternPhaseValue
के तौर पर फिर से बनाया जाता है.लोडिंग/विश्लेषण का चरण, ऐक्शन ग्राफ़ बनाने के लिए चलाया जाता है. यह एक डायरेक्टेड ऐसाइक्लिक ग्राफ़ होता है. इसमें उन कमांड के बारे में जानकारी होती है जिन्हें बिल्ड के लिए एक्ज़ीक्यूट करना होता है.
प्रोग्राम चलाने का फ़ेज़ पूरा हो गया है. इसका मतलब है कि अनुरोध किए गए टॉप-लेवल के टारगेट बनाने के लिए, ज़रूरी हर कार्रवाई को पूरा किया जाता है.
कमांड लाइन के विकल्प
Bazel इनवोकेशन के लिए कमांड-लाइन के विकल्पों के बारे में OptionsParsingResult
ऑब्जेक्ट में बताया गया है. इसमें "option classes" से लेकर विकल्पों की वैल्यू तक का मैप शामिल होता है. "विकल्प क्लास", OptionsBase
की सबक्लास होती है. यह एक-दूसरे से जुड़े कमांड लाइन विकल्पों को एक साथ ग्रुप करती है. उदाहरण के लिए:
- प्रोग्रामिंग भाषा से जुड़े विकल्प (
CppOptions
याJavaOptions
). येFragmentOptions
के सबक्लास होने चाहिए और आखिर मेंBuildOptions
ऑब्जेक्ट में रैप किए जाते हैं. - Bazel के कार्रवाइयां (
ExecutionOptions
) करने के तरीके से जुड़े विकल्प
इन विकल्पों को विश्लेषण के चरण में इस्तेमाल करने के लिए डिज़ाइन किया गया है. इनका इस्तेमाल RuleContext.getFragment()
(Java में) या ctx.fragments
(Starlark में) किया जा सकता है.
इनमें से कुछ (उदाहरण के लिए, C++ में शामिल किए गए फ़ाइलों को स्कैन करना है या नहीं) को एक्ज़ीक्यूशन फ़ेज़ में पढ़ा जाता है. हालांकि, इसके लिए हमेशा प्लंबिंग की ज़रूरत होती है, क्योंकि BuildConfiguration
तब उपलब्ध नहीं होता है. ज़्यादा जानकारी के लिए, "कॉन्फ़िगरेशन" सेक्शन देखें.
चेतावनी: हम यह मानकर चलते हैं कि OptionsBase
इंस्टेंस में बदलाव नहीं किया जा सकता और हम उनका इस्तेमाल इसी तरह करते हैं (जैसे, SkyKeys
के हिस्से के तौर पर). हालांकि, ऐसा नहीं है. इनमें बदलाव करने से, Bazel में ऐसी समस्याएं आ सकती हैं जिन्हें ठीक करना मुश्किल होता है. हालांकि, इन्हें पूरी तरह से बदला नहीं जा सकता.
(FragmentOptions
के बनने के तुरंत बाद, उसमें बदलाव किया जा सकता है. ऐसा तब तक किया जा सकता है, जब तक कोई और व्यक्ति उसका रेफ़रंस नहीं लेता और equals()
या hashCode()
को कॉल नहीं किया जाता.)
Bazel को विकल्प क्लास के बारे में इन तरीकों से पता चलता है:
- कुछ को Bazel में पहले से ही शामिल किया गया है (
CommonCommandOptions
) - हर Bazel कमांड पर मौजूद
@Command
एनोटेशन से ConfiguredRuleClassProvider
से (ये कमांड लाइन के विकल्प हैं, जो अलग-अलग प्रोग्रामिंग भाषाओं से जुड़े हैं)- Starlark के नियम, अपने विकल्प भी तय कर सकते हैं. इसके बारे में यहां देखें
हर विकल्प (Starlark से तय किए गए विकल्पों को छोड़कर), FragmentOptions
एनोटेशन वाली 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
फ़ाइलों को जोड़ने या हटाने से, अन्य पैकेज में बदलाव हो सकता है. ऐसा इसलिए होता है, क्योंकि रिकर्सिव ग्लोब पैकेज की सीमाओं पर रुक जाते हैं. इसलिए, BUILD
फ़ाइल के मौजूद होने से रिकर्सन रुक जाता है.
BUILD
फ़ाइल के आकलन को "पैकेज लोडिंग" कहा जाता है. इसे क्लास PackageFactory
में लागू किया जाता है. यह Starlark इंटरप्रेटर को कॉल करके काम करता है. इसके लिए, उपलब्ध नियम क्लास के सेट के बारे में जानकारी होना ज़रूरी है. पैकेज लोड करने का नतीजा, एक Package
ऑब्जेक्ट होता है. यह ज़्यादातर एक मैप होता है. इसमें स्ट्रिंग (टारगेट का नाम) से टारगेट तक की जानकारी होती है.
पैकेज लोड करने के दौरान, ज़्यादातर काम ग्लोबिंग का होता है: Bazel को हर सोर्स फ़ाइल को साफ़ तौर पर लिस्ट करने की ज़रूरत नहीं होती. इसके बजाय, वह ग्लोब (जैसे कि glob(["**/*.java"])
) चला सकता है. शेल के उलट, यह रिकर्सिव ग्लोब का इस्तेमाल करता है. ये सबडायरेक्ट्री में जाते हैं, लेकिन सबपैकेज में नहीं. इसके लिए, फ़ाइल सिस्टम का ऐक्सेस ज़रूरी होता है. साथ ही, इसमें समय लग सकता है. इसलिए, हम इसे एक साथ और ज़्यादा से ज़्यादा असरदार तरीके से चलाने के लिए, हर तरह की तरकीबें लागू करते हैं.
ग्लोबिंग को इन क्लास में लागू किया जाता है:
LegacyGlobber
, एक ऐसा फ़ंक्शन जो Skyframe के बारे में जानकारी नहीं रखता, लेकिन तेज़ी से काम करता हैSkyframeHybridGlobber
, यह Skyframe का इस्तेमाल करता है और "Skyframe रीस्टार्ट" (इसके बारे में नीचे बताया गया है) से बचने के लिए, पुराने ग्लोबर पर वापस आ जाता है
Package
क्लास में कुछ ऐसे सदस्य होते हैं जिनका इस्तेमाल सिर्फ़ "external" पैकेज (बाहरी डिपेंडेंसी से जुड़ा) को पार्स करने के लिए किया जाता है. साथ ही, ये सदस्य असली पैकेज के लिए काम के नहीं होते. यह डिज़ाइन से जुड़ी गड़बड़ी है, क्योंकि रेगुलर पैकेज के बारे में बताने वाले ऑब्जेक्ट में ऐसे फ़ील्ड नहीं होने चाहिए जो किसी और चीज़ के बारे में बताते हों. इनमें शामिल हैं:
- रिपॉज़िटरी मैपिंग
- रजिस्टर की गई टूलचेन
- रजिस्टर किए गए एक्ज़ीक्यूशन प्लैटफ़ॉर्म
आदर्श रूप से, "external" पैकेज को पार्स करने और सामान्य पैकेज को पार्स करने के बीच ज़्यादा अंतर होना चाहिए, ताकि 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 नियम क्लास को Bazel "पहले से" जानता है, क्योंकि वे ConfiguredRuleClassProvider
के साथ रजिस्टर होती हैं.
नियम की क्लास में यह जानकारी शामिल होती है:
- इसके एट्रिब्यूट (जैसे,
srcs
,deps
): इनके टाइप, डिफ़ॉल्ट वैल्यू, पाबंदियां वगैरह. - हर एट्रिब्यूट से जुड़े कॉन्फ़िगरेशन ट्रांज़िशन और पहलू, अगर कोई हो
- नियम लागू करना
- ट्रांज़िटिव जानकारी देने वाले, ऐसे नियम बनाते हैं जो "आम तौर पर" बनाए जाते हैं
शब्दावली से जुड़ी जानकारी: कोडबेस में, हम अक्सर "नियम" का इस्तेमाल, नियम क्लास से बनाए गए टारगेट के लिए करते हैं. हालांकि, Starlark और उपयोगकर्ता के लिए उपलब्ध दस्तावेज़ में, "नियम" का इस्तेमाल सिर्फ़ नियम क्लास के लिए किया जाना चाहिए. टारगेट सिर्फ़ एक "टारगेट" होता है. यह भी ध्यान दें कि RuleClass
के नाम में "class" होने के बावजूद, नियम क्लास और उस टाइप के टारगेट के बीच Java इनहेरिटेंस का कोई संबंध नहीं है.
Skyframe
Bazel के लिए इस्तेमाल होने वाले आकलन फ़्रेमवर्क को Skyframe कहा जाता है. इसका मॉडल यह है कि बिल्ड के दौरान बनाई जाने वाली हर चीज़ को डायरेक्टेड एसाइक्लिक ग्राफ़ में व्यवस्थित किया जाता है. इसमें किनारों को डेटा के किसी भी हिस्से से उसकी डिपेंडेंसी की ओर ले जाया जाता है. इसका मतलब है कि डेटा के अन्य हिस्सों को बनाने के लिए ज़रूरी जानकारी.
ग्राफ़ में मौजूद नोड को SkyValue
कहा जाता है और उनके नाम को SkyKey
कहा जाता है. दोनों में कोई बदलाव नहीं किया जा सकता. इनसे सिर्फ़ ऐसे ऑब्जेक्ट ऐक्सेस किए जा सकते हैं जिनमें कोई बदलाव नहीं किया जा सकता. यह इनवेरिएंट लगभग हमेशा लागू होता है. अगर यह लागू नहीं होता है, तो हम उन्हें बदलने की कोशिश नहीं करते हैं. जैसे, अलग-अलग विकल्पों वाली क्लास BuildOptions
, जो BuildConfigurationValue
और SkyKey
की सदस्य है. अगर हम उन्हें बदलते हैं, तो हम सिर्फ़ ऐसे तरीके अपनाते हैं जिन्हें बाहर से नहीं देखा जा सकता.
इससे यह पता चलता है कि Skyframe में कंप्यूट की गई हर चीज़ (जैसे कि कॉन्फ़िगर किए गए टारगेट) में भी बदलाव नहीं किया जा सकता.
Skyframe ग्राफ़ को देखने का सबसे आसान तरीका है कि आप bazel dump
--skyframe=deps
चलाएं. इससे ग्राफ़ डंप हो जाता है और हर लाइन में एक SkyValue
दिखता है. इसे छोटे बिल्ड के लिए इस्तेमाल करना सबसे अच्छा होता है, क्योंकि यह काफ़ी बड़ा हो सकता है.
Skyframe, com.google.devtools.build.skyframe
पैकेज में उपलब्ध है. इसी नाम वाले पैकेज com.google.devtools.build.lib.skyframe
में, Skyframe के ऊपर Bazel को लागू करने की सुविधा शामिल है. Skyframe के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
किसी SkyKey
को SkyValue
में बदलने के लिए, Skyframe उस कुंजी के टाइप से जुड़े SkyFunction
को चालू करेगा. फ़ंक्शन के आकलन के दौरान, यह Skyframe से अन्य डिपेंडेंसी का अनुरोध कर सकता है. इसके लिए, यह SkyFunction.Environment.getValue()
के अलग-अलग ओवरलोड को कॉल करता है. इससे उन डिपेंडेंसी को Skyframe के इंटरनल ग्राफ़ में रजिस्टर करने का साइड इफ़ेक्ट होता है. इसलिए, जब भी इसकी किसी डिपेंडेंसी में बदलाव होता है, तो Skyframe को फ़ंक्शन का फिर से आकलन करने के बारे में पता चल जाएगा. दूसरे शब्दों में कहें, तो Skyframe की कैश मेमोरी और इंक्रीमेंटल कंप्यूटेशन, SkyFunction
और SkyValue
के हिसाब से काम करते हैं.
जब भी कोई SkyFunction
ऐसी डिपेंडेंसी का अनुरोध करता है जो उपलब्ध नहीं है, तो getValue()
null वैल्यू दिखाता है. इसके बाद, फ़ंक्शन को Skyframe को कंट्रोल वापस दे देना चाहिए. इसके लिए, उसे खुद ही शून्य वैल्यू दिखानी होगी. कुछ समय बाद, Skyframe उस डिपेंडेंसी का आकलन करेगा जो उपलब्ध नहीं है. इसके बाद, फ़ंक्शन को शुरू से रीस्टार्ट करेगा. इस बार, getValue()
कॉल को गैर-शून्य नतीजे के साथ पूरा किया जाएगा.
इसका मतलब है कि रीस्टार्ट करने से पहले, SkyFunction
में किए गए किसी भी कंप्यूटेशन को दोहराना होगा. हालांकि, इसमें SkyValues
की डिपेंडेंसी का आकलन करने के लिए किया गया काम शामिल नहीं है, जिसे कैश मेमोरी में सेव किया जाता है. इसलिए, हम आम तौर पर इस समस्या को इन तरीकों से हल करते हैं:
- रीस्टार्ट की संख्या को सीमित करने के लिए, बैच में डिपेंडेंसी का एलान करना (
getValuesAndExceptions()
का इस्तेमाल करके). SkyValue
को अलग-अलग हिस्सों में बांटना. इन हिस्सों की गिनती अलग-अलगSkyFunction
करते हैं, ताकि इनकी गिनती अलग से की जा सके और इन्हें अलग से कैश मेमोरी में सेव किया जा सके. यह काम रणनीति के तहत किया जाना चाहिए, क्योंकि इससे मेमोरी का इस्तेमाल बढ़ सकता है.- रीस्टार्ट के बीच की स्थिति को सेव करना. इसके लिए,
SkyFunction.Environment.getState()
का इस्तेमाल किया जाता है. इसके अलावा, "Skyframe के बैकएंड में" स्टैटिक कैश को सेव किया जाता है. जटिल SkyFunctions के साथ, रीस्टार्ट के बीच स्टेट मैनेजमेंट मुश्किल हो सकता है. इसलिए, लॉजिकल कंकरेंसी के लिए स्ट्रक्चर्ड अप्रोच के तौर परStateMachine
s पेश किए गए थे. इनमेंSkyFunction
के अंदर हैरारिकल कंप्यूटेशन को निलंबित और फिर से शुरू करने के लिए हुक शामिल हैं. उदाहरण:DependencyResolver#computeDependencies
कॉन्फ़िगर किए गए टारगेट की डायरेक्ट डिपेंडेंसी के संभावित तौर पर बड़े सेट का हिसाब लगाने के लिए,getState()
के साथStateMachine
का इस्तेमाल करता है. ऐसा न करने पर, रीस्टार्ट करने में ज़्यादा समय लग सकता है.
बुनियादी तौर पर, Bazel को इस तरह के समाधानों की ज़रूरत होती है, क्योंकि हज़ारों-लाखों Skyframe नोड आम बात है. साथ ही, 2023 तक Java में हल्के थ्रेड का इस्तेमाल करने से, StateMachine
के मुकाबले बेहतर परफ़ॉर्मेंस नहीं मिलती.
Starlark
Starlark, खास तौर पर डोमेन के लिए बनी एक लैंग्वेज है. इसका इस्तेमाल लोग Bazel को कॉन्फ़िगर करने और उसे बढ़ाने के लिए करते हैं. इसे Python के एक सीमित सबसेट के तौर पर बनाया गया है. इसमें बहुत कम टाइप होते हैं, कंट्रोल फ़्लो पर ज़्यादा पाबंदियां होती हैं, और सबसे अहम बात यह है कि इसमें एक साथ कई बार पढ़ने की सुविधा को चालू करने के लिए, डेटा में बदलाव न करने की गारंटी होती है. यह ट्यूरिंग-कंप्लीट नहीं है. इसलिए, कुछ (लेकिन सभी नहीं) उपयोगकर्ता इस भाषा में सामान्य प्रोग्रामिंग टास्क पूरे करने की कोशिश नहीं करते हैं.
Starlark को net.starlark.java
पैकेज में लागू किया जाता है.
इसका इंडिपेंडेंट Go वर्शन भी है, जो यहां उपलब्ध है. Bazel में इस्तेमाल किया गया Java
इंप्लीमेंटेशन, फ़िलहाल एक इंटरप्रेटर है.
Starlark का इस्तेमाल कई तरह से किया जाता है. जैसे:
BUILD
फ़ाइलें. यहां नए बिल्ड टारगेट तय किए जाते हैं. इस कॉन्टेक्स्ट में चलने वाले Starlark कोड के पास, सिर्फ़BUILD
फ़ाइल के कॉन्टेंट और उससे लोड की गई.bzl
फ़ाइलों का ऐक्सेस होता है.MODULE.bazel
फ़ाइल. यहां बाहरी डिपेंडेंसी तय की जाती हैं. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास, पहले से तय किए गए कुछ निर्देशों का ऐक्सेस बहुत सीमित होता है..bzl
फ़ाइलें. यहां नए बिल्ड के नियम, रेपो के नियम, और मॉड्यूल एक्सटेंशन तय किए जाते हैं. यहां मौजूद Starlark कोड, नए फ़ंक्शन तय कर सकता है और अन्य.bzl
फ़ाइलों से लोड कर सकता है.
BUILD
और .bzl
फ़ाइलों के लिए उपलब्ध बोलियां थोड़ी अलग होती हैं, क्योंकि ये अलग-अलग चीज़ों को ज़ाहिर करती हैं. इनके बीच के अंतर की सूची यहां दी गई है.
Starlark के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
डेटा लोड होने या विश्लेषण होने का चरण
लोडिंग/विश्लेषण के चरण में, Bazel यह तय करता है कि किसी नियम को बनाने के लिए कौनसी कार्रवाइयां ज़रूरी हैं. इसकी बुनियादी यूनिट "कॉन्फ़िगर किया गया टारगेट" होती है. यह (टारगेट, कॉन्फ़िगरेशन) का पेयर होता है.
इसे "डेटा लोड होने/विश्लेषण का चरण" कहा जाता है, क्योंकि इसे दो अलग-अलग हिस्सों में बांटा जा सकता है. पहले ये हिस्से क्रम से होते थे, लेकिन अब ये एक साथ हो सकते हैं:
- पैकेज लोड किए जा रहे हैं. इसका मतलब है कि
BUILD
फ़ाइलों कोPackage
ऑब्जेक्ट में बदला जा रहा है, ताकि उन्हें दिखाया जा सके - कॉन्फ़िगर किए गए टारगेट का विश्लेषण करना. इसका मतलब है कि ऐक्शन ग्राफ़ बनाने के लिए, नियमों को लागू करना
कमांड लाइन पर अनुरोध किए गए कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में कॉन्फ़िगर किए गए हर टारगेट का विश्लेषण, बॉटम-अप तरीके से किया जाना चाहिए. इसका मतलब है कि पहले लीफ़ नोड और फिर कमांड लाइन पर मौजूद नोड का विश्लेषण किया जाना चाहिए. कॉन्फ़िगर किए गए किसी एक टारगेट के विश्लेषण के लिए, ये इनपुट इस्तेमाल किए जाते हैं:
- कॉन्फ़िगरेशन. ("कैसे" उस नियम को बनाया जाए; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म के साथ-साथ, कमांड लाइन के ऐसे विकल्प जिन्हें उपयोगकर्ता C++ कंपाइलर को पास करना चाहता है)
- सीधे तौर पर निर्भरता. नियम का विश्लेषण करने के लिए, उनकी ट्रांज़िटिव जानकारी देने वाली कंपनियां उपलब्ध हैं. इन्हें ऐसा इसलिए कहा जाता है, क्योंकि ये कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र में मौजूद जानकारी को "रोल-अप" करते हैं. जैसे, क्लासपाथ पर मौजूद सभी .jar फ़ाइलें या C++ बाइनरी में लिंक की जाने वाली सभी .o फ़ाइलें)
- टारगेट. यह उस पैकेज को लोड करने का नतीजा है जिसमें टारगेट मौजूद है. नियमों के लिए, इसमें उनके एट्रिब्यूट शामिल होते हैं. आम तौर पर, यही सबसे ज़्यादा मायने रखता है.
- कॉन्फ़िगर किए गए टारगेट को लागू करना. नियमों के लिए, यह Starlark या Java में हो सकता है. नियम के मुताबिक कॉन्फ़िगर नहीं किए गए सभी टारगेट, Java में लागू किए जाते हैं.
कॉन्फ़िगर किए गए टारगेट का विश्लेषण करने पर, यह आउटपुट मिलता है:
- यह जानकारी, उन ट्रांज़िटिव इन्फ़ो प्रोवाइडर ऐप्लिकेशन ऐक्सेस कर सकते हैं जिन्होंने ऐसे टारगेट कॉन्फ़िगर किए हैं जो इस पर निर्भर करते हैं
- यह किन आर्टफ़ैक्ट को बना सकता है और उन्हें बनाने के लिए कौनसी कार्रवाइयां कर सकता है.
Java नियमों के लिए उपलब्ध एपीआई RuleContext
है, जो Starlark नियमों के ctx
आर्ग्युमेंट के बराबर है. इसका एपीआई ज़्यादा बेहतर है. हालांकि, इसका इस्तेमाल करके Bad Things™ करना भी आसान है. उदाहरण के लिए, ऐसा कोड लिखना जिसकी टाइम या स्पेस कॉम्प्लेक्सिटी क्वाड्रेटिक (या इससे भी खराब) हो, Java एक्सेप्शन की मदद से Bazel सर्वर को क्रैश करना या इनवेरिएंट का उल्लंघन करना (जैसे, अनजाने में Options
इंस्टेंस में बदलाव करना या कॉन्फ़िगर किए गए टारगेट को बदला जा सकने वाला बनाना)
कॉन्फ़िगर किए गए टारगेट की डायरेक्ट डिपेंडेंसी तय करने वाला एल्गोरिदम, DependencyResolver.dependentNodeMap()
में मौजूद होता है.
कॉन्फ़िगरेशन
कॉन्फ़िगरेशन से यह तय होता है कि टारगेट कैसे बनाया जाए: किस प्लैटफ़ॉर्म के लिए, कमांड लाइन के किन विकल्पों के साथ वगैरह.
एक ही टारगेट को एक ही बिल्ड में कई कॉन्फ़िगरेशन के लिए बनाया जा सकता है. यह उदाहरण के लिए तब काम आता है, जब एक ही कोड का इस्तेमाल ऐसे टूल के लिए किया जाता है जिसे बिल्ड के दौरान चलाया जाता है और टारगेट कोड के लिए भी. साथ ही, जब हम क्रॉस-कंपाइलिंग कर रहे हों या जब हम एक फ़ैट Android ऐप्लिकेशन (ऐसा ऐप्लिकेशन जिसमें कई सीपीयू आर्किटेक्चर के लिए नेटिव कोड होता है) बना रहे हों
कॉन्सेप्ट के हिसाब से, कॉन्फ़िगरेशन एक BuildOptions
इंस्टेंस होता है. हालांकि, व्यवहार में BuildOptions
को BuildConfiguration
में रैप किया जाता है, जो कई तरह के अतिरिक्त फ़ंक्शन उपलब्ध कराता है. यह डिपेंडेंसी ग्राफ़ में सबसे ऊपर से सबसे नीचे तक फैलता है. अगर यह बदलता है, तो बिल्ड का फिर से विश्लेषण करना होगा.
इस वजह से, कुछ समस्याएं आ सकती हैं. जैसे, अगर टेस्ट रन के अनुरोधों की संख्या में बदलाव होता है, तो पूरे बिल्ड का फिर से विश्लेषण करना पड़ता है. भले ही, इससे सिर्फ़ टेस्ट टारगेट पर असर पड़ता हो. हम कॉन्फ़िगरेशन को "ट्रिम" करने की योजना बना रहे हैं, ताकि ऐसा न हो. हालांकि, यह सुविधा अभी उपलब्ध नहीं है.
जब किसी नियम को लागू करने के लिए कॉन्फ़िगरेशन के किसी हिस्से की ज़रूरत होती है, तो उसे RuleClass.Builder.requiresConfigurationFragments()
का इस्तेमाल करके, अपनी परिभाषा में इसका एलान करना होता है. ऐसा इसलिए किया जाता है, ताकि गलतियां न हों. जैसे, Python के नियमों में Java फ़्रैगमेंट का इस्तेमाल करना. साथ ही, कॉन्फ़िगरेशन को कम करने में आसानी हो. जैसे, अगर Python के विकल्प बदलते हैं, तो C++ टारगेट का फिर से विश्लेषण करने की ज़रूरत नहीं होती.
यह ज़रूरी नहीं है कि किसी नियम का कॉन्फ़िगरेशन, उसके "पैरंट" नियम के कॉन्फ़िगरेशन जैसा हो. डिपेंडेंसी एज में कॉन्फ़िगरेशन बदलने की प्रोसेस को "कॉन्फ़िगरेशन ट्रांज़िशन" कहा जाता है. ऐसा दो जगहों पर हो सकता है:
- डिपेंडेंसी एज पर. ये ट्रांज़िशन
Attribute.Builder.cfg()
में बताए जाते हैं. येRule
(जहां ट्रांज़िशन होता है) औरBuildOptions
(ओरिजनल कॉन्फ़िगरेशन) से एक या उससे ज़्यादाBuildOptions
(आउटपुट कॉन्फ़िगरेशन) तक के फ़ंक्शन होते हैं. - कॉन्फ़िगर किए गए टारगेट में आने वाले किसी भी एज पर. इनके बारे में
RuleClass.Builder.cfg()
में बताया गया है.
इससे जुड़ी क्लास TransitionFactory
और ConfigurationTransition
हैं.
कॉन्फ़िगरेशन ट्रांज़िशन का इस्तेमाल इन कामों के लिए किया जाता है:
- यह एलान करने के लिए कि किसी खास डिपेंडेंसी का इस्तेमाल बिल्ड के दौरान किया जाता है और इसलिए इसे एक्ज़ीक्यूशन आर्किटेक्चर में बनाया जाना चाहिए
- यह एलान करने के लिए कि किसी डिपेंडेंसी को कई आर्किटेक्चर के लिए बनाया जाना चाहिए. जैसे, फ़ैट Android APK में नेटिव कोड के लिए
अगर कॉन्फ़िगरेशन ट्रांज़िशन के बाद एक से ज़्यादा कॉन्फ़िगरेशन मिलते हैं, तो इसे स्प्लिट ट्रांज़िशन कहा जाता है.
कॉन्फ़िगरेशन ट्रांज़िशन को Starlark में भी लागू किया जा सकता है. इसके बारे में जानकारी यहां दी गई है
ट्रांज़िट की जानकारी देने वाली कंपनियां
ट्रांज़िटिव जानकारी देने वाले, कॉन्फ़िगर किए गए टारगेट के लिए एक तरीका (और _सिर्फ़ _एक तरीका) है. इससे वे कॉन्फ़िगर किए गए उन टारगेट के बारे में जान पाते हैं जिन पर वे निर्भर होते हैं. साथ ही, यह कॉन्फ़िगर किए गए उन टारगेट को अपने बारे में बताने का एक तरीका है जो उन पर निर्भर होते हैं. इनके नाम में "ट्रांज़िटिव" शब्द इसलिए है, क्योंकि आम तौर पर यह कॉन्फ़िगर किए गए टारगेट के ट्रांज़िटिव क्लोज़र का रोल-अप होता है.
आम तौर पर, Java के ट्रांज़िटिव इन्फ़ो प्रोवाइडर और Starlark के ट्रांज़िटिव इन्फ़ो प्रोवाइडर के बीच 1:1 का संबंध होता है. हालांकि, DefaultInfo
इसका अपवाद है. ऐसा इसलिए है, क्योंकि यह FileProvider
, FilesToRunProvider
, और RunfilesProvider
का कॉम्बिनेशन है. इसकी वजह यह है कि इस एपीआई को Java के एपीआई के सीधे ट्रांसलिट्रेशन के बजाय, ज़्यादा Starlark-ish माना गया था.
इनकी कुंजी इनमें से कोई एक होती है:
- यह एक 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 में filegroup नियम के output_group एट्रिब्यूट का इस्तेमाल करके ऐक्सेस किया जा सकता है. साथ ही, Java में
OutputGroupInfo
provider का इस्तेमाल करके भी इन्हें ऐक्सेस किया जा सकता है.
रनफ़ाइलें
कुछ बाइनरी को चलाने के लिए, डेटा फ़ाइलों की ज़रूरत होती है. इसका एक मुख्य उदाहरण, ऐसे टेस्ट हैं जिनके लिए इनपुट फ़ाइलों की ज़रूरत होती है. Bazel में इसे "रनफ़ाइल" के कॉन्सेप्ट से दिखाया जाता है. "रनफ़ाइल्स ट्री" किसी बाइनरी के लिए डेटा फ़ाइलों का डायरेक्ट्री ट्री होता है. इसे फ़ाइल सिस्टम में, सिमलंक ट्री के तौर पर बनाया जाता है. इसमें अलग-अलग सिमलंक होते हैं. ये सिमलंक, सोर्स या आउटपुट ट्री में मौजूद फ़ाइलों की ओर इशारा करते हैं.
रनफ़ाइल के सेट को Runfiles
इंस्टेंस के तौर पर दिखाया जाता है. यह कॉन्सेप्ट के तौर पर, रनफ़ाइल्स ट्री में मौजूद किसी फ़ाइल के पाथ से लेकर, उसे दिखाने वाले Artifact
इंस्टेंस तक का मैप होता है. यह एक Map
से थोड़ा ज़्यादा जटिल है. इसकी दो वजहें हैं:
- ज़्यादातर मामलों में, किसी फ़ाइल का रनफ़ाइल पाथ, उसके execpath के जैसा ही होता है. हम इसका इस्तेमाल कुछ रैम को सेव करने के लिए करते हैं.
- रनफ़ाइल ट्री में कई तरह की लेगसी एंट्री होती हैं. इन्हें भी दिखाना ज़रूरी है.
रनफ़ाइलें, RunfilesProvider
का इस्तेमाल करके इकट्ठा की जाती हैं: इस क्लास का एक इंस्टेंस, कॉन्फ़िगर किए गए टारगेट (जैसे कि लाइब्रेरी) और उसके ट्रांज़िटिव क्लोज़र के लिए ज़रूरी रनफ़ाइलों को दिखाता है. इन्हें नेस्ट किए गए सेट की तरह इकट्ठा किया जाता है. असल में, इन्हें नेस्ट किए गए सेट का इस्तेमाल करके लागू किया जाता है: हर टारगेट, अपनी डिपेंडेंसी की रनफ़ाइलों को यूनीयन करता है, उनमें से कुछ को जोड़ता है, और फिर नतीजे के तौर पर मिले सेट को डिपेंडेंसी ग्राफ़ में ऊपर की ओर भेजता है. RunfilesProvider
इंस्टेंस में दो Runfiles
इंस्टेंस होते हैं. एक तब होता है, जब "डेटा" एट्रिब्यूट के ज़रिए नियम पर निर्भरता होती है और दूसरा हर तरह की इनकमिंग डिपेंडेंसी के लिए होता है. ऐसा इसलिए होता है, क्योंकि डेटा एट्रिब्यूट के ज़रिए किसी टारगेट पर निर्भर रहने पर, कभी-कभी अलग-अलग रनफ़ाइल दिखती हैं. यह लेगसी सिस्टम का ऐसा व्यवहार है जो हमें नहीं चाहिए. हालांकि, हम इसे अब तक हटा नहीं पाए हैं.
बाइनरी की रनफ़ाइल को RunfilesSupport
के इंस्टेंस के तौर पर दिखाया जाता है. यह Runfiles
से अलग है, क्योंकि RunfilesSupport
को बनाया जा सकता है. हालांकि, Runfiles
सिर्फ़ एक मैपिंग है. इसके लिए, इन अतिरिक्त कॉम्पोनेंट की ज़रूरत होती है:
- इनपुट रनफ़ाइल मेनिफ़ेस्ट. यह रनफ़ाइल ट्री का क्रम से दिया गया ब्यौरा है. इसका इस्तेमाल रनफ़ाइल ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. Bazel यह मानता है कि रनफ़ाइल ट्री में बदलाव सिर्फ़ तब होता है, जब मेनिफ़ेस्ट के कॉन्टेंट में बदलाव होता है.
- आउटपुट रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल रनटाइम लाइब्रेरी करती हैं. ये लाइब्रेरी, रनफ़ाइल ट्री को मैनेज करती हैं. खास तौर पर, Windows पर ऐसा होता है. 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 proto पहलू) पेयर से अटैच होना चाहेगा.
क्लास AspectCollection
में, पहलुओं की जटिलता को कैप्चर किया जाता है.
प्लैटफ़ॉर्म और टूलचेन
Bazel, एक से ज़्यादा प्लैटफ़ॉर्म पर काम करने वाले बिल्ड के साथ काम करता है. इसका मतलब है कि ऐसे बिल्ड जहां कई आर्किटेक्चर हो सकते हैं. इनमें बिल्ड ऐक्शन चलते हैं और कई आर्किटेक्चर के लिए कोड बनाया जाता है. 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)
- कॉन्फ़िगर किए गए टारगेट (
exec_compatible_with
एट्रिब्यूट) और कॉन्फ़िगरेशन (--experimental_add_exec_constraints_to_targets
) की, एक्ज़ीक्यूशन प्लैटफ़ॉर्म की पाबंदियों का सेट,UnloadedToolchainContextKey
में
इसका नतीजा UnloadedToolchainContext
होता है. यह टूलचेन टाइप (ToolchainTypeInfo
इंस्टेंस के तौर पर दिखाया गया है) से लेकर चुनी गई टूलचेन के लेबल तक का मैप होता है. इसे "अनलोड किया गया" कहा जाता है, क्योंकि इसमें टूलचेन शामिल नहीं होते. इसमें सिर्फ़ उनके लेबल होते हैं.
इसके बाद, टूलचेन को ResolvedToolchainContext.load()
का इस्तेमाल करके लोड किया जाता है. साथ ही, कॉन्फ़िगर किए गए उस टारगेट के लिए लागू किया जाता है जिसने उन्हें अनुरोध किया था.
हमारे पास एक लेगसी सिस्टम भी है. यह सिस्टम, एक ही "होस्ट" कॉन्फ़िगरेशन पर निर्भर करता है. साथ ही, टारगेट कॉन्फ़िगरेशन को अलग-अलग कॉन्फ़िगरेशन फ़्लैग से दिखाया जाता है. जैसे, --cpu
. हम धीरे-धीरे ऊपर दिए गए सिस्टम पर स्विच कर रहे हैं. ऐसे मामलों को हैंडल करने के लिए जहां लोग लेगसी कॉन्फ़िगरेशन की वैल्यू पर भरोसा करते हैं, हमने प्लैटफ़ॉर्म मैपिंग लागू की हैं. इससे लेगसी फ़्लैग और नए स्टाइल वाले प्लैटफ़ॉर्म की पाबंदियों के बीच अनुवाद किया जा सकता है.
उनका कोड PlatformMappingFunction
में है और इसमें Starlark के अलावा "little
language" का इस्तेमाल किया गया है.
कंस्ट्रेंट
कभी-कभी कोई व्यक्ति किसी टारगेट को सिर्फ़ कुछ प्लैटफ़ॉर्म के साथ काम करने वाला बनाना चाहता है. Bazel में, इस काम को पूरा करने के लिए कई तरीके हैं:
- नियम से जुड़ी शर्तें
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, 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 को शॉर्टकट के तौर पर इस्तेमाल किया जाता है. अगर फ़ाइल के ctime में बदलाव नहीं हुआ है, तो हम उसके चेकसम का हिसाब नहीं लगाते.
- ऐसे सिंबॉलिक लिंक आर्टफ़ैक्ट जिन्हें हल नहीं किया जा सका. इनकी जांच readlink() को कॉल करके की जाती है, ताकि यह पता लगाया जा सके कि ये अप-टू-डेट हैं या नहीं. सामान्य आर्टफ़ैक्ट के उलट, ये डैंगलिंग सिंबॉलिक लिंक हो सकते हैं. आम तौर पर, इसका इस्तेमाल उन मामलों में किया जाता है जहां कुछ फ़ाइलों को किसी तरह के संग्रह में पैक किया जाता है.
- पेड़ से जुड़े आर्टफ़ैक्ट. ये सिर्फ़ फ़ाइलें नहीं हैं, बल्कि डायरेक्ट्री ट्री हैं. इनकी जांच यह देखने के लिए की जाती है कि ये अप-टू-डेट हैं या नहीं. इसके लिए, इनमें मौजूद फ़ाइलों और उनके कॉन्टेंट की जांच की जाती है. इन्हें
TreeArtifact
के तौर पर दिखाया जाता है. - मेटाडेटा के आर्टफ़ैक्ट में बदलाव नहीं किया जा सकता. इन आर्टफ़ैक्ट में किए गए बदलावों से, फिर से बनाने की प्रोसेस ट्रिगर नहीं होती. इसका इस्तेमाल सिर्फ़ बिल्ड स्टैंप की जानकारी के लिए किया जाता है: हम सिर्फ़ इसलिए फिर से बिल्ड नहीं करना चाहते, क्योंकि मौजूदा समय बदल गया है.
सोर्स आर्टफ़ैक्ट, ट्री आर्टफ़ैक्ट या अनसुलझे सिंबॉलिक लिंक आर्टफ़ैक्ट क्यों नहीं हो सकते, इसकी कोई बुनियादी वजह नहीं है. फ़िलहाल, हमने इसे लागू नहीं किया है. हालांकि, हमें इसे लागू करना चाहिए. BUILD
फ़ाइल में सोर्स डायरेक्ट्री को रेफ़रंस करना, Bazel से जुड़ी कुछ ऐसी समस्याओं में से एक है जो लंबे समय से ठीक नहीं हुई हैं. हमारे पास एक ऐसा तरीका है जो कुछ हद तक काम करता है. इसे BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM प्रॉपर्टी की मदद से चालू किया जाता है
कार्रवाइयों को सबसे अच्छी तरह से एक ऐसे कमांड के तौर पर समझा जा सकता है जिसे चलाने की ज़रूरत होती है. साथ ही, इसके लिए ज़रूरी एनवायरमेंट और इससे मिलने वाले आउटपुट के सेट के बारे में भी जानकारी मिलती है. किसी कार्रवाई के ब्यौरे में ये मुख्य कॉम्पोनेंट शामिल होते हैं:
- वह कमांड लाइन जिसे चलाना है
- इनपुट आर्टफ़ैक्ट की ज़रूरत होती है
- सेट किए जाने वाले एनवायरमेंट वैरिएबल
- ऐसी व्याख्याएं जिनमें उस एनवायरमेंट (जैसे कि प्लैटफ़ॉर्म) के बारे में बताया गया हो जिसमें इसे चलाया जाना है \
इसके अलावा, कुछ अन्य खास मामले भी हैं. जैसे, ऐसी फ़ाइल लिखना जिसका कॉन्टेंट Bazel को पता है. ये AbstractAction
के सबक्लास हैं. ज़्यादातर कार्रवाइयां SpawnAction
या StarlarkAction
होती हैं. ये दोनों एक ही हैं. इन्हें अलग-अलग क्लास में नहीं रखा जाना चाहिए. हालांकि, Java और C++ में कार्रवाइयों के अपने टाइप होते हैं (JavaCompileAction
, CppCompileAction
, और CppLinkAction
).
हमारा मकसद, सभी फ़ाइलों को SpawnAction
में ले जाना है. JavaCompileAction
, SpawnAction
के काफ़ी करीब है. हालांकि, C++ के मामले में कुछ खास बातें हैं. जैसे, .d फ़ाइल पार्स करना और फ़ाइलें शामिल करने के लिए स्कैन करना.
ऐक्शन ग्राफ़ को ज़्यादातर Skyframe ग्राफ़ में "एम्बेड" किया जाता है: कॉन्सेप्ट के तौर पर, किसी ऐक्शन को ActionExecutionFunction
के इनवोकेशन के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से Skyframe डिपेंडेंसी एज की मैपिंग के बारे में ActionExecutionFunction.getInputDeps()
और Artifact.key()
में बताया गया है. साथ ही, Skyframe एज की संख्या कम रखने के लिए, इसमें कुछ ऑप्टिमाइज़ेशन किए गए हैं:
- डिराइव किए गए आर्टफ़ैक्ट के अपने
SkyValue
नहीं होते. इसके बजाय,Artifact.getGeneratingActionKey()
का इस्तेमाल करके, उस कार्रवाई के लिए कुंजी का पता लगाया जाता है जो इसे जनरेट करती है - नेस्ट किए गए सेट की अपनी Skyframe कुंजी होती है.
शेयर की गई कार्रवाइयां
कुछ कार्रवाइयां, कॉन्फ़िगर किए गए कई टारगेट से जनरेट होती हैं. स्टार्लार्क के नियम ज़्यादा सीमित होते हैं, क्योंकि उन्हें सिर्फ़ अपनी डिराइव की गई कार्रवाइयों को उस डायरेक्ट्री में रखने की अनुमति होती है जिसे उनके कॉन्फ़िगरेशन और पैकेज से तय किया जाता है. हालांकि, ऐसा होने पर भी एक ही पैकेज में मौजूद नियमों में टकराव हो सकता है. वहीं, Java में लागू किए गए नियम, डिराइव किए गए आर्टफ़ैक्ट को कहीं भी रख सकते हैं.
इसे एक गड़बड़ी माना जाता है, लेकिन इससे छुटकारा पाना बहुत मुश्किल है. ऐसा इसलिए, क्योंकि इससे एक्ज़ीक्यूशन के समय में काफ़ी बचत होती है. उदाहरण के लिए, जब किसी सोर्स फ़ाइल को किसी तरह से प्रोसेस करने की ज़रूरत होती है और उस फ़ाइल को कई नियमों से रेफ़र किया जाता है (हैंडवेव-हैंडवेव). हालांकि, इसके लिए कुछ रैम की ज़रूरत होती है: शेयर की गई हर कार्रवाई के इंस्टेंस को मेमोरी में अलग से सेव करना होता है.
अगर दो कार्रवाइयों से एक ही आउटपुट फ़ाइल जनरेट होती है, तो वे एक जैसी होनी चाहिए:
उनमें एक जैसे इनपुट होने चाहिए, एक जैसे आउटपुट होने चाहिए, और एक ही कमांड लाइन चलनी चाहिए. यह
समानता संबंध, Actions.canBeShared()
में लागू किया जाता है. साथ ही, इसकी पुष्टि विश्लेषण और एक्ज़ीक्यूशन फ़ेज़ के बीच की जाती है. इसके लिए, हर कार्रवाई को देखा जाता है.
इसे SkyframeActionExecutor.findAndStoreArtifactConflicts()
में लागू किया गया है. साथ ही, Bazel में यह कुछ ऐसी जगहों में से एक है जहां बिल्ड का "ग्लोबल" व्यू देखना ज़रूरी होता है.
एक्ज़ीक्यूशन फ़ेज़
इस दौरान Bazel, बिल्ड ऐक्शन शुरू करता है. जैसे, आउटपुट जनरेट करने वाली कमांड.
विश्लेषण के चरण के बाद, Bazel सबसे पहले यह तय करता है कि किन आर्टफ़ैक्ट को बनाना है. इसके लिए लॉजिक, TopLevelArtifactHelper
में कोड किया गया है. आसान शब्दों में कहें, तो यह कमांड लाइन पर कॉन्फ़िगर किए गए टारगेट का filesToBuild
है. साथ ही, यह एक खास आउटपुट ग्रुप के कॉन्टेंट का filesToBuild
है. इसका मकसद "अगर यह टारगेट कमांड लाइन पर है, तो इन आर्टफ़ैक्ट को बनाएं" को साफ़ तौर पर बताना है.
अगला चरण, एक्ज़ीक्यूशन रूट बनाना है. Bazel के पास फ़ाइल सिस्टम में अलग-अलग जगहों से सोर्स पैकेज पढ़ने का विकल्प होता है (--package_path
). इसलिए, इसे स्थानीय तौर पर लागू की गई कार्रवाइयों के लिए, पूरा सोर्स ट्री उपलब्ध कराना होता है. इसे क्लास SymlinkForest
मैनेज करती है. यह क्लास, विश्लेषण के चरण में इस्तेमाल किए गए हर टारगेट पर ध्यान देती है. साथ ही, एक ऐसा डायरेक्ट्री ट्री बनाती है जो हर पैकेज को उसकी असल जगह से इस्तेमाल किए गए टारगेट के साथ सिंबल के तौर पर लिंक करता है. इसके अलावा, कमांड को सही पाथ पास किए जा सकते हैं. इसके लिए, --package_path
को ध्यान में रखें.
यह इसलिए सही नहीं है, क्योंकि:
- जब किसी पैकेज को एक पैकेज पाथ एंट्री से दूसरी एंट्री में ले जाया जाता है, तो यह ऐक्शन कमांड लाइन बदलता है. ऐसा अक्सर होता था
- अगर कोई कार्रवाई रिमोट से की जाती है, तो कमांड लाइन अलग होती है. वहीं, अगर कार्रवाई स्थानीय तौर पर की जाती है, तो कमांड लाइन अलग होती है
- इसके लिए, इस्तेमाल किए जा रहे टूल के हिसाब से कमांड लाइन ट्रांसफ़ॉर्मेशन की ज़रूरत होती है (जैसे, Java क्लासपाथ और C++ में शामिल पाथ के बीच के अंतर पर ध्यान दें)
- किसी कार्रवाई की कमांड लाइन बदलने से, उसकी ऐक्शन कैश मेमोरी की एंट्री अमान्य हो जाती है
--package_path
को धीरे-धीरे बंद किया जा रहा है
इसके बाद, Bazel ऐक्शन ग्राफ़ (द्विभागी, डायरेक्टेड ग्राफ़, जिसमें कार्रवाइयां और उनके इनपुट और आउटपुट आर्टफ़ैक्ट शामिल होते हैं) को ट्रैवर्स करना और कार्रवाइयां करना शुरू कर देता है.
हर कार्रवाई को SkyValue
क्लास ActionExecutionValue
के इंस्टेंस के तौर पर दिखाया जाता है.
कार्रवाई करने में ज़्यादा समय लगता है. इसलिए, हमारे पास कैश मेमोरी की कुछ लेयर होती हैं. ये Skyframe के पीछे काम करती हैं:
ActionExecutionFunction.stateMap
में Skyframe कोActionExecutionFunction
से रीस्टार्ट करने के लिए डेटा होता है- लोकल ऐक्शन कैश मेमोरी में, फ़ाइल सिस्टम की स्थिति के बारे में डेटा होता है
- रिमोट एक्ज़ीक्यूशन सिस्टम में आम तौर पर, अपनी कैश मेमोरी भी होती है
लोकल ऐक्शन कैश
यह कैश, Skyframe के पीछे मौजूद एक और लेयर है. अगर Skyframe में किसी कार्रवाई को फिर से लागू किया जाता है, तो भी यह लोकल ऐक्शन कैश में हिट हो सकता है. यह लोकल फ़ाइल सिस्टम की स्थिति को दिखाता है. इसे डिस्क पर क्रम से लगाया जाता है. इसका मतलब है कि जब कोई नया Bazel सर्वर शुरू होता है, तो Skyframe ग्राफ़ खाली होने के बावजूद, लोकल ऐक्शन कैश मेमोरी से हिट मिल सकते हैं.
इस कैश मेमोरी में हिट की जांच करने के लिए, ActionCacheChecker.getTokenIfNeedToExecute()
तरीके का इस्तेमाल किया जाता है .
इसके नाम के उलट, यह मैप, किसी डिराइव किए गए आर्टफ़ैक्ट के पाथ से लेकर उसे जनरेट करने वाली कार्रवाई तक का होता है. इस कार्रवाई के बारे में यहां बताया गया है:
- इनपुट और आउटपुट फ़ाइलों का सेट और उनका चेकसम
- इसकी "ऐक्शन की". आम तौर पर, यह वह कमांड लाइन होती है जिसे लागू किया गया था. हालांकि, आम तौर पर यह उन सभी चीज़ों को दिखाती है जिन्हें इनपुट फ़ाइलों के चेकसम से कैप्चर नहीं किया जाता. जैसे,
FileWriteAction
के लिए, यह लिखे गए डेटा का चेकसम होता है
"टॉप-डाउन ऐक्शन कैश मेमोरी" नाम की एक सुविधा भी है. यह सुविधा अभी डेवलपमेंट के चरण में है. यह ट्रांज़िटिव हैश का इस्तेमाल करती है, ताकि कैश मेमोरी में बार-बार न जाना पड़े.
इनपुट का पता लगाना और इनपुट को कम करना
कुछ कार्रवाइयां, सिर्फ़ इनपुट का सेट होने से ज़्यादा मुश्किल होती हैं. किसी कार्रवाई के इनपुट के सेट में दो तरह से बदलाव होते हैं:
- कार्रवाई शुरू होने से पहले, उसे नए इनपुट मिल सकते हैं. इसके अलावा, वह यह भी तय कर सकती है कि उसके कुछ इनपुट ज़रूरी नहीं हैं. इसका सबसे अच्छा उदाहरण C++ है. यहां यह अनुमान लगाना बेहतर होता है कि C++ फ़ाइल, ट्रांज़िटिव क्लोज़र से किन हेडर फ़ाइलों का इस्तेमाल करती है, ताकि हमें हर फ़ाइल को रिमोट एक्ज़ीक्यूटर को न भेजना पड़े. इसलिए, हमारे पास हर हेडर फ़ाइल को "इनपुट" के तौर पर रजिस्टर न करने का विकल्प होता है. इसके बजाय, हम ट्रांज़िटिव तौर पर शामिल हेडर के लिए सोर्स फ़ाइल को स्कैन करते हैं और सिर्फ़ उन हेडर फ़ाइलों को इनपुट के तौर पर मार्क करते हैं जिनका ज़िक्र
#include
स्टेटमेंट में किया गया है. हम अनुमान को ज़्यादा रखते हैं, ताकि हमें पूरे C प्रीप्रोसेसर को लागू न करना पड़े. फ़िलहाल, Bazel में यह विकल्प "false" पर सेट है और इसका इस्तेमाल सिर्फ़ Google में किया जाता है. - कार्रवाई के दौरान, यह पता चल सकता है कि कुछ फ़ाइलों का इस्तेमाल नहीं किया गया. C++ में, इसे ".d फ़ाइलें" कहा जाता है: कंपाइलर बताता है कि कौनसी हेडर फ़ाइलें इस्तेमाल की गई थीं. साथ ही, Make की तुलना में बेहतर इंक्रीमेंटैलिटी पाने के लिए, Bazel इस फ़ैक्ट का इस्तेमाल करता है. यह, कंपाइलर पर निर्भर होने की वजह से, स्कैनर को शामिल करने के मुकाबले बेहतर अनुमान देता है.
इन्हें Action पर मौजूद तरीकों का इस्तेमाल करके लागू किया जाता है:
Action.discoverInputs()
को कॉल किया जाता है. इससे आर्टफ़ैक्ट का नेस्ट किया गया सेट दिखना चाहिए. यह सेट, ज़रूरी आर्टफ़ैक्ट का सेट होता है. ये सोर्स आर्टफ़ैक्ट होने चाहिए, ताकि ऐक्शन ग्राफ़ में ऐसे कोई डिपेंडेंसी एज न हों जिनका कॉन्फ़िगर किए गए टारगेट ग्राफ़ में कोई मिलता-जुलता एज न हो.Action.execute()
को कॉल करके कार्रवाई की जाती है.Action.execute()
के आखिर में, कार्रवाईAction.updateInputs()
को कॉल करके यह बता सकती है कि उसके सभी इनपुट की ज़रूरत नहीं थी. अगर इस्तेमाल किए गए इनपुट को इस्तेमाल नहीं किया गया के तौर पर रिपोर्ट किया जाता है, तो इससे इंक्रीमेंटल बिल्ड गलत हो सकते हैं.
जब कोई ऐक्शन कैश, नए ऐक्शन इंस्टेंस (जैसे, सर्वर रीस्टार्ट होने के बाद बनाया गया) पर हिट दिखाता है, तो Bazel updateInputs()
को कॉल करता है. ऐसा इसलिए किया जाता है, ताकि इनपुट का सेट, पहले किए गए इनपुट की खोज और छंटाई के नतीजे को दिखा सके.
Starlark ऐक्शन, कुछ इनपुट को इस्तेमाल नहीं किया गया के तौर पर मार्क करने की सुविधा का इस्तेमाल कर सकते हैं. इसके लिए, ctx.actions.run()
के unused_inputs_list=
आर्ग्युमेंट का इस्तेमाल करें.
कार्रवाइयां करने के अलग-अलग तरीके: Strategies/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 के नियम हैं. इनके लेखकों को आम तौर पर Bazel के बारे में ज़्यादा जानकारी नहीं होती. इसके अलावा, पहलू भी एक समस्या है. ये "एक जैसी" आउटपुट फ़ाइलें जनरेट करने वाली चीज़ों के स्पेस में एक और डाइमेंशन जोड़ते हैं.
मौजूदा तरीके में, कॉन्फ़िगरेशन के लिए पाथ सेगमेंट <CPU>-<compilation mode>
है. इसमें अलग-अलग सफ़िक्स जोड़े गए हैं, ताकि Java में लागू किए गए कॉन्फ़िगरेशन ट्रांज़िशन की वजह से कार्रवाई में कोई टकराव न हो. इसके अलावा, Starlark कॉन्फ़िगरेशन ट्रांज़िशन के सेट का चेकसम जोड़ा जाता है, ताकि उपयोगकर्ता कार्रवाइयों में टकराव न कर पाएं. यह पूरी तरह से सही नहीं है. इसे OutputDirectories.buildMnemonic()
में लागू किया जाता है. साथ ही, यह हर कॉन्फ़िगरेशन फ़्रैगमेंट पर निर्भर करता है, जो आउटपुट डायरेक्ट्री के नाम में अपना हिस्सा जोड़ता है.
जांच
Bazel में, टेस्ट चलाने की कई सुविधाएं उपलब्ध हैं. यह इन पर काम करता है:
- टेस्ट को रिमोटली चलाना (अगर रिमोट एक्ज़ीक्यूशन बैकएंड उपलब्ध है)
- एक साथ कई बार टेस्ट चलाना (फ़्लेकिंग कम करने या समय का डेटा इकट्ठा करने के लिए)
- शार्डिंग टेस्ट (एक ही टेस्ट के टेस्ट केस को कई प्रोसेस में बांटना, ताकि टेस्ट तेज़ी से हो सके)
- फ़्लेकी टेस्ट को फिर से चलाना
- टेस्ट को टेस्ट सुइट में ग्रुप करना
टेस्ट, कॉन्फ़िगर किए गए सामान्य टारगेट होते हैं. इनमें TestProvider होता है. इससे यह पता चलता है कि टेस्ट को कैसे चलाया जाना चाहिए:
- ऐसे आर्टफ़ैक्ट जिनके बनने से टेस्ट चल रहा है. यह "cache
status" फ़ाइल है. इसमें क्रम से लगाया गया
TestResultData
मैसेज होता है - टेस्ट को कितनी बार चलाना है
- टेस्ट को जितने हिस्सों में बांटना है उनकी संख्या
- टेस्ट को कैसे चलाया जाना चाहिए, इस बारे में कुछ पैरामीटर (जैसे, टेस्ट टाइमआउट)
यह तय करना कि कौनसे टेस्ट चलाने हैं
यह तय करना कि कौनसे टेस्ट चलाए जाएं, एक लंबी प्रोसेस है.
सबसे पहले, टारगेट पैटर्न पार्सिंग के दौरान, टेस्ट सुइट को बार-बार बड़ा किया जाता है. इस सुविधा को TestsForTargetPatternFunction
में लागू किया गया है. थोड़ा हैरान करने वाली बात यह है कि अगर कोई टेस्ट सुइट किसी भी टेस्ट का एलान नहीं करता है, तो इसका मतलब है कि वह अपने पैकेज में मौजूद हर टेस्ट को रेफ़र करता है. इसे Package.beforeBuild()
में लागू किया जाता है. इसके लिए, टेस्ट सुइट के नियमों में $implicit_tests
नाम का एक इंप्लिसिट एट्रिब्यूट जोड़ा जाता है.
इसके बाद, कमांड लाइन के विकल्पों के हिसाब से, साइज़, टैग, टाइमआउट, और भाषा के लिए टेस्ट फ़िल्टर किए जाते हैं. इसे TestFilter
में लागू किया जाता है. साथ ही, टारगेट पार्सिंग के दौरान इसे TargetPatternPhaseFunction.determineTests()
से कॉल किया जाता है. इसके बाद, नतीजे को TargetPatternPhaseValue.getTestsToRunLabels()
में रखा जाता है. नियम के जिन एट्रिब्यूट के लिए फ़िल्टर किया जा सकता है उन्हें कॉन्फ़िगर नहीं किया जा सकता. इसकी वजह यह है कि ऐसा विश्लेषण के चरण से पहले होता है. इसलिए, कॉन्फ़िगरेशन उपलब्ध नहीं होता.
इसके बाद, BuildView.createResult()
में इस डेटा को प्रोसेस किया जाता है: जिन टारगेट का विश्लेषण नहीं हो सका उन्हें फ़िल्टर कर दिया जाता है. साथ ही, टेस्ट को एक्सक्लूसिव और नॉन-एक्सक्लूसिव टेस्ट में बांट दिया जाता है. इसके बाद, इसे AnalysisResult
में रखा जाता है. इससे ExecutionTool
को पता चलता है कि कौनसे टेस्ट चलाने हैं.
इस लंबी प्रोसेस को ज़्यादा पारदर्शी बनाने के लिए, tests()
क्वेरी ऑपरेटर (TestsFunction
में लागू किया गया) उपलब्ध है. इससे यह पता चलता है कि कमांड लाइन पर किसी टारगेट को तय करने पर, कौनसे टेस्ट
चलाए जाते हैं. यह दुर्भाग्य से फिर से लागू किया गया है. इसलिए, यह शायद ऊपर दिए गए तरीके से कई तरह से अलग है.
टेस्ट चलाए जा रहे हैं
जांच करने के लिए, कैश मेमोरी की स्थिति के आर्टफ़ैक्ट का अनुरोध किया जाता है. इसके बाद, TestRunnerAction
को लागू किया जाता है. यह TestActionContext
को कॉल करता है. TestActionContext
को --test_strategy
कमांड लाइन विकल्प से चुना जाता है. यह विकल्प, टेस्ट को अनुरोध किए गए तरीके से चलाता है.
टेस्ट, एक तय प्रोटोकॉल के हिसाब से चलाए जाते हैं. इस प्रोटोकॉल में एनवायरमेंट वैरिएबल का इस्तेमाल किया जाता है. इससे टेस्ट को यह पता चलता है कि उनसे क्या उम्मीद की जाती है. Bazel को टेस्ट से क्या उम्मीदें हैं और टेस्ट को Bazel से क्या उम्मीदें हैं, इस बारे में पूरी जानकारी यहां दी गई है. आसान शब्दों में कहें, तो एक्ज़िट कोड 0 का मतलब है कि प्रोसेस पूरी हो गई है. इसके अलावा, किसी भी अन्य कोड का मतलब है कि प्रोसेस पूरी नहीं हुई है.
कैश मेमोरी के स्टेटस की फ़ाइल के अलावा, हर टेस्ट प्रोसेस कई अन्य फ़ाइलें जनरेट करती है. इन्हें "टेस्ट लॉग डायरेक्ट्री" में रखा जाता है. यह टारगेट कॉन्फ़िगरेशन की आउटपुट डायरेक्ट्री की सबडायरेक्ट्री होती है. इसका नाम testlogs
होता है:
test.xml
, JUnit-स्टाइल वाली एक्सएमएल फ़ाइल. इसमें टेस्ट शार्ड में मौजूद अलग-अलग टेस्ट केस के बारे में जानकारी होती हैtest.log
, टेस्ट का कंसोल आउटपुट. stdout और stderr अलग-अलग नहीं होते.test.outputs
, "undeclared outputs directory"; इसका इस्तेमाल उन टेस्ट के लिए किया जाता है जो टर्मिनल पर प्रिंट करने के अलावा, फ़ाइलें भी आउटपुट करना चाहते हैं.
टेस्ट को लागू करने के दौरान दो चीज़ें हो सकती हैं, जो सामान्य टारगेट बनाने के दौरान नहीं हो सकतीं: एक्सक्लूसिव टेस्ट को लागू करना और आउटपुट स्ट्रीमिंग.
कुछ टेस्ट को एक्सक्लूसिव मोड में चलाना होता है. उदाहरण के लिए, इन्हें अन्य टेस्ट के साथ पैरलल में नहीं चलाया जा सकता. इसे टेस्ट के नियम में tags=["exclusive"]
जोड़कर या --test_strategy=exclusive
के साथ टेस्ट चलाकर ट्रिगर किया जा सकता है . हर एक्सक्लूसिव टेस्ट, Skyframe के अलग इनवोकेशन से चलाया जाता है. यह "मुख्य" बिल्ड के बाद टेस्ट को चलाने का अनुरोध करता है. इसे SkyframeExecutor.runExclusiveTest()
में लागू किया गया है.
सामान्य कार्रवाइयों के पूरा होने पर, उनका फ़ाइनल आउटपुट डंप कर दिया जाता है. हालांकि, उपयोगकर्ता टेस्ट के आउटपुट को स्ट्रीम करने का अनुरोध कर सकता है, ताकि उसे लंबे समय तक चलने वाले टेस्ट की प्रोग्रेस के बारे में जानकारी मिलती रहे. इसे --test_output=streamed
कमांड लाइन विकल्प से तय किया जाता है. इसका मतलब है कि टेस्ट को सिर्फ़ एक बार चलाया जाएगा, ताकि अलग-अलग टेस्ट के आउटपुट एक-दूसरे में न मिलें.
इसे StreamedTestOutput
क्लास में लागू किया गया है. यह इस तरह काम करता है कि यह उस टेस्ट की test.log
फ़ाइल में हुए बदलावों को पोल करता है और नए बाइट को उस टर्मिनल पर डंप करता है जहां Bazel के नियम लागू होते हैं.
जांची गई चीज़ों के नतीजे, इवेंट बस पर उपलब्ध होते हैं. इसके लिए, अलग-अलग इवेंट (जैसे कि TestAttempt
, TestResult
या TestingCompleteEvent
) देखे जाते हैं. इन नतीजों को Build Event Protocol में डंप किया जाता है. साथ ही, इन्हें AggregatingTestListener
की मदद से कंसोल पर भेजा जाता है.
कवरेज कलेक्शन
कवरेज की जानकारी, LCOV फ़ॉर्मैट में टेस्ट के ज़रिए रिपोर्ट की जाती है. यह जानकारी इन फ़ाइलों में होती है
bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
.
कवरेज इकट्ठा करने के लिए, हर टेस्ट एक्ज़ीक्यूशन को collect_coverage.sh
नाम की स्क्रिप्ट में रैप किया जाता है .
यह स्क्रिप्ट, टेस्ट का एनवायरमेंट सेट अप करती है, ताकि कवरेज कलेक्शन चालू किया जा सके. साथ ही, यह तय किया जा सके कि कवरेज रनटाइम, कवरेज फ़ाइलें कहां लिखता है. इसके बाद, यह जांच करता है. ऐसा हो सकता है कि कोई टेस्ट खुद कई सबप्रोसेस चलाता हो और उसमें अलग-अलग प्रोग्रामिंग भाषाओं में लिखे गए हिस्से शामिल हों. साथ ही, कवरेज कलेक्शन के रनटाइम अलग-अलग हों. रैपर स्क्रिप्ट, ज़रूरत पड़ने पर नतीजे वाली फ़ाइलों को LCOV फ़ॉर्मैट में बदलती है. साथ ही, उन्हें एक फ़ाइल में मर्ज करती है.
collect_coverage.sh
को टेस्ट की रणनीतियों के हिसाब से इंटरपोज़ किया जाता है. इसके लिए, collect_coverage.sh
को टेस्ट के इनपुट पर होना ज़रूरी है. ऐसा, इंप्लिसिट एट्रिब्यूट :coverage_support
की मदद से किया जाता है. इसे कॉन्फ़िगरेशन फ़्लैग --coverage_support
की वैल्यू के तौर पर सेट किया जाता है (TestConfiguration.TestOptions.coverageSupport
देखें)
कुछ भाषाएं ऑफ़लाइन इंस्ट्रुमेंटेशन करती हैं. इसका मतलब है कि कवरेज इंस्ट्रुमेंटेशन को कंपाइल करने के समय जोड़ा जाता है. जैसे, C++. वहीं, कुछ भाषाएं ऑनलाइन इंस्ट्रुमेंटेशन करती हैं. इसका मतलब है कि कवरेज इंस्ट्रुमेंटेशन को लागू करने के समय जोड़ा जाता है.
एक और मुख्य सिद्धांत बेसलाइन कवरेज है. यह किसी लाइब्रेरी, बाइनरी या टेस्ट का कवरेज है. अगर इसमें कोई कोड नहीं चलाया गया है, तो यह कवरेज है. इस समस्या को हल करने के लिए, अगर आपको किसी बाइनरी के लिए टेस्ट कवरेज का हिसाब लगाना है, तो सभी टेस्ट के कवरेज को मर्ज करना काफ़ी नहीं है. ऐसा इसलिए, क्योंकि बाइनरी में ऐसा कोड हो सकता है जो किसी भी टेस्ट से लिंक न हो. इसलिए, हम हर बाइनरी के लिए एक कवरेज फ़ाइल जनरेट करते हैं. इसमें सिर्फ़ वे फ़ाइलें शामिल होती हैं जिनके लिए हम कवरेज इकट्ठा करते हैं. इसमें कोई भी लाइन शामिल नहीं होती है. किसी टारगेट के लिए, डिफ़ॉल्ट बेसलाइन कवरेज फ़ाइल bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
पर होती है. हालांकि, नियमों को यह सुझाव दिया जाता है कि वे अपनी बेसलाइन कवरेज फ़ाइलें जनरेट करें. इनमें सोर्स फ़ाइलों के नामों के अलावा, ज़्यादा काम का कॉन्टेंट शामिल करें.
हम हर नियम के लिए कवरेज का डेटा इकट्ठा करने के लिए, फ़ाइलों के दो ग्रुप ट्रैक करते हैं: इंस्ट्रुमेंट की गई फ़ाइलों का सेट और इंस्ट्रुमेंटेशन मेटाडेटा फ़ाइलों का सेट.
इंस्ट्रुमेंट की गई फ़ाइलों का सेट सिर्फ़ इंस्ट्रुमेंट करने के लिए फ़ाइलों का एक सेट होता है. ऑनलाइन कवरेज रनटाइम के लिए, इसका इस्तेमाल रनटाइम में यह तय करने के लिए किया जा सकता है कि किन फ़ाइलों को इंस्ट्रुमेंट करना है. इसका इस्तेमाल, बेसलाइन कवरेज को लागू करने के लिए भी किया जाता है.
इंस्ट्रुमेंटेशन मेटाडेटा फ़ाइलों का सेट, अतिरिक्त फ़ाइलों का वह सेट होता है जिसकी ज़रूरत किसी टेस्ट को LCOV फ़ाइलें जनरेट करने के लिए होती है. Bazel को इन फ़ाइलों की ज़रूरत होती है. असल में, इसमें रनटाइम से जुड़ी फ़ाइलें शामिल होती हैं. उदाहरण के लिए, gcc कंपाइल करने के दौरान .gcno फ़ाइलें बनाता है. अगर कवरेज मोड चालू है, तो इन्हें टेस्ट ऐक्शन के इनपुट के सेट में जोड़ा जाता है.
कवरेज का डेटा इकट्ठा किया जा रहा है या नहीं, यह जानकारी BuildConfiguration
में सेव की जाती है. यह फ़ंक्शन इसलिए काम का है, क्योंकि इसकी मदद से टेस्ट ऐक्शन और ऐक्शन ग्राफ़ को आसानी से बदला जा सकता है. हालांकि, इसका मतलब यह भी है कि अगर यह बिट फ़्लिप हो जाती है, तो सभी टारगेट का फिर से विश्लेषण करना होगा. कुछ भाषाओं, जैसे कि C++ के लिए, कवरेज इकट्ठा करने वाला कोड जनरेट करने के लिए, कंपाइलर के अलग-अलग विकल्पों की ज़रूरत होती है. इससे इस समस्या को कुछ हद तक कम किया जा सकता है, क्योंकि इसके बाद फिर से विश्लेषण करना ज़रूरी होता है.
कवरेज की जानकारी देने वाली फ़ाइलें, लेबल पर निर्भर करती हैं. ऐसा इसलिए होता है, ताकि इन्हें इनवॉकेशन की नीति के तहत बदला जा सके. इससे, Bazel के अलग-अलग वर्शन के बीच अंतर किया जा सकता है. हमारा सुझाव है कि इन अंतरों को हटा दिया जाए और हम इनमें से किसी एक को स्टैंडर्ड के तौर पर इस्तेमाल करें.
हम "कवरेज रिपोर्ट" भी जनरेट करते हैं. इसमें Bazel इनवोकेशन में हर टेस्ट के लिए इकट्ठा किए गए कवरेज को मर्ज किया जाता है. इसे CoverageReportActionFactory
हैंडल करता है और इसे BuildView.createResult()
से कॉल किया जाता है . यह उन टूल को ऐक्सेस करता है जिनकी इसे ज़रूरत होती है. इसके लिए, यह पहले टेस्ट के :coverage_report_generator
एट्रिब्यूट को देखता है.
क्वेरी इंजन
Bazel में एक छोटी भाषा होती है. इसका इस्तेमाल, अलग-अलग ग्राफ़ के बारे में अलग-अलग चीज़ें पूछने के लिए किया जाता है. क्वेरी के ये टाइप उपलब्ध हैं:
bazel query
का इस्तेमाल, टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel cquery
का इस्तेमाल, कॉन्फ़िगर किए गए टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel aquery
का इस्तेमाल, ऐक्शन ग्राफ़ की जांच करने के लिए किया जाता है
इनमें से हर एक को, AbstractBlazeQueryEnvironment
की सबक्लासिंग करके लागू किया जाता है.
QueryFunction
को सबक्लास करके, क्वेरी के अतिरिक्त फ़ंक्शन किए जा सकते हैं
. स्ट्रीमिंग क्वेरी के नतीजों को अनुमति देने के लिए, उन्हें किसी डेटा स्ट्रक्चर में इकट्ठा करने के बजाय, query2.engine.Callback
को QueryFunction
में पास किया जाता है. QueryFunction
, उन नतीजों के लिए query2.engine.Callback
को कॉल करता है जिन्हें उसे दिखाना होता है.
क्वेरी का नतीजा कई तरीकों से दिखाया जा सकता है: लेबल, लेबल और नियम की क्लास, XML, प्रोटोबफ़ वगैरह. इन्हें OutputFormatter
के सबक्लास के तौर पर लागू किया जाता है.
कुछ क्वेरी आउटपुट फ़ॉर्मैट (जैसे, प्रोटो) के लिए यह ज़रूरी है कि Bazel, पैकेज लोड करने से जुड़ी _पूरी _जानकारी दे, ताकि आउटपुट की तुलना की जा सके और यह पता लगाया जा सके कि किसी टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को क्रम से लगाया जाना चाहिए. यही वजह है कि कुछ ही एट्रिब्यूट टाइप ऐसे हैं जिनमें जटिल Starlark वैल्यू वाले एट्रिब्यूट नहीं हैं. इसके लिए, आम तौर पर लेबल का इस्तेमाल किया जाता है. साथ ही, उस लेबल वाले नियम में जटिल जानकारी जोड़ी जाती है. यह समस्या हल करने का कोई संतोषजनक तरीका नहीं है. इसलिए, इस ज़रूरी शर्त को हटा दिया जाना चाहिए.
मॉड्यूल सिस्टम
Bazel में मॉड्यूल जोड़कर, इसकी सुविधाओं को बढ़ाया जा सकता है. हर मॉड्यूल को BlazeModule
(यह नाम Bazel के इतिहास से लिया गया है, जब इसे Blaze कहा जाता था) का सबक्लास होना चाहिए. साथ ही, इसे किसी कमांड के एक्ज़ीक्यूशन के दौरान अलग-अलग इवेंट के बारे में जानकारी मिलती है.
इनका इस्तेमाल ज़्यादातर "नॉन-कोर" फ़ंक्शन को लागू करने के लिए किया जाता है. ये फ़ंक्शन सिर्फ़ Bazel के कुछ वर्शन (जैसे, Google में इस्तेमाल किया जाने वाला वर्शन) के लिए ज़रूरी होते हैं:
- रिमोट एक्ज़ीक्यूशन सिस्टम के इंटरफ़ेस
- नए निर्देश
एक्सटेंशन पॉइंट BlazeModule
के ऑफ़र कुछ हद तक बेतरतीब हैं. इसे अच्छे डिज़ाइन सिद्धांतों के उदाहरण के तौर पर इस्तेमाल न करें.
इवेंट बस
BlazeModules, Bazel के बाकी हिस्सों के साथ इवेंट बस (EventBus
) के ज़रिए कम्यूनिकेट करते हैं: हर बिल्ड के लिए एक नया इंस्टेंस बनाया जाता है. Bazel के अलग-अलग हिस्से, इस पर इवेंट पोस्ट कर सकते हैं. साथ ही, मॉड्यूल उन इवेंट के लिए लिसनर रजिस्टर कर सकते हैं जिनमें उनकी दिलचस्पी है. उदाहरण के लिए, यहां दी गई चीज़ों को इवेंट के तौर पर दिखाया जाता है:
- बनाए जाने वाले बिल्ड टारगेट की सूची तय कर ली गई है
(
TargetParsingCompleteEvent
) - टॉप-लेवल के कॉन्फ़िगरेशन तय किए गए हैं
(
BuildConfigurationEvent
) - टारगेट बनाया गया, चाहे वह बन पाया हो या नहीं (
TargetCompleteEvent
) - टेस्ट चलाया गया (
TestAttempt
,TestSummary
)
इनमें से कुछ इवेंट, Bazel के बाहर Build Event Protocol में दिखाए जाते हैं. इन्हें BuildEvent
कहा जाता है. इससे न सिर्फ़ BlazeModule
, बल्कि Bazel प्रोसेस के बाहर की चीज़ों को भी बिल्ड का पता चलता है. इन्हें ऐक्सेस करने के लिए, प्रोटोकॉल मैसेज वाली फ़ाइल का इस्तेमाल किया जा सकता है. इसके अलावा, Bazel किसी सर्वर (जिसे Build Event Service कहा जाता है) से कनेक्ट करके इवेंट स्ट्रीम कर सकता है.
इसे build.lib.buildeventservice
और build.lib.buildeventstream
Java पैकेज में लागू किया जाता है.
बाहरी रिपॉज़िटरी
Bazel को मूल रूप से मोनोरिपो (एक ऐसा सोर्स ट्री जिसमें किसी भी चीज़ को बनाने के लिए ज़रूरी सभी चीज़ें शामिल होती हैं) में इस्तेमाल करने के लिए डिज़ाइन किया गया था. हालांकि, Bazel को ऐसे माहौल में इस्तेमाल किया जाता है जहां यह ज़रूरी नहीं है. "बाहरी रिपॉज़िटरी" एक ऐब्स्ट्रैक्शन है. इसका इस्तेमाल इन दोनों को जोड़ने के लिए किया जाता है: ये ऐसे कोड को दिखाती हैं जो बिल्ड के लिए ज़रूरी है, लेकिन मुख्य सोर्स ट्री में नहीं है.
WORKSPACE फ़ाइल
बाहरी रिपॉज़िटरी का सेट, WORKSPACE फ़ाइल को पार्स करके तय किया जाता है. उदाहरण के लिए, इस तरह का एलान:
local_repository(name="foo", path="/foo/bar")
@foo
नाम की रिपॉज़िटरी में नतीजे उपलब्ध हैं. यह इसलिए मुश्किल हो जाता है, क्योंकि Starlark फ़ाइलों में नए रिपॉज़िटरी नियम तय किए जा सकते हैं. इनका इस्तेमाल, नए Starlark कोड को लोड करने के लिए किया जा सकता है. इस कोड का इस्तेमाल, नए रिपॉज़िटरी नियम तय करने के लिए किया जा सकता है. यह सिलसिला ऐसे ही चलता रहता है…
इस मामले को हैंडल करने के लिए, WORKSPACE फ़ाइल (WorkspaceFileFunction
में) को load()
स्टेटमेंट के हिसाब से अलग-अलग हिस्सों में बांटा जाता है. चंक इंडेक्स को WorkspaceFileKey.getIndex()
से दिखाया जाता है. साथ ही, इंडेक्स X तक WorkspaceFileFunction
का हिसाब लगाने का मतलब है कि इसे Xवें load()
स्टेटमेंट तक कैलकुलेट किया जाता है.
डेटा स्टोर करने की जगह की जानकारी फ़ेच की जा रही है
रिपॉज़िटरी का कोड Bazel के लिए उपलब्ध होने से पहले, उसे फ़ेच करना ज़रूरी है. इससे Bazel, $OUTPUT_BASE/external/<repository name>
में एक डायरेक्ट्री बनाता है.
रिपॉज़िटरी को फ़ेच करने की प्रोसेस इन चरणों में पूरी होती है:
PackageLookupFunction
को पता चलता है कि उसे एक रिपॉज़िटरी की ज़रूरत है. इसलिए, वहSkyKey
के तौर परRepositoryName
बनाता है. इससेRepositoryLoaderFunction
शुरू हो जाता हैRepositoryLoaderFunction
, अनुरोध कोRepositoryDelegatorFunction
पर भेजता है. इसकी वजह साफ़ तौर पर नहीं बताई गई है. कोड के मुताबिक, ऐसा Skyframe को रीस्टार्ट करने पर, चीज़ों को दोबारा डाउनलोड करने से बचने के लिए किया जाता है. हालांकि, यह वजह बहुत ठोस नहीं हैRepositoryDelegatorFunction
WORKSPACE फ़ाइल के चंक को तब तक दोहराता है, जब तक उसे वह रिपॉज़िटरी नहीं मिल जाती जिसे फ़ेच करने के लिए कहा गया है- सही
RepositoryFunction
मिल गया है, जो रिपॉज़िटरी फ़ेच करने की सुविधा लागू करता है. यह रिपॉज़िटरी का Starlark वर्शन या Java में लागू की गई रिपॉज़िटरी के लिए हार्ड-कोड किया गया मैप है.
किसी रिपॉज़िटरी को फ़ेच करने में बहुत ज़्यादा समय लग सकता है. इसलिए, कैश मेमोरी की कई लेयर होती हैं:
- डाउनलोड की गई फ़ाइलों के लिए एक कैश मेमोरी होती है. इसे उनके चेकसम (
RepositoryCache
) के हिसाब से व्यवस्थित किया जाता है. इसके लिए, WORKSPACE फ़ाइल में चेकसम का उपलब्ध होना ज़रूरी है. हालांकि, यह हर्मेटिसिटी के लिए भी अच्छा है. इसे एक ही वर्कस्टेशन पर मौजूद Bazel के हर सर्वर इंस्टेंस के साथ शेयर किया जाता है. इससे कोई फ़र्क़ नहीं पड़ता कि वे किस वर्कस्पेस या आउटपुट बेस में चल रहे हैं. $OUTPUT_BASE/external
के तहत हर रिपॉज़िटरी के लिए एक "मार्कर फ़ाइल" लिखी जाती है. इसमें उस नियम का चेकसम होता है जिसका इस्तेमाल करके इसे फ़ेच किया गया था. अगर Bazel सर्वर रीस्टार्ट होता है, लेकिन चेकसम नहीं बदलता है, तो इसे फिर से फ़ेच नहीं किया जाता. इसेRepositoryDelegatorFunction.DigestWriter
में लागू किया गया है .--distdir
कमांड लाइन विकल्प, किसी दूसरी कैश मेमोरी को असाइन करता है. इसका इस्तेमाल, डाउनलोड किए जाने वाले आर्टफ़ैक्ट को ढूंढने के लिए किया जाता है. यह एंटरप्राइज़ सेटिंग में काम आता है, जहां Bazel को इंटरनेट से रैंडम चीज़ें फ़ेच नहीं करनी चाहिए. इसेDownloadManager
ने लागू किया है .
किसी रिपॉज़िटरी को डाउनलोड करने के बाद, उसमें मौजूद आर्टफ़ैक्ट को सोर्स आर्टफ़ैक्ट माना जाता है. इससे समस्या होती है, क्योंकि Bazel आम तौर पर सोर्स आर्टफ़ैक्ट के अप-टू-डेट होने की जांच करता है. इसके लिए, वह उन पर stat() को कॉल करता है. साथ ही, इन आर्टफ़ैक्ट को तब भी अमान्य कर दिया जाता है, जब वे जिस रिपॉज़िटरी में हैं उसकी परिभाषा बदल जाती है. इसलिए,
FileStateValue
s को बाहरी रिपॉज़िटरी में मौजूद किसी आर्टफ़ैक्ट के लिए, अपनी बाहरी रिपॉज़िटरी पर निर्भर रहना चाहिए. इसे ExternalFilesHelper
मैनेज करता है.
रिपॉज़िटरी मैपिंग
ऐसा हो सकता है कि कई रिपॉज़िटरी, एक ही रिपॉज़िटरी पर निर्भर रहना चाहें, लेकिन अलग-अलग वर्शन में. यह "डायमंड डिपेंडेंसी की समस्या" का उदाहरण है. उदाहरण के लिए, अगर बिल्ड में अलग-अलग रिपॉज़िटरी में मौजूद दो बाइनरी, Guava पर निर्भर रहना चाहती हैं, तो वे दोनों 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 खुद नहीं कर सकता या जब हमने इसे लागू किया था, तब 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
) जनरेट करती है, तो stdout में कोई बदलाव न हो.
छोटे मैसेज (गड़बड़ियां, चेतावनियां वगैरह) EventHandler
इंटरफ़ेस के ज़रिए दिखाए जाते हैं. खास तौर पर, ये EventBus
पर पोस्ट किए जाने वाले कॉन्टेंट से अलग होते हैं (यह भ्रमित करने वाला है). हर Event
में एक EventKind
(गड़बड़ी, चेतावनी, जानकारी, और कुछ अन्य) होता है. साथ ही, इनमें Location
(सोर्स कोड में वह जगह जिसकी वजह से इवेंट हुआ) भी हो सकता है.
EventHandler
लागू करने के कुछ तरीके, मिले हुए इवेंट को सेव करते हैं. इस कुकी का इस्तेमाल, यूज़र इंटरफ़ेस (यूआई) पर जानकारी को फिर से दिखाने के लिए किया जाता है. ऐसा अलग-अलग तरह की कैश मेमोरी में सेव की गई प्रोसेसिंग की वजह से होता है. उदाहरण के लिए, कैश मेमोरी में सेव किए गए कॉन्फ़िगर किए गए टारगेट से मिलने वाली चेतावनियां.
कुछ EventHandler
, इवेंट पोस्ट करने की सुविधा भी देते हैं. ये इवेंट, इवेंट बस में दिखते हैं. हालांकि, सामान्य Event
वहां _नहीं _दिखते. ये ExtendedEventHandler
के लागू किए गए वर्शन हैं. इनका मुख्य इस्तेमाल, कैश मेमोरी में सेव किए गए EventBus
इवेंट को फिर से चलाने के लिए किया जाता है. ये सभी EventBus
इवेंट, Postable
को लागू करते हैं. हालांकि, EventBus
पर पोस्ट की गई हर चीज़ के लिए, इस इंटरफ़ेस को लागू करना ज़रूरी नहीं है. सिर्फ़ वे इवेंट लागू करते हैं जिन्हें ExtendedEventHandler
ने कैश मेमोरी में सेव किया है. (यह अच्छा होगा और ज़्यादातर इवेंट ऐसा करते हैं. हालांकि, इसे लागू करना ज़रूरी नहीं है)
टर्मिनल आउटपुट ज़्यादातर UiEventHandler
के ज़रिए मिलता है. यह Bazel के सभी फ़ॉर्मैटिंग और प्रोग्रेस रिपोर्टिंग के लिए ज़िम्मेदार होता है. इसमें दो इनपुट होते हैं:
- इवेंट बस
- रिपोर्टर के ज़रिए, इसमें पाइप की गई इवेंट स्ट्रीम
कमांड को लागू करने वाली मशीनरी (उदाहरण के लिए, Bazel का बाकी हिस्सा) का क्लाइंट से आरपीसी स्ट्रीम के साथ सिर्फ़ 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
दिखाता है. इसके बंद होने का मतलब है कि टास्क पूरा हो गया है. इसका इस्तेमाल, try-with-resources स्टेटमेंट के साथ सबसे सही तरीके से किया जाता है.
हम MemoryProfiler
में मेमोरी की सामान्य प्रोफ़ाइलिंग भी करते हैं. यह हमेशा चालू रहता है. साथ ही, यह ज़्यादातर मामलों में हीप के ज़्यादा से ज़्यादा साइज़ और जीसी के व्यवहार को रिकॉर्ड करता है.
Bazel की टेस्टिंग
Bazel में दो मुख्य तरह के टेस्ट होते हैं: एक तरह के टेस्ट में Bazel को "ब्लैक बॉक्स" के तौर पर देखा जाता है और दूसरी तरह के टेस्ट में सिर्फ़ विश्लेषण फ़ेज़ को चलाया जाता है. हम पहले वाले को "इंटिग्रेशन टेस्ट" और दूसरे वाले को "यूनिट टेस्ट" कहते हैं. हालांकि, ये इंटिग्रेशन टेस्ट की तरह ज़्यादा होते हैं, लेकिन कम इंटिग्रेट किए जाते हैं. हमारे पास कुछ यूनिट टेस्ट भी हैं, जहां इनकी ज़रूरत होती है.
इंटिग्रेशन टेस्ट दो तरह के होते हैं:
- इन्हें
src/test/shell
के तहत, बैश टेस्ट फ़्रेमवर्क का इस्तेमाल करके लागू किया गया है - Java में लागू किए गए कुकी. इन्हें
BuildIntegrationTestCase
के सबक्लास के तौर पर लागू किया जाता है
BuildIntegrationTestCase
को इंटिग्रेशन की जांच करने के लिए सबसे अच्छा फ़्रेमवर्क माना जाता है, क्योंकि यह जांच के ज़्यादातर तरीकों के लिए सही है. यह एक Java फ़्रेमवर्क है. इसलिए, इसमें डीबग करने की सुविधा मिलती है. साथ ही, इसे डेवलपमेंट के लिए इस्तेमाल होने वाले कई सामान्य टूल के साथ आसानी से इंटिग्रेट किया जा सकता है. Bazel रिपॉज़िटरी में BuildIntegrationTestCase
क्लास के कई उदाहरण मौजूद हैं.
विश्लेषण से जुड़े टेस्ट, BuildViewTestCase
की सबक्लास के तौर पर लागू किए जाते हैं. इसमें एक स्क्रैच फ़ाइल सिस्टम होता है, जिसका इस्तेमाल करके BUILD
फ़ाइलें लिखी जा सकती हैं. इसके बाद, अलग-अलग हेल्पर मेथड, कॉन्फ़िगर किए गए टारगेट का अनुरोध कर सकते हैं, कॉन्फ़िगरेशन में बदलाव कर सकते हैं, और विश्लेषण के नतीजे के बारे में अलग-अलग चीज़ों की पुष्टि कर सकते हैं.