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

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

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

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