टास्क-आधारित बिल्ड सिस्टम

किसी समस्या की शिकायत करें सोर्स देखें Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

इस पेज पर टास्क-आधारित बिल्ड सिस्टम के बारे में जानकारी दी गई है. इसमें बताया गया है कि ये सिस्टम कैसे काम करते हैं. साथ ही, इसमें कुछ ऐसी मुश्किलों के बारे में भी बताया गया है जो टास्क पर आधारित सिस्टम से हो सकते हैं. शेल स्क्रिप्ट के बाद, टास्क पर आधारित बिल्ड सिस्टम, बिल्डिंग के अगले लॉजिकल इवोल्यूशन हैं.

टास्क पर आधारित बिल्ड सिस्टम को समझना

टास्क पर आधारित बिल्ड सिस्टम में, काम की बुनियादी इकाई टास्क होती है. हर टास्क एक ऐसी स्क्रिप्ट है जो किसी भी तरह के लॉजिक को एक्ज़ीक्यूट कर सकती है. साथ ही, टास्क अन्य टास्क को डिपेंडेंसी के तौर पर तय करते हैं, जो उनसे पहले चलना चाहिए. फ़िलहाल, टास्क के आधार पर काम करने वाले ज़्यादातर मुख्य बिल्ड सिस्टम इस्तेमाल किए जा रहे हैं. जैसे, 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, टास्क को दिखाने के लिए, टारगेट शब्द का इस्तेमाल करता है. साथ ही, कमांड के लिए, टास्क शब्द का इस्तेमाल करता है.) हर टास्क, Ant के तय किए गए संभावित कमांड की सूची को पूरा करता है. इसमें डायरेक्ट्री बनाना और मिटाना, javac चलाना, और JAR फ़ाइल बनाना शामिल है. उपयोगकर्ता से मिले प्लग-इन की मदद से, कमांड के इस सेट को बढ़ाया जा सकता है, ताकि किसी भी तरह के लॉजिक को कवर किया जा सके. हर टास्क के लिए, 'इस पर निर्भर करता है' एट्रिब्यूट की मदद से यह भी बताया जा सकता है कि वह किन टास्क पर निर्भर करता है. ये डिपेंडेंसी, एक ऐसा ग्राफ़ बनाती हैं जिसमें कोई साइकल नहीं होता. जैसा कि पहले चित्र में दिखाया गया है.

डिपेंडेंसी दिखाने वाला ऐक्रिलिक ग्राफ़

चित्र 1. डिपेंडेंसी दिखाने वाला ऐसाइकल ग्राफ़

उपयोगकर्ता, Ant के कमांड-लाइन टूल को टास्क देकर, बिल्ड अंजाम देते हैं. उदाहरण के लिए, जब कोई उपयोगकर्ता ant dist टाइप करता है, तो Ant यह तरीका अपनाता है:

  1. मौजूदा डायरेक्ट्री में build.xml नाम की फ़ाइल लोड करता है और इसे पार्स करके, पहली इमेज में दिखाया गया ग्राफ़ स्ट्रक्चर बनाता है.
  2. कमांड लाइन पर दिए गए dist नाम के टास्क को ढूंढता है और यह पता चलता है कि यह compile नाम के टास्क पर निर्भर है.
  3. compile नाम का टास्क ढूंढता है और पता चलता है कि यह init नाम के टास्क पर निर्भर है.
  4. init नाम का टास्क खोजता है और पता चलता है कि उस पर कोई अन्य टास्क निर्भर नहीं है.
  5. init टास्क में बताए गए निर्देशों को लागू करता है.
  6. compile टास्क में तय किए गए निर्देशों को तब चलाता है, जब उस टास्क की सभी डिपेंडेंसी चलाई जा चुकी हों.
  7. dist टास्क में तय किए गए निर्देशों को तब चलाता है, जब उस टास्क की सभी डिपेंडेंसी चलाई जा चुकी हों.

आखिर में, dist टास्क को चलाते समय Ant के ज़रिए चलाया गया कोड, यहां दी गई शेल स्क्रिप्ट के बराबर होता है:

./createTimestamp.sh
mkdir 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 पर निर्भर करता है, जो टास्क C पर निर्भर करता है. टास्क C, आउटपुट के तौर पर एक खास फ़ाइल जनरेट करता है, जिसकी टास्क A को ज़रूरत होती है. टास्क B के मालिक ने तय किया कि उसे अब टास्क C पर निर्भर रहने की ज़रूरत नहीं है. इस वजह से, टास्क A पूरा नहीं हो पाता, भले ही टास्क B को टास्क C से कोई फ़र्क़ न पड़ता हो!
  • किसी नए टास्क के डेवलपर ने टास्क को चलाने वाली मशीन के बारे में गलती से कोई अनुमान लगाया हो. जैसे, किसी टूल की जगह या किसी खास एनवायरमेंट वैरिएबल की वैल्यू. टास्क उनकी मशीन पर काम करता है, लेकिन जब भी कोई दूसरा डेवलपर उसे आज़माता है, तो फ़ेल हो जाता है.
  • टास्क में ऐसा कॉम्पोनेंट शामिल होता है जिसकी समयावधि तय नहीं होती. जैसे, इंटरनेट से कोई फ़ाइल डाउनलोड करना या किसी बिल्ड में टाइमस्टैंप जोड़ना. अब, जब भी लोग बिल्ड चलाते हैं, तो उन्हें अलग-अलग नतीजे मिल सकते हैं. इसका मतलब है कि इंजीनियर हमेशा एक-दूसरे की गड़बड़ियों को दोहराकर ठीक नहीं कर पाएंगे. इसके अलावा, ऑटोमेटेड बिल्ड सिस्टम में होने वाली गड़बड़ियों को भी ठीक नहीं कर पाएंगे.
  • एक से ज़्यादा डिपेंडेंसी वाले टास्क, रेस कंडीशन बना सकते हैं. अगर टास्क A, टास्क B और टास्क C, दोनों पर निर्भर करता है और टास्क B और C, दोनों एक ही फ़ाइल में बदलाव करते हैं, तो टास्क A का नतीजा अलग-अलग होगा. यह इस बात पर निर्भर करेगा कि टास्क B और C में से कौनसा टास्क पहले पूरा होता है.

यहां बताए गए टास्क-आधारित फ़्रेमवर्क में, परफ़ॉर्मेंस, सही होने या मैनेज करने से जुड़ी इन समस्याओं को हल करने का कोई सामान्य तरीका नहीं है. जब तक इंजीनियर बिल्ड के दौरान चलने वाले आर्बिट्रेरी कोड लिख सकते हैं, तब तक सिस्टम में बिल्ड को तुरंत और सही तरीके से चलाने के लिए ज़रूरी जानकारी मौजूद नहीं होती. इस समस्या को हल करने के लिए, हमें इंजीनियर से कुछ लेना होगा और इसे सिस्टम के हाथों में वापस लाना होगा. साथ ही, सिस्टम की भूमिका को फिर से तय करना होगा कि काम चल रहे टास्क नहीं, बल्कि आर्टफ़ैक्ट तैयार करना है.

इस तरीके से, आर्टफ़ैक्ट पर आधारित बिल्ड सिस्टम बनाए गए. जैसे, Blaze और Bazel.