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

इस पेज पर, टास्क-आधारित बिल्ड सिस्टम, उनके काम करने के तरीके, और उनसे जुड़ी कुछ समस्याओं के बारे में बताया गया है. शेल स्क्रिप्ट के बाद, टास्क-आधारित बिल्ड सिस्टम, बिल्ड करने के तरीके का अगला चरण है.

टास्क-आधारित बिल्ड सिस्टम के बारे में जानकारी

टास्क-आधारित बिल्ड सिस्टम में, काम की बुनियादी यूनिट टास्क होती है. हर टास्क एक स्क्रिप्ट होती है, जो किसी भी तरह की लॉजिक को लागू कर सकती है. साथ ही, टास्क, अन्य टास्क को डिपेंडेंसी के तौर पर तय करते हैं, जिन्हें उनसे पहले रन करना ज़रूरी होता है. आज-कल इस्तेमाल किए जाने वाले ज़्यादातर बड़े बिल्ड सिस्टम, जैसे कि Ant, Maven, Gradle, Grunt, और Rake, टास्क-आधारित हैं. शेल स्क्रिप्ट के बजाय, ज़्यादातर आधुनिक बिल्ड सिस्टम के लिए, इंजीनियरों को बिल्ड फ़ाइलें बनानी होती हैं. इनमें यह जानकारी होती है कि बिल्ड कैसे किया जाए.

Ant के मैन्युअल से यह उदाहरण देखें: Ant manual:

<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 फ़ाइल बनाना शामिल है. कमांड के इस सेट को, उपयोगकर्ता की ओर से दिए गए प्लग-इन की मदद से बढ़ाया जा सकता है, ताकि किसी भी तरह की लॉजिक को लागू किया जा सके. हर टास्क, depends एट्रिब्यूट के ज़रिए, उन टास्क को भी तय कर सकता है जिन पर वह निर्भर करता है. ये डिपेंडेंसी, एक ऐसा ऐसाइक्लिक ग्राफ़ बनाती हैं जैसा कि इमेज 1 में दिखाया गया है.

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

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

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

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

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

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