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

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

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

נקודת מבט פונקציונלית

קל ליצור אנלוגיה בין מערכות פיתוח מבוססות חפצים לבין תכנות פונקציונלי. שפות תכנות מסורתיות (כמו Java , C ו-Python) מציינות רשימות של הצהרות לביצוע בזה אחר זה, באותו אופן שבו מערכות build מבוססות משימות מאפשרות למתכנתים להגדיר סדרה של צעדים{ 101}לביצוע. לעומת זאת, שפות תכנות במצבים פונקציונליים (כמו למשל האסל ו-ML), בנויות בצורה דומה יותר לסדרה של משוואות מתמטיות. בשפות מתפקדות, המתכנת מתאר חישוב שמבוסס לבצע, אך משאיר את הפרטים של המועד והאופן המדויק שבהם מתבצע חישוב זה עבור המהדר.

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

הסבר על מערכות build המבוססות על פריטי מידע שנוצרו בתהליך פיתוח (Artifact)

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

כך נראה קובץ build (שבדרך כלל נקרא BUILD) ב-Bazel:

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

ב-Bazel, BUILD קבצים מגדירים יעדים – שני סוגי היעדים כאן הם java_binary ו-java_library. כל יעד תואם למרכיב שנוצר בתהליך פיתוח (Artifact) שניתן ליצירה על ידי המערכת: יעדים בינאריים מייצרים בינאריים שניתן להריץ ישירות, וטירגוטים לספרייה מפיקים ספריות שניתן להשתמש בהן בספריות או בספריות אחרות. לכל יעד יש:

  • name: האופן שבו היעד מוזכר בשורת הפקודה וביעדים אחרים
  • srcs: קובצי המקור שנאספו כדי ליצור את פריט המידע שנוצר בתהליך פיתוח (Artifact) עבור היעד
  • deps: יעדים נוספים שיש לבנות לפני היעד הזה ולקשר אליו

סוגי תלות יכולים להיות באותה חבילה (כגון תלות של MyBinary ב-:mylib) או בחבילה אחרת באותה היררכיית מקור (כגון מידת התלות של mylib בתאריך //java/com/example/common).

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

  1. מנתח כל קובץ BUILD בסביבת העבודה כדי ליצור תרשים של יחסי תלות בין חפצים.
  2. משתמש בתרשים כדי לקבוע את יחסי התלות העקיפים של MyBinary; כלומר, כל יעד ש-MyBinary תלוי בו וכל יעד שהיעדים האלה תלויים בו, באופן רקוריתי.
  3. בונה כל אחת מהתלות האלה לפי הסדר. Bazel מתחילה בבניית כל יעד שאין לו יחסי תלות אחרים ועוקבת אחרי אילו יחסי תלות עדיין יש לבנות עבור כל יעד. לאחר בניית יחסי תלות של יעד מסוים, בזל מתחילה לבנות את היעד. התהליך הזה יימשך עד שייצרו כל יחסי התלות העקיפים של MyBinary.
  4. בונה את MyBinary כדי ליצור קובץ בינארי ניתן להפעלה המקשר בין כל יחסי התלות שנבנו בשלב 3.

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

עם זאת, היתרונות כוללים מעבר לקבילות. הדבר הבא, שמתבסס על הגישה הזו, מבהיר מתי המפתח מקליד bazel build :MyBinary פעם שנייה בלי לבצע שינויים: הנתונים. ייתכן שזה בגלל תפיסת התכנות הפונקציונליות שבה דיברנו קודם לכן – בזל יודעת שכל מטרה היא תוצאה של הפעלת מהדר של Java, והיא יודעת שהפלט של מהדר Java תלוי רק על הקלט, כל עוד הקלט לא השתנה, ניתן לעשות שימוש חוזר בפלט. והניתוח הזה פועל בכל רמה; אם MyBinary.java ישתנה, Bazel תדע לבנות מחדש את MyBinary אבל לעשות שימוש חוזר ב-mylib. אם משתנה קובץ מקור עבור //java/com/example/common, Bazel יודעת לבנות מחדש את הספרייה הזו, mylib ואת MyBinary, אבל לעשות שימוש חוזר ב-//java/com/example/myproduct/otherlib. משום שחברת Bazel מכירה את המאפיינים של הכלים שהיא מפעילה בכל שלב, כך היא יכולה לבנות מחדש רק את קבוצת המינימום של פריטי המידע שנוצרו בתהליך פיתוח, בכל פעם שהיא מבטיחה שלא ייווצרו גרסאות build לא מעודכנות.

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

תעלולים אחרים מבזל

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

כלים כתלות

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

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

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

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

הרחבת מערכת ה-build

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

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

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

בידוד הסביבה

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

קביעת תלות תלויה דטרמיניסטית

עדיין נותרה בעיה אחת: מערכות build צריכות לעתים קרובות להוריד תלות (בין אם כלים או ספריות) ממקורות חיצוניים, במקום לבנות אותם ישירות. ניתן לראות זאת בדוגמה דרך תלות @com_google_common_guava_guava//jar, שמורידה קובץ JAR מ-Maven.

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

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

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

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

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