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

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

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

במערכת build מבוססת משימות, יחידת העבודה הבסיסית היא המשימה. כל משימה היא סקריפט שיכול לבצע כל היגיון, ומשימות מציינות משימות אחרות כתלות שחייבות לפעול לפניהן. רוב מערכות הבנייה העיקריות שבהן נעשה שימוש כיום, כגון נמלים, Maven, גרדל, גרונט ורייק, מבוססות על משימות. במקום סקריפטים של מעטפת, רוב מערכות ה-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). נמלה משתמשת במילה יעד לייצג אתמשימה, והיא משתמשת במילהמשימה להתייחס אליהן פקודות ). כל משימה מבצעת רשימה של פקודות אפשריות שהוגדרו על ידי Ant, הכוללות כאן יצירה ומחיקה של ספריות, הפעלת javac ויצירת קובץ JAR. ניתן להרחיב את קבוצת הפקודות הזו על ידי יישומי פלאגין שמספק המשתמש, כדי לכסות כל סוג של לוגיקה. כל משימה יכולה גם להגדיר אילו משימות הן תלויות דרך המאפיין 'תלוי'. יחסי התלות האלה יוצרים תרשים מחזורי כפי שניתן לראות באיור 1.

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

איור 1. תרשים מחזורי שמראה יחסי תלות

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

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

בסופו של דבר, הקוד שמבוצע על ידי נמלה בזמן הרצת המשימה 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, והוא קובע את כל מה שיש להריץ.

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

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

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

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

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

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

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

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

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

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

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

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