ניהול תלות

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

טיפול במודולים וב תלות

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

שימוש במודולים מפורטים וכלל 1:1

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

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

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

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

חלק מהכלים האלה, כמו buildifier ו-buildozer, זמינים ב-Bazel בספרייהbuildtools.

צמצום החשיפה של המודול

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

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

ניהול יחסי תלות

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

יחסי תלות פנימיים

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

יחסי תלות זמניים

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

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

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

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

יחסי תלות חיצוניים

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

ניהול תלות אוטומטי לעומת ידני

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

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

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

הכלל של גרסה אחת

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

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

יחסי תלות חיצוניים חולפים

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

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

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

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

שמירה של תוצאות build במטמון באמצעות יחסי תלות חיצוניים

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

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

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

אבטחה ואמינות של יחסי תלות חיצוניים

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