इस दस्तावेज़ में, कोड बेस के बारे में बताया गया है. साथ ही, यह भी बताया गया है कि Bazel का स्ट्रक्चर कैसा है. यह उन लोगों के लिए है जो Bazel में योगदान देना चाहते हैं, न कि आम उपयोगकर्ताओं के लिए.
परिचय
बेज़ेल का कोड बेस बड़ा है (~350KLOC प्रोडक्शन कोड और ~260 KLOC टेस्ट कोड) और कोई भी पूरे लैंडस्केप से वाकिफ़ नहीं है: सभी लोग अपनी खास घाटी को अच्छी तरह से जानते हैं, लेकिन कुछ लोगों को यह पता है कि हर दिशा में पहाड़ियों पर क्या मौजूद है.
इस दस्तावेज़ में कोड बेस के बारे में खास जानकारी दी गई है, ताकि यात्रा की शुरुआत आसानी से की जा सके. इससे, लोग अपने सफ़र के दौरान, जंगल के अंधेरे में अकेले रह जाएंगे और रास्ते में आसान रास्ता खो जाएगा.
Bazel के सोर्स कोड का सार्वजनिक वर्शन, GitHub पर github.com/bazelbuild/bazel पर मौजूद है. यह “सही सोर्स” नहीं है. इसे Google के अंदर मौजूद सोर्स ट्री से लिया गया है. इसमें ऐसी अतिरिक्त सुविधाएं शामिल हैं जो Google के बाहर काम की नहीं हैं. लंबे समय के लक्ष्य के तौर पर, GitHub को सटीक जानकारी का सोर्स बनाना है.
योगदानों को GitHub के सामान्य पुल रिक्वेस्ट मैकेनिज्म की मदद से स्वीकार किया जाता है. साथ ही, Googler उन्हें मैन्युअल तरीके से इंटरनल सोर्स ट्री में इंपोर्ट करता है. इसके बाद, उन्हें GitHub पर फिर से एक्सपोर्ट किया जाता है.
क्लाइंट/सर्वर आर्किटेक्चर
Bazel का ज़्यादातर हिस्सा, सर्वर प्रोसेस में मौजूद होता है. यह प्रोसेस, बिल्ड के बीच RAM में रहती है. इससे Bazel, बिल्ड के बीच स्टेटस बनाए रख पाता है.
यही वजह है कि बेज़ल कमांड लाइन में दो तरह के विकल्प होते हैं: स्टार्टअप और कमांड. इस तरह की कमांड लाइन में:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
कुछ विकल्प (--host_jvm_args=
), चलाए जाने वाले कमांड के नाम से पहले और कुछ उसके बाद (-c opt
) होते हैं. पहले तरह के विकल्प को "स्टार्टअप विकल्प" कहा जाता है और यह पूरी सर्वर प्रोसेस पर असर डालता है. वहीं, दूसरे तरह के विकल्प, यानी "कमांड विकल्प" सिर्फ़ एक कमांड पर असर डालते हैं.
हर सर्वर इंस्टेंस में एक सोर्स ट्री ("वर्कस्पेस") होता है. साथ ही, आम तौर पर हर वर्कस्पेस में एक ऐक्टिव सर्वर इंस्टेंस होता है. कस्टम आउटपुट बेस बताकर इसे गच्चा दिया जा सकता है (ज़्यादा जानकारी के लिए "डायरेक्ट्री लेआउट" सेक्शन देखें).
Bazel को एक ELF एक्ज़ीक्यूटेबल के तौर पर डिस्ट्रिब्यूट किया जाता है. यह एक मान्य .zip फ़ाइल भी है.
bazel
टाइप करने पर, C++ में लागू किए गए ऊपर दिए गए ELF executable ("क्लाइंट") को कंट्रोल मिल जाता है. यह इन चरणों का इस्तेमाल करके, सही सर्वर प्रोसेस सेट अप करता है:
- यह जांचता है कि क्या यह पहले से ही एक्सट्रैक्ट हो चुका है. अगर ऐसा नहीं है, तो यह ऐसा करता है. यहीं से सर्वर लागू करने की प्रोसेस शुरू होती है.
- यह जांच करता है कि कोई चालू सर्वर इंस्टेंस काम कर रहा है या नहीं: वह चल रहा है,
उसमें स्टार्टअप के सही विकल्प हैं, और वह सही वर्कस्पेस डायरेक्ट्री का इस्तेमाल करता है. यह
$OUTPUT_BASE/server
डायरेक्ट्री में जाकर, चल रहे सर्वर को ढूंढता है. इस डायरेक्ट्री में, उस पोर्ट की लॉक फ़ाइल होती है जिस पर सर्वर सुन रहा होता है. - ज़रूरत पड़ने पर, पुरानी सर्वर प्रोसेस को बंद कर देता है
- ज़रूरत पड़ने पर, नई सर्वर प्रोसेस शुरू करता है
सही सर्वर प्रोसेस तैयार होने के बाद, जिस निर्देश को चलाना है उसे gRPC इंटरफ़ेस के ज़रिए भेजा जाता है. इसके बाद, Bazel का आउटपुट टर्मिनल पर वापस भेजा जाता है. एक समय पर सिर्फ़ एक निर्देश चलाया जा सकता है. इसे लागू करने के लिए, C++ और Java में अलग-अलग हिस्सों के साथ, लॉक करने के बेहतर तरीके का इस्तेमाल किया जाता है. एक साथ कई कमांड चलाने के लिए, कुछ बुनियादी ढांचा मौजूद है, क्योंकि bazel version
को किसी दूसरे कमांड के साथ चलाने में कुछ परेशानी होती है. मुख्य समस्या, BlazeModule
s के लाइफ़ साइकल और BlazeRuntime
में कुछ स्टेटस है.
किसी निर्देश के आखिर में, Bazel सर्वर वह बाहर निकलने का कोड भेजता है जिसे क्लाइंट को दिखाना चाहिए. bazel run
को लागू करना एक दिलचस्प बात है: इस कमांड का काम, हाल ही में Bazel से बनाए गए कुछ कोड को चलाना है. हालांकि, यह सर्वर प्रोसेस से ऐसा नहीं कर सकता, क्योंकि इसमें टर्मिनल नहीं है. इसलिए, यह क्लाइंट को बताता है कि उसे कौनसी बाइनरी को ujexec() और किन आर्ग्युमेंट के साथ इस्तेमाल करना चाहिए.
जब कोई व्यक्ति Ctrl-C दबाता है, तो क्लाइंट इसे gRPC कनेक्शन पर Cancel कॉल में बदल देता है. यह कॉल, कमांड को जल्द से जल्द खत्म करने की कोशिश करता है. तीसरे 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 को निकाला जाता है. यह काम अपने-आप किया जाता है. साथ ही, ऐप्लिकेशन के हर वर्शन में इंस्टॉल बेस के तहत, चेकसम के आधार पर एक सबडायरेक्ट्री मिलती है. यह डिफ़ॉल्ट रूप से $OUTPUT_USER_ROOT/install
पर सेट होता है. इसे --install_base
कमांड लाइन विकल्प का इस्तेमाल करके बदला जा सकता है.
"आउटपुट बेस" वह जगह होती है जहां किसी खास वर्कस्पेस से जुड़ा Bazel इंस्टेंस, आउटपुट लिखता है. हर आउटपुट बेस में, किसी भी समय ज़्यादा से ज़्यादा एक Bazel सर्वर इंस्टेंस चल रहा होता है. आम तौर पर, यह $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
बजे होता है. इसे --output_base
स्टार्टअप विकल्प का इस्तेमाल करके बदला जा सकता है. यह विकल्प, कई कामों के लिए मददगार होता है. जैसे, किसी भी समय किसी भी वर्कस्पेस में सिर्फ़ एक Bazel इंस्टेंस चल सकता है.
आउटपुट डायरेक्ट्री में दूसरी चीज़ों के साथ-साथ ये चीज़ें भी शामिल होती हैं:
- डेटा स्टोर करने की बाहरी जगहों को
$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()
से पहले कॉल किया जा सकता है.)
Bazel, विकल्प क्लास के बारे में इन तरीकों से जानता है:
- कुछ Bazel में पहले से मौजूद हैं (
CommonCommandOptions
) - हर Basel कमांड पर @Command एनोटेशन से
ConfiguredRuleClassProvider
से (ये अलग-अलग प्रोग्रामिंग भाषाओं से जुड़े कमांड लाइन विकल्प हैं)- Starlark के नियम अपने विकल्प भी तय कर सकते हैं (यहां देखें)
हर विकल्प (Starlark से तय किए गए विकल्पों को छोड़कर), FragmentOptions
सबक्लास का एक सदस्य वैरिएबल होता है. इसमें @Option
एनोटेशन होता है, जो कुछ सहायता टेक्स्ट के साथ-साथ कमांड लाइन विकल्प का नाम और टाइप बताता है.
कमांड लाइन के विकल्प की वैल्यू का Java टाइप आम तौर पर आसान होता है (जैसे, कोई स्ट्रिंग, कोई पूर्णांक, कोई बूलियन, कोई लेबल वगैरह). हालांकि, हम ज़्यादा मुश्किल टाइप के विकल्पों के साथ भी काम करते हैं. इस मामले में, कमांड लाइन स्ट्रिंग को डेटा टाइप में बदलने का काम, com.google.devtools.common.options.Converter
को लागू करने पर होता है.
Bazel को दिखने वाला सोर्स ट्री
Baज़र, सॉफ़्टवेयर बनाने का कारोबार करता है. ऐसा सोर्स कोड को पढ़कर और उसे समझकर किया जाता है. बेज़ल, जिस सोर्स कोड पर काम करता है उसकी पूरी संख्या को "Workspace" कहा जाता है. इसे डेटा स्टोर करने की जगहों, पैकेज, और नियमों के हिसाब से बनाया गया है.
डेटा स्टोर करने की जगह
"रिपॉज़िटरी" एक सोर्स ट्री होता है, जिस पर डेवलपर काम करता है. आम तौर पर, यह एक प्रोजेक्ट को दिखाता है. Bazel का पूर्वज, Blaze, एक मोनोरेपो पर काम करता था. इसके उलट, Bazel उन प्रोजेक्ट के साथ काम करता है जिनका सोर्स कोड कई रिपॉज़िटरी में मौजूद होता है. जिस रिपॉज़िटरी से बेज़ल का इस्तेमाल शुरू किया गया है उसे “मुख्य डेटा स्टोर करने की जगह” और अन्य डेटा स्टोर करने की जगहों को “एक्सटर्नल डेटा स्टोर करने की जगहें” कहा जाता है.
रिपॉज़िटरी को उसकी रूट डायरेक्ट्री में WORKSPACE
(या WORKSPACE.bazel
) नाम की फ़ाइल से मार्क किया जाता है. इस फ़ाइल में, पूरे बिल्ड के लिए "ग्लोबल" जानकारी होती है. उदाहरण के लिए, उपलब्ध बाहरी रिपॉज़िटरी का सेट. यह एक सामान्य Starlark फ़ाइल की तरह काम करती है. इसका मतलब है कि किसी भी Starlark फ़ाइल को load()
किया जा सकता है.
इसका इस्तेमाल आम तौर पर, उन डेटा स्टोर करने की जगहों को शामिल करने के लिए किया जाता है जिनकी ज़रूरत, साफ़ तौर पर रेफ़र की गई डेटा स्टोर करने की जगह को होती है. हम इसे "deps.bzl
पैटर्न" कहते हैं
बाहरी रिपॉज़िटरी का कोड, $OUTPUT_BASE/external
में लिंक किया गया है या डाउनलोड किया गया है.
बिल्ड को चलाते समय, पूरे सोर्स ट्री को एक साथ जोड़ना ज़रूरी होता है. यह SymlinkForest की मदद से किया जाता है, जो मुख्य डेटा स्टोर करने की जगह में मौजूद हर पैकेज को $EXECROOT
और हर बाहरी रिपॉज़िटरी को या तो $EXECROOT/external
या $EXECROOT/..
से लिंक कर देता है. पहले यह पैकेज external
नाम के पैकेज को, डेटा स्टोर करने की जगह में नहीं रख सकता. इसलिए, हम इससे बाहर माइग्रेट कर रहे हैं
पैकेज
हर रिपॉज़िटरी में पैकेज, मिलती-जुलती फ़ाइलों का कलेक्शन, और डिपेंडेंसी की जानकारी होती है. इनकी जानकारी, BUILD
या BUILD.bazel
नाम की फ़ाइल में दी जाती है. अगर दोनों मौजूद हैं, तो Bazel BUILD.bazel
को प्राथमिकता देता है. BUILD
फ़ाइलों को अब भी स्वीकार करने की वजह यह है कि Bazel के पूर्वज Blaze ने इस फ़ाइल के नाम का इस्तेमाल किया था. हालांकि, यह आम तौर पर इस्तेमाल किया जाने वाला पाथ सेगमेंट है. खास तौर पर, Windows पर, जहां फ़ाइल के नाम केस-इन्सेंसिव होते हैं.
पैकेज एक-दूसरे से अलग होते हैं: किसी पैकेज की BUILD
फ़ाइल में बदलाव करने से, दूसरे पैकेज में बदलाव नहीं होता. BUILD
फ़ाइलों को जोड़ने या हटाने से, अन्य पैकेज बदल सकते हैं. ऐसा इसलिए होता है, क्योंकि बार-बार लागू होने वाले ग्लोब पैकेज की सीमाओं पर रुक जाते हैं. इसलिए, BUILD
फ़ाइल की मौजूदगी से बार-बार लागू होने की प्रोसेस रुक जाती है.
BUILD
फ़ाइल का आकलन करने की प्रोसेस को "पैकेज लोड करना" कहा जाता है. इसे PackageFactory
क्लास में लागू किया गया है. यह Starlark इंटरप्रेटर को कॉल करके काम करता है. साथ ही, इसके लिए उपलब्ध नियम क्लास के सेट के बारे में जानकारी ज़रूरी है. पैकेज लोड करने का नतीजा, एक Package
ऑब्जेक्ट होता है. यह ज़्यादातर किसी स्ट्रिंग (टारगेट का नाम) से टारगेट पर मैप होता है.
पैकेज लोड करने के दौरान, ग्लोबिंग की वजह से समस्याएं आती हैं: Bazel को हर सोर्स फ़ाइल को साफ़ तौर पर सूची में शामिल करने की ज़रूरत नहीं होती. इसके बजाय, यह ग्लोब (जैसे, glob(["**/*.java"])
) चला सकता है. शेल के विपरीत, यह बार-बार होने वाली ग्लोबिंग के साथ काम करता है, जो सब-डायरेक्ट्री में जाती है (लेकिन सब-पैकेज में नहीं). इसके लिए, फ़ाइल सिस्टम का ऐक्सेस ज़रूरी है. यह प्रोसेस धीमी हो सकती है. इसलिए, हम इसे एक साथ और बेहतर तरीके से चलाने के लिए, सभी तरह की तरकीबें अपनाते हैं.
ग्लोबिंग की सुविधा इन क्लास में लागू की गई है:
LegacyGlobber
, एक तेज़ और खुशी से Skyframe के बारे में अनजान globberSkyframeHybridGlobber
, यह एक ऐसा वर्शन है जो Skyframe का इस्तेमाल करता है और “Skyframe रीस्टार्ट” (इसके बारे में नीचे बताया गया है) से बचने के लिए, लेगसी globber पर वापस आ जाता है
Package
क्लास में कुछ ऐसे सदस्य होते हैं जिनका इस्तेमाल सिर्फ़ WORKSPACE फ़ाइल को पार्स करने के लिए किया जाता है. ये सदस्य, असल पैकेज के लिए काम के नहीं होते. यह डिज़ाइन में मौजूद एक गड़बड़ी है. इसकी वजह यह है कि रेगुलर पैकेज की जानकारी देने वाले ऑब्जेक्ट में, ऐसे फ़ील्ड नहीं होने चाहिए जिनमें किसी और चीज़ की जानकारी हो. इनमें शामिल हैं:
- रिपॉज़िटरी की मैपिंग
- रजिस्टर किए गए टूलचेन
- रजिस्टर किए गए एक्सीक्यूशन प्लैटफ़ॉर्म
आम तौर पर, WORKSPACE फ़ाइल को पार्स करने और सामान्य पैकेज को पार्स करने के बीच ज़्यादा अंतर होता है, ताकि Package
को दोनों की ज़रूरतों को पूरा करने की ज़रूरत न पड़े. दुर्भाग्य से, ऐसा करना मुश्किल है, क्योंकि दोनों एक-दूसरे से जुड़े हुए हैं.
लेबल, टारगेट, और नियम
पैकेज में टारगेट होते हैं. इनके टाइप इस तरह के होते हैं:
- फ़ाइलें: ऐसी चीज़ें जो बिल्ड का इनपुट या आउटपुट होती हैं. बैजल के पार्सल में, हम उन्हें आर्टफ़ैक्ट कहते हैं. इन आर्टफ़ैक्ट के बारे में कहीं और बताया गया है. बिल्ड के दौरान बनाई गई सभी फ़ाइलें टारगेट नहीं होती हैं. बेज़ल के आउटपुट में आम तौर पर कोई लेबल नहीं होना चाहिए.
- नियम: इनमें इनपुट से आउटपुट पाने का तरीका बताया गया है. आम तौर पर, ये किसी प्रोग्रामिंग भाषा (जैसे,
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=detailed
को चलाना. इससे ग्राफ़, हर लाइन में एक 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()
का इस्तेमाल करके या "Skyframe के पीछे" ad hoc स्टैटिक कैश रखकर, रीस्टार्ट के बीच स्टेटस सेव करना.
आम तौर पर, हमें इस तरह के तरीके अपनाने की ज़रूरत होती है, क्योंकि हमारे पास आम तौर पर, सैकड़ों हज़ार इन-फ़्लाइट Skyframe नोड होते हैं. साथ ही, Java में लाइटवेट थ्रेड काम नहीं करते.
स्टारलार्क
Starlark, डोमेन के हिसाब से बनाई गई भाषा है. इसका इस्तेमाल, लोग Bazel को कॉन्फ़िगर करने और उसे बेहतर बनाने के लिए करते हैं. इसे Python के सीमित सबसेट के तौर पर माना जाता है, जिसमें बहुत कम टाइप होते हैं. साथ ही, कंट्रोल फ़्लो पर ज़्यादा पाबंदियां होती हैं. सबसे अहम बात यह है कि एक साथ कई फ़ाइलें पढ़ने की सुविधा चालू करने के लिए, डेटा में बदलाव न होने की गारंटी दी जाती है. यह ट्यूरिंग-पूरी नहीं है, जिसकी वजह से कुछ (लेकिन सभी नहीं) उपयोगकर्ताओं को उसी भाषा में सामान्य प्रोग्रामिंग करने से मना किया जाता है.
Starlark को net.starlark.java
पैकेज में लागू किया गया है.
इसके अलावा, यहां Go में भी इसे लागू किया जा सकता है. फ़िलहाल, Bazel में इस्तेमाल किया जा रहा Java, एक इंटरप्रेटर है.
Starlark का इस्तेमाल कई कामों के लिए किया जाता है. जैसे:
BUILD
भाषा. यहां नए नियम तय किए जाते हैं. इस कॉन्टेक्स्ट में चलने वाले Starlark कोड के पास, सिर्फ़BUILD
फ़ाइल के कॉन्टेंट और इससे लोड की गई.bzl
फ़ाइलों का ऐक्सेस होता है.- नियम की परिभाषाएं. नए नियम (जैसे कि किसी नई भाषा के लिए सहायता) को इस तरह परिभाषित किया जाता है. इस कॉन्टेक्स्ट में चल रहे Starlark कोड के पास, अपनी डायरेक्ट डिपेंडेंसी से मिले कॉन्फ़िगरेशन और डेटा का ऐक्सेस होता है. इस बारे में ज़्यादा जानकारी बाद में दी जाएगी.
- WORKSPACE फ़ाइल. यहां बाहरी रिपॉज़िटरी (मुख्य सोर्स ट्री में मौजूद नहीं होने वाला कोड) तय किए जाते हैं.
- रिपॉज़िटरी के नियम की परिभाषाएं. यहां नए तरह के एक्सटर्नल रिपॉज़िटरी (डेटा स्टोर करने की जगह) को तय किया जाता है. इस कॉन्टेक्स्ट में चलने वाला Starlark कोड, उस मशीन पर कोई भी कोड चला सकता है जहां Bazel चल रहा है. साथ ही, यह कोड Workspace से बाहर भी पहुंच सकता है.
BUILD
और .bzl
फ़ाइलों के लिए उपलब्ध बोलियां थोड़ी अलग होती हैं, क्योंकि उनमें अलग-अलग चीज़ें ज़ाहिर होती हैं. अंतर की सूची यहां दी गई है.
Starlark के बारे में ज़्यादा जानकारी यहां उपलब्ध है.
लोडिंग/विश्लेषण का चरण
लोड करने/विश्लेषण करने के चरण में, Bazel यह तय करता है कि किसी खास नियम को बनाने के लिए कौनसी कार्रवाइयां ज़रूरी हैं. इसकी बुनियादी यूनिट, "कॉन्फ़िगर किया गया टारगेट" है, जो (टारगेट, कॉन्फ़िगरेशन) पेयर है.
इसे "लोडिंग/विश्लेषण का फ़ेज़" कहा जाता है, क्योंकि इसे दो अलग-अलग हिस्सों में बांटा जा सकता है. पहले इन हिस्सों को क्रम से चलाया जाता था, लेकिन अब ये एक-दूसरे के साथ ओवरलैप हो सकते हैं:
- पैकेज लोड करना, यानी
BUILD
फ़ाइलों को उनPackage
ऑब्जेक्ट में बदलना जो उन्हें दिखाते हैं - कॉन्फ़िगर किए गए टारगेट का विश्लेषण करना, यानी कि ऐक्शन ग्राफ़ बनाने के लिए नियमों को लागू करना
कमांड लाइन पर अनुरोध किए गए कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र में मौजूद हर कॉन्फ़िगर किए गए टारगेट का विश्लेषण, नीचे से ऊपर की ओर किया जाना चाहिए. इसका मतलब है कि सबसे पहले लीफ़ नोड और फिर कमांड लाइन पर मौजूद टारगेट का विश्लेषण किया जाना चाहिए. कॉन्फ़िगर किए गए किसी एक टारगेट के विश्लेषण के इनपुट ये हैं:
- कॉन्फ़िगरेशन. ("वह नियम कैसे बनाया जाए; उदाहरण के लिए, टारगेट प्लैटफ़ॉर्म के साथ-साथ कमांड-लाइन के विकल्पों जैसी चीज़ें भी, जिन्हें उपयोगकर्ता C++ कंपाइलर में भेजना चाहता है)
- डायरेक्ट डिपेंडेंसी. ट्रांज़िशन की जानकारी देने वाली उनकी कंपनियां, विश्लेषण किए जा रहे नियम के लिए उपलब्ध हैं. इन्हें इस तरह इसलिए कहा जाता है, क्योंकि ये कॉन्फ़िगर किए गए टारगेट के ट्रांज़िशन क्लोज़र में जानकारी का "रोल-अप" उपलब्ध कराते हैं. जैसे, क्लासपाथ पर मौजूद सभी .jar फ़ाइलें या C++ बाइनरी में लिंक की जाने वाली सभी .o फ़ाइलें)
- टारगेट. यह उस पैकेज को लोड करने का नतीजा है जिसमें टारगेट मौजूद है. नियमों के लिए, इसमें उनके एट्रिब्यूट शामिल होते हैं. आम तौर पर, यही बात मायने रखती है.
- कॉन्फ़िगर किए गए टारगेट को लागू करना. नियमों के हिसाब से, यह स्टारलार्क या जावा में हो सकता है. गैर-नियम कॉन्फ़िगर किए गए सभी टारगेट, 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
है, जो 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
, "इस नियम के तहत आने वाली फ़ाइलों के सेट" का धुंधला कॉन्सेप्ट. ये वे फ़ाइलें होती हैं जो तब बनाई जाती हैं, जब कॉन्फ़िगर किया गया टारगेट, कमांड लाइन पर या जेनरूल के सोर्स में होता है. - उनकी रनफ़ाइल, सामान्य और डेटा.
- उनके आउटपुट ग्रुप. ये "फ़ाइलों के अन्य सेट" हैं, जिन्हें नियम से बनाया जा सकता है. इन्हें BUILD में filegroup नियम के output_group एट्रिब्यूट का इस्तेमाल करके और Java में
OutputGroupInfo
प्रोवाइडर का इस्तेमाल करके ऐक्सेस किया जा सकता है.
रनफ़ाइलें
कुछ बाइनरी को चलने के लिए डेटा फ़ाइलों की ज़रूरत होती है. इसका एक उदाहरण, ऐसे टेस्ट हैं जिनमें इनपुट फ़ाइलों की ज़रूरत होती है. Bazel में इसे "रनफ़ाइल" के कॉन्सेप्ट से दिखाया जाता है. "रनफ़ाइल ट्री", किसी खास बाइनरी के लिए डेटा फ़ाइलों की डायरेक्ट्री ट्री होती है. इसे फ़ाइल सिस्टम में सिमलिंक ट्री के तौर पर बनाया जाता है. इसमें अलग-अलग सिमलिंक होते हैं, जो आउटपुट ट्री के सोर्स में मौजूद फ़ाइलों पर ले जाते हैं.
रनफ़ाइल के सेट को Runfiles
इंस्टेंस के तौर पर दिखाया जाता है. यह कॉन्सेप्ट के हिसाब से, रनफ़ाइल्स ट्री में मौजूद किसी फ़ाइल के पाथ से उस Artifact
इंस्टेंस तक का मैप होता है जो उसे दिखाता है. यह एक Map
से थोड़ा ज़्यादा मुश्किल है. ऐसा दो वजहों से है:
- ज़्यादातर मामलों में, किसी फ़ाइल का runfiles पाथ और execpath एक ही होता है. हम इसका इस्तेमाल, कुछ रैम बचाने के लिए करते हैं.
- रनफ़ाइल ट्री में, लेगसी टाइप की कई एंट्री होती हैं. इन्हें भी दिखाना ज़रूरी है.
रनफ़ाइलों को RunfilesProvider
का इस्तेमाल करके इकट्ठा किया जाता है: इस क्लास का एक इंस्टेंस, कॉन्फ़िगर किए गए टारगेट (जैसे, लाइब्रेरी) और उसके ट्रांज़िशन क्लोज़र की ज़रूरतों की रनफ़ाइलों को दिखाता है. साथ ही, इन्हें नेस्ट किए गए सेट की तरह इकट्ठा किया जाता है. असल में, इन्हें नेस्ट किए गए सेट का इस्तेमाल करके लागू किया जाता है: हर टारगेट, अपनी डिपेंडेंसी की रनफ़ाइलों को जोड़ता है और कुछ अपनी रनफ़ाइलें जोड़ता है. इसके बाद, वह नतीजे वाले सेट को डिपेंडेंसी ग्राफ़ में ऊपर की ओर भेजता है. किसी RunfilesProvider
इंस्टेंस में दो Runfiles
इंस्टेंस होते हैं. पहला, "डेटा" एट्रिब्यूट के ज़रिए नियम पर निर्भर होने पर और दूसरा, आने वाली हर तरह की अन्य डिपेंडेंसी के लिए. ऐसा इसलिए होता है, क्योंकि डेटा एट्रिब्यूट के ज़रिए किसी टारगेट पर निर्भर होने पर, कभी-कभी अलग-अलग रनफ़ाइलें दिखती हैं. यह एक गड़बड़ी है, जिसे हम अब तक ठीक नहीं कर पाए हैं.
बाइनरी के रनफ़ाइल को RunfilesSupport
के इंस्टेंस के तौर पर दिखाया जाता है. यह Runfiles
से अलग है, क्योंकि RunfilesSupport
को असल में बनाया जा सकता है. Runfiles
सिर्फ़ एक मैपिंग है. इसके लिए इन अतिरिक्त कॉम्पोनेंट की ज़रूरत होती है:
- इनपुट रनफ़ाइल मेनिफ़ेस्ट. यह, रनफ़ाइल ट्री का सिलसिलेवार ब्यौरा है. इसका इस्तेमाल, रनफ़ाइल्स ट्री के कॉन्टेंट के लिए प्रॉक्सी के तौर पर किया जाता है. साथ ही, Bazel यह मानता है कि रनफ़ाइल्स ट्री में सिर्फ़ तब बदलाव होता है, जब मेनिफ़ेस्ट के कॉन्टेंट में बदलाव होता है.
- आउटपुट के लिए, रनफ़ाइल मेनिफ़ेस्ट. इसका इस्तेमाल, रनटाइम लाइब्रेरी करती हैं, जो रनफ़ाइल ट्री को मैनेज करती हैं. खास तौर पर, Windows पर, जो कभी-कभी सिंबल लिंक के साथ काम नहीं करता.
- Runfiles मिडलमैन. रनफ़ाइल ट्री मौजूद होने के लिए, सिमलिंक ट्री और सिमलिंक जिस आर्टफ़ैक्ट पर ले जाते हैं उसे बनाना ज़रूरी है. डिपेंडेंसी एज की संख्या कम करने के लिए, इन सभी को दिखाने के लिए, रनफ़ाइल मिडलमैन का इस्तेमाल किया जा सकता है.
- उस बाइनरी को चलाने के लिए कमांड लाइन आर्ग्युमेंट जिसका इस्तेमाल करके,
RunfilesSupport
ऑब्जेक्ट का रनफ़ाइल बनाया जाता है.
पक्ष
ऐसेट, "डिपेंडेंसी ग्राफ़ में कैलकुलेशन को नीचे तक भेजने" का एक तरीका है. Bazel का इस्तेमाल करने वाले लोगों के लिए, इनके बारे में यहां बताया गया है. ऐसा करने का एक अच्छा उदाहरण प्रोटोकॉल बफ़र है: proto_library
नियम को किसी खास भाषा के बारे में नहीं पता होना चाहिए, लेकिन किसी भी प्रोग्रामिंग भाषा में प्रोटोकॉल बफ़र मैसेज (“प्रोटोकॉल बफ़र की “बेसिक यूनिट”) को लागू करने की प्रोसेस को proto_library
नियम के हिसाब से बनाया जाना चाहिए, ताकि अगर एक ही भाषा के दो टारगेट एक ही प्रोटोकॉल बफ़र पर निर्भर हों, तो इसे सिर्फ़ एक बार बनाया जाए.
कॉन्फ़िगर किए गए टारगेट की तरह ही, इन्हें Skyframe में SkyValue
के तौर पर दिखाया जाता है. साथ ही, इन्हें बनाने का तरीका भी कॉन्फ़िगर किए गए टारगेट बनाने के तरीके से काफ़ी मिलता-जुलता है: इनमें ConfiguredAspectFactory
नाम की फ़ैक्ट्री क्लास होती है, जिसके पास RuleContext
का ऐक्सेस होता है. हालांकि, कॉन्फ़िगर किए गए टारगेट फ़ैक्ट्री के उलट, यह उस कॉन्फ़िगर किए गए टारगेट और उसके प्रोवाइडर के बारे में भी जानती है जिससे यह जुड़ी होती है.
डिपेंडेंसी ग्राफ़ में नीचे बताए गए आसपेक्ट के सेट की जानकारी, Attribute.Builder.aspects()
फ़ंक्शन का इस्तेमाल करके हर एट्रिब्यूट के लिए दी जाती है. इस प्रोसेस में हिस्सा लेने वाली कुछ क्लास के नाम भ्रमित करने वाले हैं:
AspectClass
, इस एस्पेक्ट को लागू करने का तरीका है. यह Java (इस मामले में यह एक सबक्लास है) या Starlark (इस मामले में यहStarlarkAspectClass
का एक इंस्टेंस है) में हो सकता है. यहRuleConfiguredTargetFactory
के जैसा ही है.AspectDefinition
, एस्पेक्ट की परिभाषा है. इसमें, ज़रूरी सेवा देने वाली कंपनियां और सेवा देने वाली कंपनियां शामिल होती हैं. साथ ही, इसमें लागू करने का रेफ़रंस भी होता है, जैसे कि सहीAspectClass
इंस्टेंस. यहRuleClass
के जैसे ही है.AspectParameters
, किसी ऐसे पहलू को पैरामेटाइज़ करने का एक तरीका है जिसे डिपेंडेंसी ग्राफ़ में नीचे दिखाया जाता है. फ़िलहाल, यह मैप को स्ट्रिंग करने के लिए स्ट्रिंग है. प्रोटोकॉल बफ़र के काम के होने का एक अच्छा उदाहरण: अगर किसी भाषा में एक से ज़्यादा एपीआई हैं, तो यह जानकारी कि प्रोटोकॉल बफ़र किस एपीआई के लिए बनाए जाने चाहिए, उसे डिपेंडेंसी ग्राफ़ में भेजा जाना चाहिए.Aspect
उस डेटा को दिखाता है जो डिपेंडेंसी ग्राफ़ में नीचे की ओर भेजे जाने वाले किसी पहलू का हिसाब लगाने के लिए ज़रूरी है. इसमें आसपेक्ट क्लास, उसकी परिभाषा, और उसके पैरामीटर शामिल होते हैं.RuleAspect
एक ऐसा फ़ंक्शन है जो यह तय करता है कि किसी खास नियम के किन पहलुओं को प्रॉपगेट करना चाहिए. यहRule
->Aspect
फ़ंक्शन है.
एक समस्या यह है कि एस्पेक्ट, दूसरे एस्पेक्ट से जुड़े हो सकते हैं. उदाहरण के लिए, किसी Java IDE के क्लासपाथ को इकट्ठा करने वाले एस्पेक्ट को क्लासपाथ पर मौजूद सभी .jar फ़ाइलों के बारे में जानना होगा. हालांकि, उनमें से कुछ प्रोटोकॉल बफ़र हैं. इस मामले में, IDE आसपेक्ट रेशियो को
(proto_library
नियम + Java प्रोटो आसपेक्ट रेशियो) पेयर के साथ अटैच करना होगा.
अलग-अलग पहलुओं की जटिलता को क्लास
AspectCollection
में कैप्चर किया जाता है.
प्लैटफ़ॉर्म और टूलचेन
Baज़र, मल्टी-प्लैटफ़ॉर्म बिल्ड के साथ काम करता है. इसका मतलब है कि ऐसी बिल्ड प्रोसेस में एक से ज़्यादा आर्किटेक्चर मौजूद हो सकते हैं जहां बिल्ड ऐक्शन चल सकते हैं. साथ ही, जिस कोड के लिए बनाया गया है उसके लिए अलग-अलग आर्किटेक्चर इस्तेमाल किए जा सकते हैं. Bazel में इन आर्किटेक्चर को प्लैटफ़ॉर्म कहा जाता है. इनके बारे में पूरी जानकारी यहां दी गई है
किसी प्लैटफ़ॉर्म के बारे में, सीमा सेटिंग (जैसे, "सीपीयू आर्किटेक्चर" का कॉन्सेप्ट) से सीमा की वैल्यू (जैसे, x86_64 जैसा कोई सीपीयू) तक की की-वैल्यू मैपिंग से बताया जाता है. हमारे पास @platforms
रिपॉज़िटरी में सबसे ज़्यादा इस्तेमाल की जाने वाली कंस्ट्रेंट सेटिंग और वैल्यू का एक "शब्दकोश" है.
टूलचेन का कॉन्सेप्ट इस बात पर आधारित है कि कौनसे प्लैटफ़ॉर्म पर बिल्ड चल रहा है और कौनसे प्लैटफ़ॉर्म टारगेट किए जा रहे हैं. इसके आधार पर, आपको अलग-अलग कंपाइलर का इस्तेमाल करना पड़ सकता है. उदाहरण के लिए, कोई खास C++ टूलचेन किसी खास ओएस पर चल सकता है और कुछ अन्य ओएस को टारगेट कर सकता है. Bazel को यह तय करना होगा कि सेट किए गए एक्सीक्यूशन और टारगेट प्लैटफ़ॉर्म के आधार पर, C++ के किस कंपाइलर का इस्तेमाल किया जाए. टूलचेन के दस्तावेज़ यहां देखे जा सकते हैं.
ऐसा करने के लिए, टूलचेन को एक्ज़ीक्यूशन के सेट और उनके साथ काम करने वाले टारगेट प्लैटफ़ॉर्म की कंस्ट्रेंट के साथ एनोटेट किया जाता है. ऐसा करने के लिए, टूलचेन की परिभाषा दो हिस्सों में बंटी है:
toolchain()
नियम, जो किसी टूलचेन के साथ काम करने वाले एक्ज़ीक्यूशन और टारगेट की सीमाओं के सेट के बारे में बताता है. साथ ही, यह भी बताता है कि यह किस तरह का टूलचेन है, जैसे कि C++ या Java. टूलचेन के टाइप के बारे मेंtoolchain_type()
नियम से पता चलता है- भाषा के हिसाब से बना नियम, जिसमें असल टूलचेन के बारे में बताया गया हो (जैसे कि
cc_toolchain()
)
ऐसा इसलिए किया जाता है, क्योंकि टूलचेन रिज़ॉल्यूशन करने के लिए, हमें हर टूलचेन की सीमाओं के बारे में पता होना चाहिए. साथ ही, भाषा के हिसाब से *_toolchain()
नियमों में इससे ज़्यादा जानकारी होती है, इसलिए उन्हें लोड होने में ज़्यादा समय लगता है.
एक्सीक्यूशन प्लैटफ़ॉर्म को इनमें से किसी एक तरीके से तय किया जाता है:
register_execution_platforms()
फ़ंक्शन का इस्तेमाल करके, WORKSPACE फ़ाइल में- कमांड लाइन पर, --extra_execution_platforms कमांड लाइन के विकल्प का इस्तेमाल करके
उपलब्ध एक्सीक्यूशन प्लैटफ़ॉर्म का सेट, RegisteredExecutionPlatformsFunction
में कैलकुलेट किया जाता है.
कॉन्फ़िगर किए गए टारगेट के लिए टारगेट प्लैटफ़ॉर्म,
PlatformOptions.computeTargetPlatform()
से तय होता है . यह प्लैटफ़ॉर्म की सूची है, क्योंकि हम एक से ज़्यादा टारगेट प्लैटफ़ॉर्म के साथ काम करना चाहते हैं. हालांकि, फ़िलहाल इसे लागू नहीं किया गया है.
कॉन्फ़िगर किए गए टारगेट के लिए इस्तेमाल किए जाने वाले टूलचेन का सेट, ToolchainResolutionFunction
से तय होता है. यह इनका फ़ंक्शन है:
- रजिस्टर किए गए टूलचेन का सेट (WORKSPACE फ़ाइल और कॉन्फ़िगरेशन में)
- कॉन्फ़िगरेशन में, पसंद के मुताबिक लागू करने और टारगेट करने के लिए प्लैटफ़ॉर्म
- कॉन्फ़िगर किए गए टारगेट के लिए ज़रूरी टूलचैन टाइप का सेट (
UnloadedToolchainContextKey)
में UnloadedToolchainContextKey
में, कॉन्फ़िगर किए गए टारगेट (exec_compatible_with
एट्रिब्यूट) और कॉन्फ़िगरेशन (--experimental_add_exec_constraints_to_targets
) के लिए, प्लैटफ़ॉर्म पर लागू होने वाली पाबंदियों का सेट
इसका नतीजा UnloadedToolchainContext
होता है. यह असल में, टूलचेन टाइप (ToolchainTypeInfo
इंस्टेंस के तौर पर दिखाया गया) से चुने गए टूलचेन के लेबल तक का मैप होता है. इसे "अनलोड किया गया" इसलिए कहा जाता है, क्योंकि इसमें टूलचेन नहीं होते, सिर्फ़ उनके लेबल होते हैं.
इसके बाद, टूलचेन ResolvedToolchainContext.load()
का इस्तेमाल करके लोड किए जाते हैं और कॉन्फ़िगर किए गए उस टारगेट के लागू होने पर इस्तेमाल किए जाते हैं जिसने उनका अनुरोध किया था.
हमारे पास एक लेगसी सिस्टम भी है, जो एक ही "होस्ट" कॉन्फ़िगरेशन पर निर्भर करता है. साथ ही, टारगेट कॉन्फ़िगरेशन को अलग-अलग कॉन्फ़िगरेशन फ़्लैग, जैसे कि --cpu
से दिखाया जाता है. हम धीरे-धीरे ऊपर बताए गए सिस्टम पर ट्रांज़िशन कर रहे हैं. ऐसे मामलों को हैंडल करने के लिए जहां लोग लेगसी कॉन्फ़िगरेशन वैल्यू पर भरोसा करते हैं, हमने प्लैटफ़ॉर्म मैपिंग लागू की है, ताकि लेगसी फ़्लैग और नए स्टाइल के प्लैटफ़ॉर्म की पाबंदियों के बीच अनुवाद किया जा सके.
उनका कोड PlatformMappingFunction
में है और इसमें 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 जैसे बड़े डेवलपर के साथ बड़े कोडबेस पर काम करना है, तो आपको यह ध्यान रखना होगा कि कोई भी व्यक्ति आपके कोड पर अपनी मर्ज़ी से काम न कर पाए. ऐसा न करने पर, हाइरम के नियम के मुताबिक, लोग उन व्यवहारों पर भरोसा करेंगे जिन्हें आपने लागू करने की जानकारी माना था.
बेज़ल इसके लिए पारदर्शिता नाम के तरीके का इस्तेमाल करता है: आपके पास यह एलान करने का विकल्प है कि किसी खास टारगेट पर, सिर्फ़ पारदर्शिता एट्रिब्यूट का इस्तेमाल किया जा सकता है. यह एट्रिब्यूट थोड़ा खास है, क्योंकि इसमें लेबल की सूची होती है. हालांकि, ये लेबल किसी खास टारगेट के पॉइंटर के बजाय, पैकेज के नामों पर पैटर्न को कोड में बदल सकते हैं. (हां, यह डिज़ाइन की एक गलती है.)
इसे इन जगहों पर लागू किया गया है:
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
के इंस्टेंस के तौर पर दिखाया जाता है. इन्हें दो हिस्सों में बांटा गया है. साथ ही, इनमें निर्देश और असाइकलिक ग्राफ़ होते हैं. इन्हें "ऐक्शन ग्राफ़" कहा जाता है.
आर्टफ़ैक्ट दो तरह के होते हैं: सोर्स आर्टफ़ैक्ट (एक ऐसा आर्टफ़ैक्ट जो बेज़ल का इस्तेमाल शुरू करने से पहले उपलब्ध होता है) और डिराइव्ड आर्टफ़ैक्ट (जिन्हें बनाया जाना चाहिए). डेरिव्ड आर्टफ़ैक्ट कई तरह के हो सकते हैं:
- **सामान्य आर्टफ़ैक्ट. **इनके चेकसम का हिसाब लगाकर, इन्हें अप-टू-डेट रखने की जांच की जाती है. इसके लिए, शॉर्टकट के तौर पर mtime का इस्तेमाल किया जाता है. अगर फ़ाइल का समय नहीं बदलता है, तो हम फ़ाइल को चेकसम नहीं करते.
- समाधान नहीं किए गए सिंबललिंक आर्टफ़ैक्ट. readlink() को कॉल करके, इनके अप-टू-डेट होने की जांच की जाती है. सामान्य आर्टफ़ैक्ट के मुकाबले, ये डैंगलिंग स्लिंक्स हो सकते हैं. आम तौर पर, इसका इस्तेमाल उन मामलों में किया जाता है जहां कुछ फ़ाइलों को किसी तरह के संग्रह में पैक किया जाता है.
- पेड़ों से जुड़े आर्टफ़ैक्ट. ये एक फ़ाइल नहीं, बल्कि डायरेक्ट्री ट्री हैं. इनकी जांच करके यह पता लगाया जाता है कि वे अप-टू-डेट हैं या नहीं. इसके लिए, इनमें मौजूद फ़ाइलों और उनके कॉन्टेंट की जांच की जाती है. इन्हें
TreeArtifact
के तौर पर दिखाया जाता है. - मेटाडेटा के लगातार आर्टफ़ैक्ट. इन आर्टफ़ैक्ट में किए जाने वाले बदलावों की वजह से, दोबारा बनने की प्रोसेस ट्रिगर नहीं होती. इसका इस्तेमाल सिर्फ़ बिल्ड स्टैंप की जानकारी के लिए किया जाता है: हम सिर्फ़ इसलिए फिर से बिल्ड नहीं करना चाहते, क्योंकि मौजूदा समय बदल गया है.
सोर्स आर्टफ़ैक्ट, ट्री आर्टफ़ैक्ट या सिमलिंक आर्टफ़ैक्ट न होने की कोई बुनियादी वजह नहीं है. हालांकि, यह सिर्फ़ इसलिए है कि हमने इसे अभी तक लागू नहीं किया है (हालांकि, BUILD
फ़ाइल में सोर्स डायरेक्ट्री का रेफ़रंस देना, बेज़ेल में लंबे समय से चली आ रही गलतता की समस्याओं में से एक है. हमने इसे लागू करने के लिए एक तरीका अपनाया है जो BAZEL_TRACK_SOURCE_DIRECTORIES=1
जेवीएम प्रॉपर्टी से चालू होती है)
Artifact
के एक खास तरह के उदाहरण हैं, मध्यस्थ. इन्हें Artifact
इंस्टेंस से दिखाया जाता है, जो MiddlemanAction
के आउटपुट होते हैं. इनका इस्तेमाल, कुछ चीज़ों के लिए खास तौर पर किया जाता है:
- आर्टफ़ैक्ट को एक साथ ग्रुप करने के लिए, एग्रीगेट करने वाले मिडलमैन का इस्तेमाल किया जाता है. ऐसा इसलिए किया जाता है, ताकि अगर कई कार्रवाइयां इनपुट के एक ही बड़े सेट का इस्तेमाल करती हैं, तो हमारे पास N*M डिपेंडेंसी एज न हों, सिर्फ़ N+M (इन्हें नेस्ट किए गए सेट से बदला जा रहा है)
- डिपेंडेंसी मिडलमैन को शेड्यूल करने से यह पक्का होता है कि एक कार्रवाई, दूसरी कार्रवाई से पहले पूरी हो.
इनका इस्तेमाल ज़्यादातर लिंटिंग के लिए किया जाता है. हालांकि, C++ कंपाइलेशन के लिए भी इनका इस्तेमाल किया जाता है. ज़्यादा जानकारी के लिए
CcCompilationContext.createMiddleman()
देखें - Runfiles मिडलमैन का इस्तेमाल, Runfiles ट्री की मौजूदगी की पुष्टि करने के लिए किया जाता है, ताकि किसी को आउटपुट मेनिफ़ेस्ट और Runfiles ट्री के रेफ़रंस वाले हर आर्टफ़ैक्ट पर अलग से निर्भर न होना पड़े.
कार्रवाइयों को एक निर्देश के रूप में सबसे अच्छी तरह समझा जा सकता है, जिसे चलाने की ज़रूरत होती है, वह वातावरण जिसके लिए इसकी ज़रूरत होती है, और उससे मिलने वाले आउटपुट का सेट. किसी कार्रवाई की जानकारी के मुख्य कॉम्पोनेंट, यहां दी गई हैं:
- वह कमांड लाइन जिसे चलाना है
- इसके लिए ज़रूरी इनपुट आर्टफ़ैक्ट
- ऐसे एनवायरमेंट वैरिएबल जिन्हें सेट करना ज़रूरी है
- ऐसी व्याख्या जो आस-पास के माहौल (जैसे कि प्लैटफ़ॉर्म) के बारे में बताती है, जहां इसे चलाना ज़रूरी है \
कुछ और खास मामले भी हैं, जैसे कि ऐसी फ़ाइल लिखना जिसका कॉन्टेंट, Bazel को पता हो. ये AbstractAction
के सबक्लास हैं. ज़्यादातर कार्रवाइयां SpawnAction
या StarlarkAction
होती हैं (यही भी होती है कि ये अलग-अलग क्लास नहीं होनी चाहिए). हालांकि, Java और C++ के ऐक्शन टाइप (JavaCompileAction
, CppCompileAction
, और CppLinkAction
) हैं.
हम आखिर में सभी चीज़ों को SpawnAction
पर ले जाना चाहते हैं; JavaCompileAction
काफ़ी करीब है, लेकिन .d फ़ाइल को पार्स करने और शामिल करने की स्कैनिंग की वजह से, C++ थोड़ा खास मामला है.
ऐक्शन ग्राफ़ को ज़्यादातर Skyframe ग्राफ़ में "एम्बेड" किया जाता है: कॉन्सेप्ट के हिसाब से, किसी ऐक्शन को लागू करने को ActionExecutionFunction
को कॉल करने के तौर पर दिखाया जाता है. ऐक्शन ग्राफ़ डिपेंडेंसी एज से Skyframe डिपेंडेंसी एज की मैपिंग के बारे में ActionExecutionFunction.getInputDeps()
और Artifact.key()
में बताया गया है. इसमें Skyframe एज की संख्या कम रखने के लिए, कुछ ऑप्टिमाइज़ेशन किए गए हैं:
- डेरिव्ड आर्टफ़ैक्ट के पास अपने
SkyValue
नहीं होते. इसके बजाय,Artifact.getGeneratingActionKey()
का इस्तेमाल उस कार्रवाई की कुंजी का पता लगाने के लिए किया जाता है जो इसे जनरेट करती है - नेस्ट किए गए सेट की अपनी Skyframe कुंजी होती है.
शेयर की गई कार्रवाइयां
कुछ कार्रवाइयां, कॉन्फ़िगर किए गए कई टारगेट से जनरेट होती हैं. Starlark नियमों के दायरे में ज़्यादा कार्रवाइयां नहीं आती हैं, क्योंकि उन्हें सिर्फ़ अपने कॉन्फ़िगरेशन और पैकेज के हिसाब से तय की गई डायरेक्ट्री में, डेरिव्ड ऐक्शन डालने की अनुमति होती है. हालांकि, एक ही पैकेज में मौजूद नियमों में अंतर हो सकता है. वहीं, Java में लागू किए गए नियमों से, डेरिव्ड आर्टफ़ैक्ट को कहीं भी डाला जा सकता है.
इसे गड़बड़ी माना जाता है, लेकिन इसे हटाना काफ़ी मुश्किल है. ऐसा इसलिए, क्योंकि इससे प्रोसेस करने में लगने वाले समय में काफ़ी बचत होती है. उदाहरण के लिए, जब किसी सोर्स फ़ाइल को किसी तरह से प्रोसेस करना होता है और उस फ़ाइल का रेफ़रंस कई नियमों (हैंडवाइब-हैंडवाइब) से मिलता है. हालांकि, इसके लिए कुछ रैम की ज़रूरत होती है: शेयर की गई कार्रवाई के हर उदाहरण को मेमोरी में अलग से सेव करना पड़ता है.
अगर दो कार्रवाइयां एक ही आउटपुट फ़ाइल जनरेट करती हैं, तो वे एक जैसी होनी चाहिए:
उनमें एक जैसे इनपुट, एक जैसे आउटपुट होने चाहिए और एक ही कमांड लाइन को चलाना चाहिए. यह समानता संबंध Actions.canBeShared()
में लागू किया जाता है और हर कार्रवाई को देखकर, विश्लेषण और निष्पादन के चरणों के बीच इसकी पुष्टि की जाती है.
इसे SkyframeActionExecutor.findAndStoreArtifactConflicts()
में लागू किया गया है और यह Bazel की उन कुछ जगहों में से एक है जहां बिल्ड के "ग्लोबल" व्यू की ज़रूरत होती है.
लागू करने का चरण
इसके बाद, Bazel असल में बिल्ड ऐक्शन चलाना शुरू करता है. जैसे, आउटपुट देने वाले कमांड.
विश्लेषण के बाद, Bazel सबसे पहले यह तय करता है कि कौनसे आर्टफ़ैक्ट बनाने हैं. इसके लिए लॉजिक, TopLevelArtifactHelper
में कोड में बदला गया है. यह कमांड लाइन पर कॉन्फ़िगर किए गए टारगेट का filesToBuild
और खास आउटपुट ग्रुप का कॉन्टेंट है. इसका मकसद, "अगर यह टारगेट कमांड लाइन पर है, तो इन आर्टफ़ैक्ट को बनाएं" को साफ़ तौर पर बताना है.
अगला चरण एक्ज़ीक्यूशन रूट बनाना है. Bazel के पास फ़ाइल सिस्टम (--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 को यह बताया जा सके कि उसके सभी इनपुट ज़रूरी नहीं थे. अगर इस्तेमाल किए गए इनपुट को इस्तेमाल नहीं किए गए के तौर पर रिपोर्ट किया जाता है, तो इससे गलत इंक्रीमेंटल बिल्ड बन सकते हैं.
जब ऐक्शन कैश मेमोरी, कार्रवाई वाले नए इंस्टेंस पर हिट दिखाती है (जैसे कि सर्वर के रीस्टार्ट होने के बाद बनाया गया), तो Basel updateInputs()
को खुद कॉल करता है. इससे इनपुट के सेट में, इनपुट के खोजे जाने और पहले से की गई काट-छांट से मिलने वाला नतीजा दिखता है.
Starlark कार्रवाइयां सुविधा का इस्तेमाल करके, कुछ इनपुट को 'इस्तेमाल नहीं किया गया' के तौर पर मार्क किया जा सकता है. इसके लिए, ctx.actions.run()
के unused_inputs_list=
आर्ग्युमेंट का इस्तेमाल किया जा सकता है.
कार्रवाइयां करने के अलग-अलग तरीके: रणनीतियां/ActionContexts
कुछ कार्रवाइयां अलग-अलग तरीकों से चलाई जा सकती हैं. उदाहरण के लिए, किसी कमांड लाइन को स्थानीय तौर पर, स्थानीय तौर पर अलग-अलग तरह के सैंडबॉक्स में या फिर किसी दूसरी जगह से चलाया जा सकता है. इस कॉन्सेप्ट को ActionContext
(या Strategy
, क्योंकि हमने नाम बदलने की प्रोसेस को सिर्फ़ आधा पूरा किया है...) कहा जाता है
किसी ऐक्शन कॉन्टेक्स्ट का लाइफ़ साइकल इस तरह होता है:
- जब कार्रवाइयां शुरू की जाती हैं, तो
BlazeModule
इंस्टेंस से पूछा जाता है कि उनके पास कौनसे ऐक्शन कॉन्टेक्स्ट हैं. यहExecutionTool
के कन्स्ट्रक्टर में होता है. ऐक्शन कॉन्टेक्स्ट टाइप की पहचान, JavaClass
के किसी ऐसे इंस्टेंस से की जाती है जोActionContext
के किसी सब-इंटरफ़ेस को रेफ़र करता है. साथ ही, यह भी तय करता है कि ऐक्शन कॉन्टेक्स्ट को किस इंटरफ़ेस को लागू करना चाहिए. - कार्रवाई के सही संदर्भ को उपलब्ध विकल्पों में से चुना जाता है. इसके बाद, उसे
ActionExecutionContext
औरBlazeExecutor
पर भेज दिया जाता है. - कार्रवाइयां,
ActionExecutionContext.getContext()
औरBlazeExecutor.getStrategy()
का इस्तेमाल करके कॉन्टेक्स्ट का अनुरोध करती हैं (इसके लिए, सिर्फ़ एक तरीका होना चाहिए…)
रणनीतियां, अपनी भूमिका निभाने के लिए दूसरी रणनीतियों को कॉल कर सकती हैं. उदाहरण के लिए, डाइनैमिक रणनीति में, स्थानीय और रिमोट, दोनों तरह से कार्रवाइयां शुरू की जाती हैं. इसके बाद, पहले पूरी होने वाली कार्रवाइयों का इस्तेमाल किया जाता है.
एक ध्यान देने लायक रणनीति वह है जो स्थायी वर्कर प्रोसेस (WorkerSpawnStrategy
) को लागू करती है. कुछ टूल में स्टार्टअप का समय ज़्यादा होता है. इसलिए, इन टूल को हर कार्रवाई के लिए एक बार में शुरू करने के बजाय, एक ऐक्शन के बीच फिर से इस्तेमाल किया जाना चाहिए (यह सही समस्या के बारे में बताता है, क्योंकि बेज़ल वर्कर प्रोसेस के वादे पर निर्भर करता है कि वह अलग-अलग अनुरोधों के बीच मॉनिटर की जा सकने वाली स्थिति नहीं लागू करता)
टूल बदलने पर, वर्कर्स प्रोसेस को फिर से शुरू करना होगा. किसी वर्कर का फिर से इस्तेमाल किया जा सकता है या नहीं, यह 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 की सुविधा होती है. इससे पता चलता है कि टेस्ट को कैसे चलाया जाना चाहिए:
- ऐसे आर्टफ़ैक्ट जिनकी बिल्डिंग के नतीजे में टेस्ट चल रहा है. यह एक "कैश मेमोरी का स्टेटस" फ़ाइल है, जिसमें
TestResultData
मैसेज को सीरियलाइज़ किया गया है - टेस्ट को कितनी बार चलाना चाहिए
- टेस्ट को कितने हिस्सों में बांटना है
- टेस्ट को कैसे चलाया जाना चाहिए, इस बारे में कुछ पैरामीटर (जैसे, टेस्ट टाइम आउट)
यह तय करना कि कौनसे टेस्ट चलाने हैं
यह तय करना कि कौनसे टेस्ट चलाए जाएं, एक जटिल प्रोसेस है.
सबसे पहले, टारगेट पैटर्न को पार्स करने के दौरान, टेस्ट सुइट को बार-बार बड़ा किया जाता है. यह एक्सपैंशन, TestsForTargetPatternFunction
में लागू किया गया है. एक तरह से यह आश्चर्य की बात है कि अगर किसी टेस्ट सुइट में कोई टेस्ट नहीं है, तो इसका मतलब है कि उसके पैकेज में मौजूद हर टेस्ट के लिए ऐसा है. इसे Package.beforeBuild()
में लागू किया गया है. इसके लिए, टेस्ट सुइट के नियमों में $implicit_tests
नाम का एक इंप्लिसिट एट्रिब्यूट जोड़ा गया है.
इसके बाद, कमांड लाइन के विकल्पों के हिसाब से साइज़, टैग, टाइम आउट, और भाषा के हिसाब से जांच को फ़िल्टर किया जाता है. इसे TestFilter
में लागू किया जाता है और टारगेट पार्स करने के दौरान, TargetPatternPhaseFunction.determineTests()
से इसे कॉल किया जाता है. साथ ही, नतीजे को TargetPatternPhaseValue.getTestsToRunLabels()
में डाला जाता है. फ़िल्टर किए जा सकने वाले नियम एट्रिब्यूट को कॉन्फ़िगर नहीं किया जा सकता, क्योंकि यह विश्लेषण के चरण से पहले होता है. इसलिए, कॉन्फ़िगरेशन उपलब्ध नहीं होता.
इसके बाद, इसे BuildView.createResult()
में और प्रोसेस किया जाता है: जिन टारगेट का विश्लेषण नहीं हो पाया उन्हें फ़िल्टर कर दिया जाता है और टेस्ट को एक्सक्लूज़िव और नॉन-एक्सक्लूज़िव टेस्ट में बांट दिया जाता है. इसके बाद, इसे AnalysisResult
में डाल दिया जाता है. इससे ExecutionTool
को पता चलता है कि कौनसे टेस्ट चलाने हैं.
इस पूरी प्रोसेस को ज़्यादा पारदर्शी बनाने के लिए, tests()
क्वेरी ऑपरेटर (TestsFunction
में लागू किया गया) उपलब्ध है. इससे यह पता चलता है कि कमांड लाइन पर किसी खास टारगेट के बताए जाने पर कौनसे टेस्ट चलाए जाते हैं. यह अफ़सोस की बात है कि
इसे फिर से लागू किया गया है. इसलिए, हो सकता है कि यह ऊपर दी गई बातों से अलग-अलग तरीकों से अलग हो.
टेस्ट चलाना
कैश मेमोरी की स्थिति के आर्टफ़ैक्ट का अनुरोध करके, टेस्ट चलाए जाते हैं. इसके बाद,
TestRunnerAction
लागू होता है, जो आखिर में --test_strategy
कमांड लाइन विकल्प की मदद से चुने गए TestActionContext
को कॉल करता है. यह विकल्प,
अनुरोध किए गए तरीके से जांच करता है.
टेस्ट, एक खास प्रोटोकॉल के हिसाब से चलाए जाते हैं. यह प्रोटोकॉल, एनवायरमेंट वैरिएबल का इस्तेमाल करके, टेस्ट को यह बताता है कि उनसे क्या उम्मीद की जा रही है. Bazel के लिए टेस्ट से क्या उम्मीद की जा सकती है और टेस्ट के लिए Bazel से क्या उम्मीद की जा सकती है, इस बारे में ज़्यादा जानकारी यहां दी गई है. सबसे आसान तरीके से, 0 वाले एक्सिट कोड का मतलब है कि प्रोसेस पूरी हो गई है. किसी भी अन्य कोड का मतलब है कि प्रोसेस पूरी नहीं हुई है.
कैश मेमोरी की स्थिति वाली फ़ाइल के अलावा, हर जांच प्रोसेस से कई अन्य फ़ाइलें बनती हैं. इन्हें "टेस्ट लॉग डायरेक्ट्री" में डाला जाता है. यह टारगेट कॉन्फ़िगरेशन की आउटपुट डायरेक्ट्री की सबडायरेक्ट्री होती है, जिसे testlogs
कहा जाता है:
test.xml
, JUnit-शैली की एक एक्सएमएल फ़ाइल, जिसमें टेस्ट शार्ड में अलग-अलग टेस्ट केस की जानकारी दी गई हैtest.log
, टेस्ट का कंसोल आउटपुट. stdout और stderr को अलग नहीं किया गया है.test.outputs
, "बिना एलान की गई आउटपुट डायरेक्ट्री"; इसका इस्तेमाल उन टेस्ट के लिए किया जाता है जो टर्मिनल पर प्रिंट करने के अलावा, फ़ाइलों को भी आउटपुट करना चाहते हैं.
टेस्ट को लागू करने के दौरान दो चीज़ें हो सकती हैं, जो सामान्य टारगेट बनाते समय नहीं हो सकतीं: एक्सक्लूज़िव टेस्ट लागू करना और आउटपुट स्ट्रीम करना.
कुछ टेस्ट को एक्सक्लूज़िव मोड में चलाना ज़रूरी होता है. उदाहरण के लिए, इन्हें अन्य टेस्ट के साथ नहीं चलाना चाहिए. इसे जांच के नियम में tags=["exclusive"]
जोड़कर या --test_strategy=exclusive
के साथ जांच चलाकर पाया जा सकता है . हर एक्सक्लूज़िव जांच, Skyframe के अलग-अलग इनवोकेशन से चलाई जाती है. यह "मुख्य" बिल्ड के बाद, जांच को चलाने का अनुरोध करता है. इसे SkyframeExecutor.runExclusiveTest()
में लागू किया गया है.
सामान्य कार्रवाइयों के उलट, जिनका टर्मिनल आउटपुट कार्रवाई पूरी होने पर डंप हो जाता है, उपयोगकर्ता टेस्ट के आउटपुट को स्ट्रीम करने का अनुरोध कर सकता है, ताकि उन्हें लंबे समय तक चलने वाले टेस्ट की प्रोग्रेस के बारे में जानकारी मिल सके. इसे --test_output=streamed
कमांड लाइन विकल्प से तय किया जाता है. साथ ही, इसमें खास तौर पर एक टेस्ट लागू किया जाता है, ताकि अलग-अलग टेस्ट के आउटपुट एक-दूसरे से न बदले.
इसे StreamedTestOutput
क्लास में लागू किया गया है. यह काम, टेस्ट की test.log
फ़ाइल में हुए बदलावों को पोल करके करता है. साथ ही, Bazel के नियमों वाले टर्मिनल में नए बाइट डालता है.
चलाए गए टेस्ट के नतीजे, इवेंट बस पर उपलब्ध होते हैं. इसके लिए, TestAttempt
, TestResult
या TestingCompleteEvent
जैसे अलग-अलग इवेंट को देखा जाता है. इन्हें बिल्ड इवेंट प्रोटोकॉल में डाला जाता है और AggregatingTestListener
की मदद से कंसोल में भेजा जाता है.
कवरेज कलेक्शन
कवरेज की रिपोर्ट, bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
फ़ाइलों में LCOV फ़ॉर्मैट में टेस्ट के ज़रिए दी जाती है .
कवरेज इकट्ठा करने के लिए, हर टेस्ट को collect_coverage.sh
नाम की स्क्रिप्ट में रैप किया जाता है.
यह स्क्रिप्ट, कवरेज इकट्ठा करने की सुविधा चालू करने के लिए, टेस्ट का एनवायरमेंट सेट अप करती है. साथ ही, यह तय करती है कि कवरेज रनटाइम(रनटाइम) से कवरेज फ़ाइलें कहां लिखी जाती हैं. इसके बाद, यह टेस्ट करता है. एक टेस्ट में कई सबप्रोसेस चल सकती हैं. साथ ही, इसमें कई अलग-अलग प्रोग्रामिंग भाषाओं में लिखे गए हिस्से हो सकते हैं. इनमें अलग-अलग कवरेज कलेक्शन रनटाइम होते हैं. रैपर स्क्रिप्ट, ज़रूरत पड़ने पर नतीजों वाली फ़ाइलों को LCOV फ़ॉर्मैट में बदलती है और उन्हें एक फ़ाइल में मर्ज करती है.
collect_coverage.sh
का इंटरपोज़िशन, टेस्ट की रणनीतियों की मदद से किया जाता है. इसके लिए, टेस्ट के इनपुट में collect_coverage.sh
का शामिल होना ज़रूरी है. यह काम, एलिमेंट के लिए डिफ़ॉल्ट तौर पर लागू होने वाले एट्रिब्यूट :coverage_support
की मदद से किया जाता है. इस एट्रिब्यूट की वैल्यू, कॉन्फ़िगरेशन फ़्लैग --coverage_support
की वैल्यू पर सेट होती है (TestConfiguration.TestOptions.coverageSupport
देखें)
कुछ भाषाएं ऑफ़लाइन इंस्ट्रूमेंटेशन करती हैं. इसका मतलब है कि कवरेज इंस्ट्रूमेंटेशन को C++ जैसी भाषाओं में, कॉम्पाइल करने के समय जोड़ा जाता है. वहीं, कुछ भाषाएं ऑनलाइन इंस्ट्रूमेंटेशन करती हैं. इसका मतलब है कि कवरेज इंस्ट्रूमेंटेशन को, प्रोग्राम को लागू करने के समय जोड़ा जाता है.
बेसलाइन कवरेज एक और मुख्य कॉन्सेप्ट है. यह किसी लाइब्रेरी, बिनेरी या टेस्ट की कवरेज है. इससे पता चलता है कि उसमें कोई कोड नहीं चलाया गया था. यह समस्या हल करता है कि अगर आपको किसी बाइनरी के लिए टेस्ट कवरेज का हिसाब लगाना है, तो सभी टेस्ट की कवरेज को मर्ज करना काफ़ी नहीं है. ऐसा इसलिए, क्योंकि बाइनरी में ऐसा कोड हो सकता है जो किसी भी टेस्ट से लिंक न हो. इसलिए, हम हर बाइनरी के लिए एक कवरेज फ़ाइल जनरेट करते हैं. इसमें सिर्फ़ वे फ़ाइलें शामिल होती हैं जिनके लिए हम कवरेज इकट्ठा करते हैं. इनमें ऐसी कोई लाइन नहीं होती जिसकी कवरेज ली गई हो. टारगेट के लिए बेसलाइन कवरेज फ़ाइल,
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
पर है . अगर Bazel को --nobuild_tests_only
फ़्लैग दिया जाता है, तो यह टेस्ट के साथ-साथ बाइनरी और लाइब्रेरी के लिए भी जनरेट होता है.
बेसलाइन कवरेज अभी उपलब्ध नहीं है.
हम हर नियम के लिए, कवरेज इकट्ठा करने के लिए फ़ाइलों के दो ग्रुप ट्रैक करते हैं: इंस्ट्रूमेंट की गई फ़ाइलों का सेट और इंस्ट्रूमेंटेशन मेटाडेटा फ़ाइलों का सेट.
इंस्ट्रुमेंट वाली फ़ाइलों का सेट, इंस्ट्रुमेंट में सेव की गई फ़ाइलों का सेट है. ऑनलाइन कवरेज के रनटाइम के लिए, रनटाइम के दौरान इसका इस्तेमाल करके यह तय किया जा सकता है कि किन फ़ाइलों को इंस्ट्रूमेंट करना है. इसका इस्तेमाल, बेसलाइन कवरेज लागू करने के लिए भी किया जाता है.
इंस्ट्रूमेंटेशन मेटाडेटा फ़ाइलों का सेट, अतिरिक्त फ़ाइलों का सेट होता है. किसी टेस्ट को, Bazel के लिए ज़रूरी LCOV फ़ाइलें जनरेट करने के लिए, इन फ़ाइलों की ज़रूरत होती है. आम तौर पर, इसमें रनटाइम के हिसाब से फ़ाइलें शामिल होती हैं. उदाहरण के लिए, gcc कंपाइलेशन के दौरान .gcno फ़ाइलें जनरेट करता है. कवरेज मोड चालू होने पर, इन्हें टेस्ट ऐक्शन के इनपुट के सेट में जोड़ा जाता है.
कवरेज को इकट्ठा किया जा रहा है या नहीं, यह BuildConfiguration
में सेव किया जाता है. यह बेहद आसान है, क्योंकि यह इस बिट के आधार पर टेस्ट ऐक्शन और ऐक्शन ग्राफ़ को बदलने का आसान तरीका है. हालांकि, इसका मतलब यह भी है कि अगर इस बिट को फ़्लिप किया जाता है, तो सभी टारगेट का फिर से विश्लेषण करने की ज़रूरत होगी (कुछ भाषाओं, जैसे कि C++ में कवरेज इकट्ठा करने के लिए अलग-अलग कंपाइलर विकल्पों की ज़रूरत होती है. इससे कवरेज इकट्ठा हो सकता है. हालांकि, फिर से विश्लेषण करने की ज़रूरत पड़ती है).
कवरेज से जुड़ी सहायता फ़ाइलें सीधे तौर पर लेबल पर निर्भर करती हैं, ताकि उन्हें शुरू करने की नीति से बदला जा सके. इससे बेज़ल के अलग-अलग वर्शन में, फ़ाइलों में अंतर हो पाता है. आम तौर पर, इन अंतरों को हटा दिया जाता है और हम इनमें से किसी एक को स्टैंडर्ड के तौर पर इस्तेमाल करते हैं.
हम एक "कवरेज रिपोर्ट" भी जनरेट करते हैं, जो Bazel के हर टेस्ट के लिए इकट्ठा की गई कवरेज को मर्ज करती है. इसे CoverageReportActionFactory
मैनेज करता है और BuildView.createResult()
से कॉल किया जाता है . यह, पहले टेस्ट के :coverage_report_generator
एट्रिब्यूट को देखकर, ज़रूरी टूल का ऐक्सेस पाता है.
क्वेरी इंजन
बेज़ल से अलग-अलग ग्राफ़ के बारे में बहुत कुछ जानने के लिए बहुत कम भाषा का इस्तेमाल किया जाता है. यहां क्वेरी के ये टाइप दिए गए हैं:
bazel query
का इस्तेमाल, टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel cquery
का इस्तेमाल, कॉन्फ़िगर किए गए टारगेट ग्राफ़ की जांच करने के लिए किया जाता हैbazel aquery
का इस्तेमाल, ऐक्शन ग्राफ़ की जांच करने के लिए किया जाता है
इनमें से हर सुविधा को AbstractBlazeQueryEnvironment
की सबक्लास बनाकर लागू किया जाता है.
QueryFunction
को सबक्लास करके, क्वेरी के अन्य फ़ंक्शन जोड़े जा सकते हैं. क्वेरी के नतीजों को स्ट्रीम करने की अनुमति देने के लिए, उन्हें कुछ डेटा स्ट्रक्चर में इकट्ठा करने के बजाय, QueryFunction
को query2.engine.Callback
पास किया जाता है. यह ऐसे नतीजों के लिए कॉल करता है जिन्हें वह दिखाना चाहता है.
क्वेरी का नतीजा कई तरीकों से दिखाया जा सकता है: लेबल, लेबल और नियम वाली क्लास, एक्सएमएल, प्रोटोबस वगैरह. इन्हें OutputFormatter
के सबक्लास के तौर पर लागू किया जाता है.
कुछ क्वेरी आउटपुट फ़ॉर्मैट (proto, definitely) की एक खास ज़रूरत यह है कि Bazel को पैकेज लोड करने से मिलने वाली _पूरी _जानकारी को एमिट करना होगा, ताकि कोई व्यक्ति आउटपुट की तुलना कर सके और यह पता लगा सके कि किसी खास टारगेट में बदलाव हुआ है या नहीं. इसलिए, एट्रिब्यूट की वैल्यू को सीरियल में बदला जा सकता है. यही वजह है कि ऐसे बहुत कम एट्रिब्यूट टाइप हैं जिनमें Starlark की जटिल वैल्यू नहीं होती हैं. आम तौर पर, समस्या का हल किसी लेबल का इस्तेमाल करना और जटिल जानकारी को उस लेबल वाले नियम में जोड़ना है. यह समस्या हल करने का एक अच्छा तरीका नहीं है और इस शर्त को हटाना अच्छा होगा.
मॉड्यूल सिस्टम
Bazel में मॉड्यूल जोड़कर, इसे बेहतर बनाया जा सकता है. हर मॉड्यूल को BlazeModule
का सबक्लास होना चाहिए (यह नाम, Bazel के इतिहास का एक हिस्सा है, जब इसे Blaze कहा जाता था). साथ ही, किसी कमांड को लागू करने के दौरान, उसे अलग-अलग इवेंट के बारे में जानकारी मिलती है.
इनका इस्तेमाल ज़्यादातर, "नॉन-कोर" फ़ंक्शन के अलग-अलग हिस्सों को लागू करने के लिए किया जाता है. इन फ़ंक्शन की ज़रूरत, Bazel के कुछ वर्शन (जैसे, Google में इस्तेमाल होने वाले वर्शन) को होती है:
- रिमोट एक्ज़ीक्यूशन सिस्टम के इंटरफ़ेस
- नए निर्देश
एक्सटेंशन पॉइंट BlazeModule
के ऑफ़र का सेट कुछ हद तक गड़बड़ी वाला है. इसका इस्तेमाल डिज़ाइन से जुड़े अच्छे सिद्धांतों के उदाहरण के तौर पर न करें.
इवेंट बस
BlazeModules, बाकी Bazel के साथ इवेंट बस (EventBus
) के ज़रिए मुख्य रूप से कम्यूनिकेट करते हैं: हर बिल्ड के लिए एक नया इंस्टेंस बनाया जाता है. Bazel के अलग-अलग हिस्से, उसमें इवेंट पोस्ट कर सकते हैं. साथ ही, मॉड्यूल उन इवेंट के लिए लिसनर रजिस्टर कर सकते हैं जिनमें उनकी दिलचस्पी है. उदाहरण के लिए, इन चीज़ों को इवेंट के तौर पर दिखाया जाता है:
- बनाए जाने वाले बिल्ड टारगेट की सूची तय कर दी गई है
(
TargetParsingCompleteEvent
) - टॉप-लेवल कॉन्फ़िगरेशन तय कर दिए गए हैं
(
BuildConfigurationEvent
) - टारगेट बनाया गया या नहीं (
TargetCompleteEvent
) - टेस्ट किया गया (
TestAttempt
,TestSummary
)
इनमें से कुछ इवेंट, बैज के बाहर बिल्ड इवेंट प्रोटोकॉल में दिखाए जाते हैं (ये BuildEvent
s हैं). इससे न सिर्फ़ BlazeModule
, बल्कि Bazel प्रोसेस के बाहर की चीज़ें भी बिल्ड को मॉनिटर कर सकती हैं. इन्हें या तो ऐसी फ़ाइल के तौर पर ऐक्सेस किया जा सकता है जिसमें
प्रोटोकॉल मैसेज होते हैं या Ba बैंक, इवेंट स्ट्रीम करने के लिए किसी सर्वर (जिसे Build इवेंट सेवा कहा जाता है) से कनेक्ट कर सकता है.
इसे build.lib.buildeventservice
और
build.lib.buildeventstream
Java पैकेज में लागू किया गया है.
बाहरी डेटा स्टोर करने की जगहें
Bazel को मूल रूप से, मोनोरेपो (एक सोर्स ट्री जिसमें प्रोग्राम बनाने के लिए ज़रूरी सभी चीज़ें होती हैं) में इस्तेमाल करने के लिए डिज़ाइन किया गया था. हालांकि, Bazel को अब ऐसी दुनिया में इस्तेमाल किया जा रहा है जहां यह ज़रूरी नहीं है कि वह मोनोरेपो में ही इस्तेमाल किया जाए. "बाहरी रिपॉज़िटरी" एक ऐसा एब्स्ट्रैक्शन है जिसका इस्तेमाल इन दोनों दुनिया को जोड़ने के लिए किया जाता है: ये ऐसे कोड को दिखाते हैं जो बिल्ड के लिए ज़रूरी है, लेकिन मुख्य सोर्स ट्री में नहीं है.
WORKSPACE फ़ाइल
बाहरी रिपॉज़िटरी का सेट, WORKSPACE फ़ाइल को पार्स करके तय किया जाता है. उदाहरण के लिए, इस तरह का एलान:
local_repository(name="foo", path="/foo/bar")
@foo
नाम की रिपॉज़िटरी में नतीजे उपलब्ध हैं. यह तब मुश्किल हो जाता है, जब कोई व्यक्ति Starlark फ़ाइलों में नए रिपॉज़िटरी नियम तय कर सकता है. इसके बाद, इनका इस्तेमाल नए Starlark कोड को लोड करने के लिए किया जा सकता है. इस कोड का इस्तेमाल, नए रिपॉज़िटरी नियम तय करने के लिए किया जा सकता है.
इस मामले को हैंडल करने के लिए, WorkspaceFileFunction
में मौजूद WORKSPACE फ़ाइल को पार्स करने की प्रोसेस को load()
स्टेटमेंट के हिसाब से अलग-अलग हिस्सों में बांटा जाता है. चंक इंडेक्स को WorkspaceFileKey.getIndex()
से दिखाया जाता है और इंडेक्स X तक WorkspaceFileFunction
का हिसाब लगाने का मतलब है कि Xवें load()
स्टेटमेंट तक इसका आकलन किया जाता है.
डेटा स्टोर करने की जगहें फ़ेच करना
रिपॉज़िटरी का कोड, Bazel के लिए उपलब्ध होने से पहले, उसे फ़ेच करना ज़रूरी है. इससे Bazel, $OUTPUT_BASE/external/<repository name>
के नीचे एक डायरेक्ट्री बनाता है.
रिपॉज़िटरी को फ़ेच करने के लिए, यह तरीका अपनाएं:
PackageLookupFunction
को पता चलता है कि उसे एक रिपॉज़िटरी की ज़रूरत है और वहSkyKey
के तौर परRepositoryName
बनाता है, जोRepositoryLoaderFunction
को ट्रिगर करता हैRepositoryLoaderFunction
,RepositoryDelegatorFunction
को अनुरोध भेजता है. हालांकि, इसकी वजह साफ़ तौर पर नहीं बताई गई है. कोड के मुताबिक, Skyframe के रीस्टार्ट होने पर, चीज़ों को फिर से डाउनलोड करने से बचने के लिए ऐसा किया जाता है. हालांकि, यह वजह सही नहीं हैRepositoryDelegatorFunction
, WORKSPACE फ़ाइल के चंक को तब तक दोहराकर, वह रिपॉज़िटरी नियम ढूंढता है जिसे फ़ेच करने के लिए कहा गया है. ऐसा तब तक किया जाता है, जब तक कि अनुरोध की गई रिपॉज़िटरी नहीं मिल जाती- का सही विकल्प मिलता है, जो रिपॉज़िटरी फ़ेच करने की सुविधा को लागू करता है. यह रिपॉज़िटरी का Starlark लागू करने वाला या Java में लागू किए गए रिपॉज़िटरी के लिए, हार्ड-कोड किया गया मैप होता है.
RepositoryFunction
कैश मेमोरी की कई लेयर होती हैं, क्योंकि रिपॉज़िटरी फ़ेच करना बहुत महंगा हो सकता है:
- डाउनलोड की गई फ़ाइलों के लिए एक कैश मेमोरी है, जिसे उनके चेकसम (
RepositoryCache
) से जोड़ा जाता है. इसके लिए यह ज़रूरी है कि Workspace फ़ाइल में चेकसम उपलब्ध हो, लेकिन फिर भी यह हरमैटिसिटी के लिए अच्छा है. इसे एक ही वर्कस्टेशन पर मौजूद हर Bazel सर्वर इंस्टेंस शेयर करता है. भले ही, वे किसी भी वर्कस्पेस या आउटपुट बेस में चल रहे हों. $OUTPUT_BASE/external
में मौजूद हर रिपॉज़िटरी के लिए एक "मार्कर फ़ाइल" लिखी जाती है. इसमें उस नियम का चेकसम होता है जिसका इस्तेमाल उसे फ़ेच करने के लिए किया गया था. अगर Bazel के सर्वर को रीस्टार्ट करने के बाद भी चेकसम में कोई बदलाव नहीं होता है, तो उसे फिर से फ़ेच नहीं किया जाता. इसेRepositoryDelegatorFunction.DigestWriter
में लागू किया गया है.--distdir
कमांड लाइन विकल्प, एक और कैश मेमोरी तय करता है. इसका इस्तेमाल, डाउनलोड किए जाने वाले आर्टफ़ैक्ट को खोजने के लिए किया जाता है. यह एंटरप्राइज़ सेटिंग में काम आता है, जहां Bazel को इंटरनेट से कोई भी चीज़ फ़ेच नहीं करनी चाहिए. इसेDownloadManager
ने लागू किया है .
डेटा स्टोर करने की जगह को डाउनलोड करने के बाद, उसमें मौजूद आर्टफ़ैक्ट को सोर्स आर्टफ़ैक्ट के तौर पर माना जाता है. इससे एक समस्या हो सकती है, क्योंकि बेज़ल आम तौर पर सोर्स आर्टफ़ैक्ट की अप-टू-डेट होने की जांच करने के लिए, stat() को कॉल करता है. साथ ही, जब रिपॉज़िटरी की परिभाषा में बदलाव होता है, तब ये आर्टफ़ैक्ट भी अमान्य हो जाते हैं. इसलिए,
किसी बाहरी डेटा स्टोर करने की जगह में मौजूद आर्टफ़ैक्ट के FileStateValue
को उसके बाहरी डेटा स्टोर करने की जगह पर निर्भर होना चाहिए. इसे ExternalFilesHelper
मैनेज करता है.
मैनेज की जा रही डायरेक्ट्री
कभी-कभी, बाहरी रिपॉज़िटरी को वर्कस्पेस रूट में मौजूद फ़ाइलों में बदलाव करना पड़ता है. जैसे, पैकेज मैनेजर, जो डाउनलोड किए गए पैकेज को सोर्स ट्री की सबडायरेक्ट्री में रखता है. यह बात, Bazel के इस अनुमान से मेल नहीं खाती कि सोर्स फ़ाइलों में सिर्फ़ उपयोगकर्ता बदलाव करता है, न कि Bazel. साथ ही, इससे पैकेज को Workspace के रूट में मौजूद हर डायरेक्ट्री का रेफ़रंस देने की अनुमति मिलती है. इस तरह की बाहरी रिपॉज़िटरी को काम करने के लिए, Bazel दो काम करता है:
- इससे उपयोगकर्ता को Workspace की उन सबडायरेक्ट्री के बारे में बताने की अनुमति मिलती है जिनमें Bazel को ऐक्सेस करने की अनुमति नहीं है. इन्हें
.bazelignore
नाम की फ़ाइल में लिस्ट किया जाता है और इसकी सुविधाBlacklistedPackagePrefixesFunction
में लागू की जाती है. - हम वर्कस्पेस की सबडायरेक्ट्री से एक्सटर्नल रिपॉज़िटरी (डेटा स्टोर करने की जगह) में की जाने वाली मैपिंग को कोड में बदलते हैं. इसे
ManagedDirectoriesKnowledge
में मैनेज किया जाता है. साथ ही,FileStateValue
को उसी तरह हैंडल किया जाता है जिस तरह बाहरी डेटा स्टोर करने की सामान्य जगहों के लिए इन मैपिंग को हैंडल किया जाता है.
रिपॉज़िटरी मैपिंग
ऐसा हो सकता है कि कई रिपॉज़िटरी, एक ही रिपॉज़िटरी पर निर्भर करना चाहें, लेकिन अलग-अलग वर्शन में. यह "डायमंड डिपेंडेंसी समस्या" का एक उदाहरण है. उदाहरण के लिए, अगर बिल्ड में अलग-अलग रिपॉज़िटरी में मौजूद दो बाइनरी को 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
के ज़रिए दिखाया जाता है. यह आउटपुट को फ़ॉर्मैट करने और प्रोग्रेस रिपोर्ट करने के लिए ज़रूरी है. इसमें दो इनपुट होते हैं:
- इवेंट बस
- Reporter की मदद से इसमें भेजी गई इवेंट स्ट्रीम
क्लाइंट की आरपीसी स्ट्रीम से, कमांड को लागू करने वाली मशीन (उदाहरण के लिए, बाज़ल का बाकी हिस्सा) का सीधा कनेक्शन सिर्फ़ 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
के तहत, बहुत बेहतर तरीके से बने bash टेस्ट फ़्रेमवर्क का इस्तेमाल करके लागू किए गए- Java में लागू की गई सुविधाएं. इन्हें
BuildIntegrationTestCase
के सबक्लास के तौर पर लागू किया जाता है
BuildIntegrationTestCase
, इंटिग्रेशन टेस्टिंग के लिए सबसे ज़्यादा इस्तेमाल किया जाने वाला फ़्रेमवर्क है, क्योंकि यह ज़्यादातर टेस्टिंग स्थितियों के लिए बेहतर तरीके से तैयार है. यह एक Java फ़्रेमवर्क है. इसलिए, इसमें डीबग करने की सुविधा मिलती है. साथ ही, इसे कई सामान्य डेवलपमेंट टूल के साथ आसानी से इंटिग्रेट किया जा सकता है. Bazel रिपॉज़िटरी में BuildIntegrationTestCase
क्लास के कई उदाहरण हैं.
विश्लेषण टेस्ट, BuildViewTestCase
के सबक्लास के तौर पर लागू किए जाते हैं. इसमें एक स्क्रैच फ़ाइल सिस्टम होता है, जिसका इस्तेमाल BUILD
फ़ाइलें लिखने के लिए किया जा सकता है. इसके बाद, अलग-अलग सहायक तरीके, कॉन्फ़िगर किए गए टारगेट का अनुरोध कर सकते हैं, कॉन्फ़िगरेशन में बदलाव कर सकते हैं, और विश्लेषण के नतीजे के बारे में अलग-अलग बातें बता सकते हैं.