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

इस पेज पर, टास्क-आधारित बिल्ड सिस्टम के बारे में बताया गया है. साथ ही, यह भी बताया गया है कि ये सिस्टम कैसे काम करते हैं और इनमें कौनसी समस्याएं आ सकती हैं. शेल स्क्रिप्ट के बाद, टास्क-आधारित बिल्ड सिस्टम, बिल्ड करने का अगला लॉजिकल तरीका है.

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

टास्क-आधारित बिल्ड सिस्टम में, काम की बुनियादी यूनिट टास्क होती है. हर टास्क एक स्क्रिप्ट होती है, जो किसी भी तरह का लॉजिक लागू कर सकती है. साथ ही, टास्क, अन्य टास्क को डिपेंडेंसी के तौर पर तय करते हैं. इन्हें, टास्क से पहले रन करना ज़रूरी होता है. आज-कल इस्तेमाल किए जा रहे ज़्यादातर बड़े बिल्ड सिस्टम, टास्क-आधारित होते हैं. जैसे, 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.