מערכות בנייה מבוססות משימות

הדף הזה עוסק במערכות בנייה מבוססות-משימות, איך הם עובדים וכמה סיבוכים שעלולים להתרחש במערכות מבוססות-משימות. לאחר שימוש בסקריפטים של צדפות, מערכות הבנייה מבוססות-המשימות הן ההתפתחות הלוגית הבאה של הבנייה.

הבנת מערכות build מבוססות משימות

במערכת בנייה מבוססת משימות, יחידת העבודה הבסיסית היא המשימה. כל משימה היא סקריפט שיכול להפעיל כל סוג של לוגיקה, והמשימות מכילות משימות אחרות שתלויות בהן. רוב מערכות הבנייה העיקריות שבהן משתמשים כיום, כגון נמלה, מייבן, גראדה, רוטב ופריקה, מבוססות על משימות. במקום להשתמש בסקריפטים של מעטפת, רוב מערכות ה-build המודרניות מחייבות מהנדסי תוכנה ליצור קובצי build שמתארים את אופן ביצוע ה-build.

בצעו את הדוגמה הבאה במדריך ידני של נמלים:

<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>

קובץ ה-build נכתב ב-XML ומגדיר מטא-נתונים פשוטים לגבי ה-build יחד עם רשימת משימות (תגי <target> ב-XML). (המילה 'יעד' משתמשת במונח יעד כדי לייצג משימה, והמילה 'משימה' מתייחסת למשימה). כל משימה מבצעת רשימה של פקודות אפשריות שהוגדרו על ידי Ants, כולל יצירה ומחיקה של ספריות, הפעלת javac ויצירה של קובץ JAR. ניתן להרחיב את קבוצת הפקודות הזו על ידי יישומי פלאגין שסופקו על ידי המשתמשים, כך שהיא תכסה כל סוג לוגיקה. בכל משימה אפשר גם להגדיר את המשימות שהיא תלויה בהן דרך המאפיין התלוי. יחסי תלות אלה יוצרים גרף אציקלי, כפי שמתואר באיור 1.

תרשים אקריליק שמראה יחסי תלות

איור 1. תרשים אציקלי המציג את יחסי תלות

המשתמשים יכולים לבצע גרסאות build על ידי מתן משימות לכלי שורת הפקודה של אנט. לדוגמה, כשמשתמש מקליד ant dist, אנט צריך לבצע את הפעולות הבאות:

  1. טוען קובץ בשם build.xml בספרייה הנוכחית ומנתח אותו כדי ליצור את מבנה התרשים המוצג באיור 1.
  2. מתבצע חיפוש של המשימה בשם dist שצוינה בשורת הפקודה, ומגלה שיש לה תלות במשימה בשם compile.
  3. מתבצע חיפוש של המשימה בשם compile ומגלה שיש לה תלות במשימה בשם init.
  4. מתבצע חיפוש של המשימה בשם init ומגלה שאין לה יחסי תלות.
  5. ביצוע הפקודות שהוגדרו במשימה init.
  6. מבצע את הפקודות שהוגדרו במשימה compile, בהינתן שכל התלויות במשימה הזו בוצעו.
  7. מבצע את הפקודות שהוגדרו במשימה dist, בהינתן שכל התלויות במשימה הזו בוצעו.

בסופו של דבר, הקוד שהופעל על ידי Ant במהלך הרצת המשימה dist הוא זהה לסקריפט המעטפת הבא:

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

כשהתחביר מוסר, קובץ ה-build וסקריפט ה-build למעשה לא שונים מדי. עם זאת, כבר השקענו הרבה בתחום הזה. אנחנו יכולים ליצור קובצי build חדשים בספריות אחרות ולקשר אותם יחד. אנחנו יכולים להוסיף בקלות משימות חדשות שתלויות במשימות קיימות בדרכים שרירותיות ומורכבות. אנחנו צריכים להעביר את השם של משימה אחת בלבד לכלי שורת הפקודה ant, והיא קובעת את כל מה שרוצים להריץ.

Ant הוא קטע תוכנה ישן שהושק לראשונה בשנת 2000. כלים אחרים כמו Maven ו-Grade השתפרו בשנים הבאות, ולמעשה החליפו אותם על ידי הוספת תכונות כמו ניהול אוטומטי של יחסי תלות חיצוניים ותחביר נקי יותר ללא XML. אבל האופי של המערכות החדשות האלה לא משתנה: הם מאפשרים למהנדסים לכתוב סקריפטים באופן מהותי ומודולרי כמשימות, ומספקים כלים לביצוע המשימות האלו וניהול יחסי תלות שלהן.

הצד האפל של מערכות ה-build המבוססות על משימות

מאחר שהכלים האלה מאפשרים למעשה למהנדסים להגדיר סקריפט כלשהו כמשימה, הם עוצמתיים מאוד, ומאפשרים לכם לעשות כמעט כל דבר שאפשר לדמיין איתם. עם זאת, לכוח הזה חסרונות ויכולות להיות בעיות בעבודה עם מערכות build מבוססות-משימות, שהמורכבות שלהן הולכת וגדלה. הבעיה במערכות כאלה היא שבסופו של דבר הן נותנות יותר מדי עוצמה למהנדסים ולא מספיק כוח למערכת. מכיוון שהמערכת לא מבינה מה קורה בסקריפטים, היא משפיעה על הביצועים כי היא חייבת להיות שמרנית מאוד מבחינת התזמון שלה והפעולות שהיא מבצעת. ולא קיימת דרך שבה המערכת יכולה לאשר שכל סקריפט עושה את מה שהוא צריך, כך שסקריפטים נוטים לגדול במורכבות ולהיות בסופו של דבר דבר אחר הצריך ניפוי באגים.

קשיים בביצוע מקבילות של שלבי build

לתחנות עבודה של פיתוח מודרני יש עוצמה רבה, עם מספר ליבות שאפשר לבצע כמה שלבים בו-זמנית. אבל במקרים רבים, מערכות מבוססות-משימות לא יכולות לבצע מקבילות של ביצוע, גם אם נראה שהן צריכות להספיק. נניח שמשימה א' תלויה במשימות ב' ו-ג'. מכיוון שמשימות ב' ו-ג' אינן תלויות זו בזו, האם בטוח להריץ אותן בו-זמנית כדי שהמערכת תוכל להגיע למשימה א' מהר יותר? אולי הם לא יגעו באותם משאבים. אבל לא – אולי שני הקבצים ישתמשו באותו קובץ כדי לעקוב אחרי הסטטוסים שלהם ולהפעיל אותם בו-זמנית, כדי למנוע התנגשות. בדרך כלל, המערכת לא יכולה לדעת מהן הסיכונים, לכן היא חייבת להסתכן בהתנגשויות אלה (ומובילות לבעיות נדירות, אבל קשה מאוד, לניפוי באגים), או שהן צריכות להגביל את כל ה-build כדי לעבוד על שרשור יחיד בתהליך יחיד. זה יכול להיות בזבוז ענק של מכונה פיתוחית חזקה, והיא מונעת לחלוטין את האפשרות להפיץ את המכונה במספר מכונות.

קושי בביצוע גרסאות מצטברות

מערכת build טובה מאפשרת למהנדסים לבצע גרסאות build אמינות, כך שבעקבות שינוי קטן לא צריך ליצור מחדש את בסיס הקוד מאפס. זה חשוב במיוחד אם מערכת ה-build איטית ולא ניתן ליצור במקביל שלבי build מהסיבות שצוינו למעלה. אבל למרבה הצער, גם כאן מערכות מורכבות המבוססות על משימות לא מקבלות מענה. מכיוון שהמשימות יכולות לעשות כל דבר, אין דרך כללית לבדוק אם הן כבר בוצעו. משימות רבות פשוט דורשות קבוצה של קובצי מקור ומפעילים מהדר, כדי ליצור קבוצה של קבצים בינאריים. לכן אין צורך להריץ אותם מחדש אם קובצי המקור לא השתנו. אבל, ללא מידע נוסף, המערכת לא יכולה לומר זאת בוודאות, ייתכן שהמשימה מורידה קובץ שיכול היה להשתנות, או שכתוב חותמת זמן שעלולה להיות שונה בכל הרצה. בדרך כלל, כדי להבטיח את התקינות, המערכת בדרך כלל צריכה להריץ מחדש כל משימה במהלך כל build. יש מערכות בנייה שמנסות להפעיל גרסאות build מצטברות, בכך שהן מאפשרות למהנדסים לציין את התנאים שבהם יש להריץ משימה מחדש. לפעמים זה אפשרי, אבל לעיתים קרובות זו בעיה מורכבת יותר ממה שהיא מופיעה. לדוגמה, בשפות כמו C++ שאפשר להוסיף קבצים ישירות לקבצים אחרים, אי אפשר לקבוע את כל קבוצת הקבצים שצריך לצפות בהם כדי לבצע שינויים בלי לנתח את מקורות הקלט. לעיתים קרובות מהנדסים משתמשים בקיצורי דרך, וקיצורי הדרך האלה עלולים לגרום לבעיות נדירות ומתסכלות, כשצריך לעשות זאת גם כשאין צורך לעשות זאת. במקרים כאלה, המהנדסים מתרגלים לבצע תחזוקה שוטפת לפני כל בנייה כדי לקבל מצב נקי, כשהם מפסידים לחלוטין את המטרה של יצירת גרסאות מצטברות. הבנתי מתי צריך להריץ משימה מחדש באופן מפתיע, והיא עבודה טובה יותר ממכונות מאשר בני אדם.

קשיים בתחזוקה ובניפוי באגים בסקריפטים

בסופו של דבר, קשה יותר לעבוד עם הסקריפטים ל-build שהוגדרו על ידי מערכות build מבוססות משימות. לעיתים קרובות, הם מקבלים בחינה קפדנית יותר, אך סקריפטים של build הם קוד בדיוק כמו שאנחנו בונים את המערכת, והם מקומות נסתרים לבאגים להסתרה. הנה כמה דוגמאות לבאגים נפוצים מאוד בעבודה עם מערכת build מבוססת משימות:

  • משימה א' תלויה במשימה ב' כדי ליצור קובץ מסוים כפלט. הבעלים של המשימה ב' לא מבינה שמשימות אחרות מסתמכות עליה, כך שהיא משנה את הפלט במיקום אחר. לא תוכלו לזהות את הבעיה עד שמישהו ינסה להריץ את משימה א' ויגלה שהיא נכשלה.
  • משימה א' תלויה במשימה ב'. משימה זו תלויה במשימה ג'. היא מייצרת קובץ מסוים כפלט שנדרש במשימה א'. הבעלים של משימה ב' מחליטה שהיא לא צריכה יותר להיות תלויה במשימה ג', וכתוצאה מכך משימה א' תיכשל למרות שמשימה ב' לא משנה כלל למשימה ג'.
  • המפתח של משימה חדשה יוצא בטעות בטעות, כמו המחשב שמפעיל את המשימה, למשל, המיקום של כלי או הערך של משתני סביבה מסוימים. המשימה פועלת במחשב שלהם, אך היא נכשלת בכל פעם שמפתח אחר מנסה אותה.
  • משימה מכילה רכיב לא קבוע, כמו הורדת קובץ מהאינטרנט או הוספת חותמת זמן לבנייה. כיום, אנשים עשויים לקבל תוצאות שונות בכל פעם שהם מריצים את גרסת ה-build, כך שמהנדסים לא תמיד יוכלו לשחזר ולתקן כשלים או כשלים שמתרחשים במערכת האוטומטית.
  • משימות עם מספר יחסי תלות יכולות ליצור תנאים למרוץ. אם משימה א' תלויה גם במשימה ב' וגם במשימה ג' והמשימה ב' ו-ג' משנות את אותו הקובץ, משימה א' תקבל תוצאה שונה בהתאם למשימה הראשונה ב' ולמשימה ג'.

אין דרך כללית לפתור את הבעיות האלה, בנוגע לנכונות או לקיימוּת, בתוך המסגרת מבוססת המשימות שפירטנו כאן. כל עוד המהנדסים יכולים לכתוב קוד שרירותי שפועל במהלך ה-build, לא יכולה להיות למערכת מספיק מידע כדי שתמיד תהיה לה אפשרות להריץ גרסאות build במהירות ובצורה נכונה. כדי לפתור את הבעיה, עלינו להסיר את הידיים מהמהנדסים ולהחזיר אותו לידי המערכת, וליצור מחדש את תפקיד המערכת לא בתור משימות שפועלות, אלא כדי לייצר פריטים.

הגישה הזו הובילה ליצירת מערכות build המבוססות על פריטי מידע שנוצרים בתהליך פיתוח, כמו Blaze ו-Bazel.