इस पेज पर, टास्क पर आधारित बिल्ड सिस्टम के बारे में बताया गया है. साथ ही, यह भी बताया गया है कि ये सिस्टम कैसे काम करते हैं और इनमें कौनसी समस्याएं आ सकती हैं. शेल स्क्रिप्ट के बाद, टास्क पर आधारित बिल्ड सिस्टम, बिल्ड करने का अगला लॉजिकल तरीका है.
टास्क पर आधारित बिल्ड सिस्टम के बारे में जानकारी
टास्क पर आधारित बिल्ड सिस्टम में, काम की बुनियादी इकाई टास्क होती है. हर टास्क एक स्क्रिप्ट होती है, जो किसी भी तरह के लॉजिक को लागू कर सकती है. टास्क, अन्य टास्क को डिपेंडेंसी के तौर पर तय करते हैं. ये डिपेंडेंसी, टास्क से पहले लागू होनी चाहिए. आजकल इस्तेमाल किए जा रहे ज़्यादातर बिल्ड सिस्टम, टास्क पर आधारित होते हैं. जैसे, Ant, Maven, Gradle, Grunt, और Rake. शेल स्क्रिप्ट के बजाय, ज़्यादातर आधुनिक बिल्ड सिस्टम के लिए इंजीनियरों को बिल्ड फ़ाइलें बनानी होती हैं. इनमें यह बताया जाता है कि बिल्ड कैसे किया जाए.
चींटी के बारे में जानकारी देने वाली किताब से यह उदाहरण देखें:
<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>
   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>
बिल्डफ़ाइल को एक्सएमएल में लिखा जाता है. यह बिल्ड के बारे में कुछ सामान्य मेटाडेटा के साथ-साथ टास्क की सूची (एक्सएमएल में <target> टैग) तय करती है. (Ant, टास्क को दिखाने के लिए target शब्द का इस्तेमाल करता है. साथ ही, कमांड के लिए task शब्द का इस्तेमाल करता है.) हर टास्क, Ant की तय की गई संभावित कमांड की सूची को लागू करता है. इसमें डायरेक्ट्री बनाना और मिटाना, javac चलाना, और JAR फ़ाइल बनाना शामिल है. उपयोगकर्ता से मिले प्लग-इन की मदद से, कमांड के इस सेट को बढ़ाया जा सकता है, ताकि किसी भी तरह के लॉजिक को शामिल किया जा सके. हर टास्क, depends एट्रिब्यूट का इस्तेमाल करके उन टास्क को भी तय कर सकता है जिन पर वह निर्भर करता है. ये डिपेंडेंसी, एक ऐसाइक्लिक ग्राफ़ बनाती हैं. जैसा कि पहले फ़िगर में दिखाया गया है.
इमेज 1. डिपेंडेंसी दिखाने वाला एक ऐसाइक्लिक ग्राफ़
उपयोगकर्ता, Ant के कमांड-लाइन टूल को टास्क देकर बिल्ड करते हैं. उदाहरण के लिए, जब कोई उपयोगकर्ता ant dist टाइप करता है, तो Ant ये कार्रवाइयां करता है:
- यह मौजूदा डायरेक्ट्री में मौजूद build.xmlनाम की फ़ाइल को लोड करता है और उसे पार्स करके, ग्राफ़ स्ट्रक्चर बनाता है. यह स्ट्रक्चर, इमेज 1 में दिखाया गया है.
- यह कमांड लाइन पर दिए गए distनाम के टास्क को ढूंढता है. साथ ही, यह पता लगाता है कि यहcompileनाम के टास्क पर निर्भर है.
- compileनाम के टास्क को ढूंढता है और यह पता लगाता है कि यह- initनाम के टास्क पर निर्भर है.
- initनाम के टास्क को खोजता है और पता लगाता है कि यह किसी भी टास्क पर निर्भर नहीं है.
- यह initटास्क में तय की गई कमांड को पूरा करता है.
- compileमें तय किए गए निर्देशों को तब लागू करता है, जब उस टास्क की सभी डिपेंडेंसी पूरी हो गई हों.
- distमें तय किए गए निर्देशों को तब लागू करता है, जब उस टास्क की सभी डिपेंडेंसी पूरी हो गई हों.
आखिर में, dist टास्क को चलाने के दौरान Ant से लागू किया गया कोड, इस शेल स्क्रिप्ट के बराबर होता है:
./createTimestamp.shmkdir build/javac src/* -d build/mkdir -p dist/lib/jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*
सिंटैक्स हटा दिए जाने पर, बिल्डफ़ाइल और बिल्ड स्क्रिप्ट में ज़्यादा अंतर नहीं होता. हालांकि, ऐसा करने से हमें पहले ही काफ़ी फ़ायदा मिल चुका है. हम अन्य डायरेक्ट्री में नई बिल्डफ़ाइलें बना सकते हैं और उन्हें एक-दूसरे से लिंक कर सकते हैं. हम आसानी से ऐसे नए टास्क जोड़ सकते हैं जो मौजूदा टास्क पर निर्भर होते हैं. हमें ant कमांड-लाइन टूल को सिर्फ़ एक टास्क का नाम देना होता है. इसके बाद, यह टूल यह तय करता है कि कौनसी चीज़ें चलानी हैं.
Ant एक पुराना सॉफ़्टवेयर है. इसे पहली बार साल 2000 में रिलीज़ किया गया था. इन सालों में, Maven और Gradle जैसे अन्य टूल ने Ant को बेहतर बनाया है. साथ ही, बाहरी डिपेंडेंसी के अपने-आप मैनेज होने और बिना किसी एक्सएमएल के बेहतर सिंटैक्स जैसी सुविधाएं जोड़कर, इसे बदल दिया है. हालांकि, इन नए सिस्टम का काम करने का तरीका वही है. ये इंजीनियर को टास्क के तौर पर, सिद्धांतों और मॉड्यूलर तरीके से बिल्ड स्क्रिप्ट लिखने की अनुमति देते हैं. साथ ही, उन टास्क को पूरा करने और उनके बीच डिपेंडेंसी मैनेज करने के लिए टूल उपलब्ध कराते हैं.
टास्क पर आधारित बिल्ड सिस्टम की कमियां
इन टूल की मदद से, इंजीनियर किसी भी स्क्रिप्ट को टास्क के तौर पर सेट कर सकते हैं. इसलिए, ये टूल बहुत काम के होते हैं. इनकी मदद से, वे सभी काम किए जा सकते हैं जिनके बारे में आपने सोचा है. हालांकि, इस सुविधा के कुछ नुकसान भी हैं. साथ ही, टास्क पर आधारित बिल्ड सिस्टम के साथ काम करना मुश्किल हो सकता है, क्योंकि उनकी बिल्ड स्क्रिप्ट ज़्यादा जटिल होती हैं. इस तरह के सिस्टम में समस्या यह होती है कि वे इंजीनियरों को बहुत ज़्यादा और सिस्टम को बहुत कम पावर देते हैं. सिस्टम को यह नहीं पता कि स्क्रिप्ट क्या कर रही हैं. इसलिए, परफ़ॉर्मेंस पर असर पड़ता है. ऐसा इसलिए होता है, क्योंकि सिस्टम को यह तय करने में बहुत सावधानी बरतनी पड़ती है कि बिल्ड के चरणों को कब शेड्यूल और लागू किया जाए. सिस्टम के पास यह पुष्टि करने का कोई तरीका नहीं है कि हर स्क्रिप्ट, तय किए गए काम को सही तरीके से कर रही है या नहीं. इसलिए, स्क्रिप्ट ज़्यादा जटिल हो जाती हैं और आखिर में, उन्हें डीबग करने की ज़रूरत पड़ती है.
बिल्ड के चरणों को पैरलल करने में आने वाली समस्याएं
आजकल के डेवलपमेंट वर्कस्टेशन काफ़ी पावरफ़ुल होते हैं. इनमें कई कोर होते हैं, जो एक साथ कई बिल्ड स्टेप पूरे कर सकते हैं. हालांकि, टास्क पर आधारित सिस्टम अक्सर टास्क को एक साथ पूरा नहीं कर पाते. भले ही, ऐसा लगता हो कि उन्हें ऐसा करना चाहिए. मान लें कि टास्क A, टास्क B और C पर निर्भर करता है. टास्क B और C एक-दूसरे पर निर्भर नहीं हैं. इसलिए, क्या उन्हें एक साथ चलाया जा सकता है, ताकि सिस्टम टास्क A को ज़्यादा तेज़ी से पूरा कर सके? ऐसा हो सकता है, अगर वे एक ही रिसॉर्स का इस्तेमाल न करें. हालांकि, ऐसा हो सकता है कि दोनों एक ही फ़ाइल का इस्तेमाल करके, अपनी स्थितियों को ट्रैक करते हों. ऐसे में, दोनों को एक साथ चलाने पर टकराव हो सकता है. आम तौर पर, सिस्टम को यह पता नहीं चलता है. इसलिए, या तो उसे इन टकरावों का जोखिम उठाना पड़ता है (जिससे बिल्ड की समस्याएं होती हैं. ये समस्याएं कभी-कभी होती हैं, लेकिन इन्हें ठीक करना बहुत मुश्किल होता है) या उसे पूरे बिल्ड को एक ही प्रोसेस में एक ही थ्रेड पर चलाने के लिए सीमित करना पड़ता है. इससे डेवलपर मशीन का सही तरीके से इस्तेमाल नहीं हो पाता. साथ ही, एक से ज़्यादा मशीनों पर बिल्ड डिस्ट्रिब्यूट करने की सुविधा भी नहीं मिलती.
इंक्रीमेंटल बिल्ड बनाने में समस्या आ रही है
एक अच्छा बिल्ड सिस्टम, इंजीनियरों को भरोसेमंद इंक्रीमेंटल बिल्ड बनाने की सुविधा देता है. इससे किसी छोटे बदलाव के लिए, पूरे कोडबेस को शुरू से फिर से बनाने की ज़रूरत नहीं पड़ती. अगर बिल्ड सिस्टम धीमा है और ऊपर बताई गई वजहों से, बिल्ड के चरणों को एक साथ प्रोसेस नहीं कर पाता है, तो यह खास तौर पर ज़रूरी है. हालांकि, अफ़सोस की बात है कि टास्क पर आधारित बिल्ड सिस्टम को भी यहां समस्या होती है. टास्क किसी भी तरह के हो सकते हैं. इसलिए, यह पता नहीं लगाया जा सकता कि उन्हें पहले ही पूरा कर लिया गया है या नहीं. कई टास्क में, सोर्स फ़ाइलों का एक सेट लिया जाता है और बाइनरी का एक सेट बनाने के लिए कंपाइलर चलाया जाता है. इसलिए, अगर सोर्स फ़ाइलों में कोई बदलाव नहीं किया गया है, तो उन्हें फिर से चलाने की ज़रूरत नहीं होती. हालांकि, अतिरिक्त जानकारी के बिना सिस्टम यह पक्के तौर पर नहीं कह सकता. ऐसा हो सकता है कि टास्क ऐसी फ़ाइल डाउनलोड करता हो जिसमें बदलाव किया जा सकता है या ऐसा हो सकता है कि वह ऐसा टाइमस्टैंप लिखता हो जो हर बार अलग हो सकता है. सही नतीजे पाने के लिए, सिस्टम को आम तौर पर हर बिल्ड के दौरान हर टास्क को फिर से चलाना पड़ता है. कुछ बिल्ड सिस्टम, इंक्रीमेंटल बिल्ड की सुविधा चालू करने की कोशिश करते हैं. इसके लिए, वे इंजीनियरों को ऐसी शर्तें तय करने की अनुमति देते हैं जिनके तहत किसी टास्क को फिर से चलाना ज़रूरी होता है. कभी-कभी ऐसा करना मुमकिन होता है, लेकिन अक्सर यह समस्या उतनी आसान नहीं होती जितनी दिखती है. उदाहरण के लिए, C++ जैसी भाषाओं में फ़ाइलों को सीधे तौर पर दूसरी फ़ाइलों में शामिल किया जा सकता है. इसलिए, इनपुट सोर्स को पार्स किए बिना, यह पता नहीं लगाया जा सकता कि बदलावों के लिए फ़ाइलों के पूरे सेट पर नज़र रखनी है. इंजीनियर अक्सर शॉर्टकट का इस्तेमाल करते हैं. इन शॉर्टकट की वजह से, कभी-कभी ऐसी समस्याएं हो सकती हैं जिन्हें ठीक करना मुश्किल होता है. जैसे, किसी टास्क के नतीजे को तब भी रीयूज़ किया जाता है, जब ऐसा नहीं करना चाहिए. ऐसा बार-बार होने पर, इंजीनियर हर बिल्ड से पहले क्लीन कमांड का इस्तेमाल करने लगते हैं, ताकि उन्हें नई स्थिति मिल सके. इससे इंक्रीमेंटल बिल्ड का मकसद पूरी तरह से खत्म हो जाता है. यह पता लगाना कि किसी टास्क को कब फिर से चलाना है, काफ़ी मुश्किल होता है. इसलिए, इस काम को इंसानों की तुलना में मशीनें बेहतर तरीके से कर सकती हैं.
स्क्रिप्ट को बनाए रखने और डीबग करने में मुश्किल होना
आखिर में, टास्क पर आधारित बिल्ड सिस्टम की ओर से लागू की गई बिल्ड स्क्रिप्ट के साथ काम करना अक्सर मुश्किल होता है. हालांकि, बिल्ड स्क्रिप्ट की अक्सर कम जांच की जाती है, लेकिन ये स्क्रिप्ट, बनाए जा रहे सिस्टम की तरह ही कोड होती हैं. इसलिए, इनमें बग आसानी से छिप सकते हैं. टास्क पर आधारित बिल्ड सिस्टम का इस्तेमाल करते समय, आम तौर पर होने वाली कुछ समस्याओं के उदाहरण यहां दिए गए हैं:
- टास्क A को आउटपुट के तौर पर कोई फ़ाइल जनरेट करने के लिए, टास्क B पर निर्भर रहना पड़ता है. टास्क B के मालिक को यह नहीं पता कि अन्य टास्क इस पर निर्भर हैं. इसलिए, वह इसे बदलकर किसी दूसरी जगह पर आउटपुट जनरेट करने के लिए सेट कर देता है. इसका पता तब तक नहीं लगाया जा सकता, जब तक कोई व्यक्ति टास्क A को चलाने की कोशिश नहीं करता और उसे पता नहीं चलता कि यह काम नहीं कर रहा है.
- टास्क A, टास्क B पर निर्भर करता है. टास्क B, टास्क C पर निर्भर करता है. टास्क C, आउटपुट के तौर पर एक खास फ़ाइल जनरेट करता है. यह फ़ाइल, टास्क A के लिए ज़रूरी है. टास्क B का मालिक यह तय करता है कि अब इसे टास्क C पर निर्भर रहने की ज़रूरत नहीं है. इससे टास्क A पूरा नहीं हो पाता, भले ही टास्क B को टास्क C से कोई मतलब न हो!
- किसी नए टास्क का डेवलपर, टास्क को चलाने वाली मशीन के बारे में गलती से कोई अनुमान लगा लेता है. जैसे, किसी टूल की जगह या किसी खास एनवायरमेंट वैरिएबल की वैल्यू. यह टास्क उनकी मशीन पर काम करता है, लेकिन जब भी कोई दूसरा डेवलपर इसे आज़माता है, तो यह काम नहीं करता.
- टास्क में कोई ऐसा कॉम्पोनेंट शामिल है जो पहले से तय नहीं होता. जैसे, इंटरनेट से कोई फ़ाइल डाउनलोड करना या किसी बिल्ड में टाइमस्टैंप जोड़ना. अब, लोगों को हर बार बिल्ड चलाने पर अलग-अलग नतीजे मिलते हैं. इसका मतलब है कि इंजीनियर, एक-दूसरे की गड़बड़ियों या ऑटोमेटेड बिल्ड सिस्टम पर होने वाली गड़बड़ियों को हमेशा ठीक नहीं कर पाएंगे.
- एक से ज़्यादा डिपेंडेंसी वाले टास्क से रेस कंडीशन बन सकती हैं. अगर टास्क A, टास्क B और टास्क C, दोनों पर निर्भर करता है और टास्क B और C, दोनों एक ही फ़ाइल में बदलाव करते हैं, तो टास्क A को अलग-अलग नतीजे मिलते हैं. यह इस बात पर निर्भर करता है कि टास्क B और C में से कौनसा टास्क पहले पूरा होता है.
यहां दिए गए टास्क-आधारित फ़्रेमवर्क में, परफ़ॉर्मेंस, सही जवाब, या रखरखाव से जुड़ी इन समस्याओं को हल करने का कोई सामान्य तरीका नहीं है. जब तक इंजीनियर, बिल्ड के दौरान चलने वाला कोई भी कोड लिख सकते हैं, तब तक सिस्टम के पास इतनी जानकारी नहीं हो सकती कि वह हमेशा बिल्ड को तेज़ी से और सही तरीके से चला सके. इस समस्या को हल करने के लिए, हमें कुछ अधिकार इंजीनियरों से लेकर सिस्टम को देने होंगे. साथ ही, सिस्टम की भूमिका को फिर से परिभाषित करना होगा. सिस्टम को सिर्फ़ टास्क पूरे करने के बजाय, आर्टफ़ैक्ट बनाने की भूमिका देनी होगी.
इस तरीके से, आर्टफ़ैक्ट पर आधारित बिल्ड सिस्टम बनाए गए. जैसे, Blaze और Bazel.
