בונים תוכניות עם Bazel

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

מדריך למתחילים

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

bazel help
                             [Bazel release bazel version]
Usage: bazel command options ...

הפקודות הזמינות

  • analyze-profile: מתבצע ניתוח של נתוני הפרופיל.
  • aquery: ביצוע שאילתה בתרשים הניתוח שלאחר הניתוח.
  • build: בניית היעדים שצוינו.
  • canonicalize-flags: קנונים של דגלי בזל.
  • clean: מסיר קובצי פלט ומפסיק את השרת.
  • cquery: מפעיל שאילתת post-analysis פרסום של תלות תלות.
  • dump: מחזיר את המצב הפנימי של תהליך השרת של Bazel.
  • help: מדפיסים הדפסות בפקודות או באינדקס.
  • info: הצגת פרטים על זמן הריצה של שרת הבסיס.
  • fetch: אחזור של כל התלות החיצוניות של יעד.
  • mobile-install: התקנת אפליקציות במכשירים ניידים.
  • query: ביצוע שאילתת תרשים תלות.
  • run: מפעיל את היעד שצוין.
  • shutdown: הפסקת השרת Bazel.
  • test: בנייה וריצה של יעדי הבדיקה שצוינו.
  • version: הדפסה של פרטי הגרסה של Bazel.

קבלת עזרה

  • bazel help command: עזרה בהדפסה ואפשרויות להדפסה עבור command.
  • bazel helpstartup_options: אפשרויות עבור JVM המארחים את Bazel.
  • bazel helptarget-syntax: הסבר על התחביר לציון יעדים.
  • bazel help info-keys: הצגת רשימה של מפתחות שמשתמשים בפקודת המידע.

הכלי bazel מבצע פונקציות רבות שנקראות פקודות. הסיבות הנפוצות ביותר הן bazel build וbazel test. תוכלו לעיין בהודעות העזרה אונליין באמצעות bazel help.

בניית יעד אחד

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

כדי ליצור תוכנית ב-Bazel, מקלידים bazel build ולאחר מכן מזינים את היעד שרוצים ליצור.

bazel build //foo

לאחר הרצת הפקודה ליצירת //foo, תראו פלט דומה לזה:

INFO: Analyzed target //foo:foo (14 packages loaded, 48 targets configured).
INFO: Found 1 target...
Target //foo:foo up-to-date:
  bazel-bin/foo/foo
INFO: Elapsed time: 9.905s, Critical Path: 3.25s
INFO: Build completed successfully, 6 total actions

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

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

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

אם מקלידים את אותה פקודה שוב, ה-build מסתיים הרבה יותר מהר.

bazel build //foo
INFO: Analyzed target //foo:foo (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //foo:foo up-to-date:
  bazel-bin/foo/foo
INFO: Elapsed time: 0.144s, Critical Path: 0.00s
INFO: Build completed successfully, 1 total action

build null. מכיוון ששום דבר לא השתנה, אין חבילות לטעינה מחדש ואין צורך לבצע פעולות build. אם משהו השתנה ב 'foo' או בתלויותיו, Bazel תבצע מחדש פעולות build מסוימות או תשלים גרסה מצטברת.

בניית יעדים מרובים

Bazel מאפשר לבנות מספר יעדים. ביחד, הם נקראים תבניות יעד. תחביר כזה משמש בפקודות כמו build, test או query.

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

כל דפוסי היעד שמתחילים ב-// נפתרים ביחס לסביבת העבודה הנוכחית.

//foo/bar:wiz רק היעד היחיד: //foo/bar:wiz.
//foo/bar שווה ערך ל-//foo/bar:bar.
//foo/bar:all כל יעדי הכלל בחבילה foo/bar.
//foo/... כל יעדי הכללים בכל החבילות שמתחת לספרייה foo.
//foo/...:all כל יעדי הכללים בכל החבילות שמתחת לספרייה foo.
//foo/...:* כל היעדים (כללים וקבצים) בכל החבילות מתחת לספרייה foo.
//foo/...:all-targets כל היעדים (כללים וקבצים) בכל החבילות מתחת לספרייה foo.
//... כל היעדים בחבילות של סביבת העבודה. זה לא כולל יעדים ממאגרים חיצוניים.
//:all כל היעדים בחבילה ברמה העליונה, אם קיים קובץ 'BUILD' בשורש העבודה.

דפוסי יעד שלא מתחילים ב-// נפתרים ביחס לספריית העבודה הנוכחית. הדוגמאות הבאות מבוססות על ספריית עבודה פעילה של foo:

:foo שווה ערך ל-//foo:foo.
bar:wiz שווה ערך ל-//foo/bar:wiz.
bar/wiz שווה ערך ל:
  • //foo/bar/wiz:wiz אם foo/bar/wiz היא חבילה
  • //foo/bar:wiz אם foo/bar היא חבילה
  • //foo:bar/wiz אחרת
bar:all שווה ערך ל-//foo/bar:all.
:all שווה ערך ל-//foo:all.
...:all שווה ערך ל-//foo/...:all.
... שווה ערך ל-//foo/...:all.
bar/...:all שווה ערך ל-//foo/bar/...:all.

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

בנוסף, Bazel לא עוקבת אחר קישורים סימבוליים להערכת דפוסי יעד חוזרים בכל ספרייה שמכילה קובץ בשם: DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN

foo/... הוא תו כללי לחיפוש מעל חבילות, המציין את כל החבילות הנמצאות מתחת לספרייה הבאה foo (לכל השורשים של נתיב החבילה). :all הוא תו כללי לחיפוש מעל יעדים, שתואם לכל הכללים בחבילה. אפשר לשלב את שתי העמודות האלה, כמו ב-foo/...:all, וכשמשתמשים בשני התווים הכלליים לחיפוש, יכול להיות שהקיצור הזה יהיה foo/....

בנוסף, :* (או :all-targets) הוא תו כללי לחיפוש שתואם לכל יעד בחבילות התואמות, כולל קבצים שלרוב לא נוצרים באמצעות כלל, כמו _deploy.jar קבצים המשויכים לכללי java_binary.

פירוש הדבר הוא ש-:* מציין קבוצת ערכי-על של :all. אף על פי שהתחביר עשוי להיות מבלבל, אפשר להשתמש בתו הכללי המוכר :all כדי ליצור גרסאות אופייניות, שבהן לא רוצים ליצור יעדי בנייה כמו _deploy.jar.

בנוסף, Bazel מאפשרת להשתמש בקו נטוי במקום בנקודתיים באמצעות תחביר התווית. הרבה פעמים זה נוח כשמשתמשים בסיומת שם ה-Bash. לדוגמה, המאפיין foo/bar/wiz שווה ל-//foo/bar:wiz (אם יש חבילה foo/bar) או ל-//foo:bar/wiz (אם קיימת חבילה foo).

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

bazel build foo/... bar/...

פירוש הדבר "בניית כל היעדים מתחת לfoo וגם כל היעדים מתחת לbar"בעוד

bazel build -- foo/... -foo/bar/...

פירוש הדבר הוא &מירכאות; יש לבנות את כל היעדים מתחת ל-foo מלבד הפלחים שמתחת ל-foo/bar". (הארגומנט -- נדרש כדי למנוע את הארגומנטים הבאים שמתחילים ב-- כאפשרויות נוספות).

חשוב לציין שהפחתה של יעדים בשיטה הזו לא מבטיחה שהם לא יהיו מובְנים, כי ייתכן שהם תלויים ביעדים שלא הופחתו. לדוגמה, אם יש מטרה עסקית //foo:all-apis שתלויה בחברות אחרות ב//foo/bar:api, היא תבנה כחלק מהבניין הראשון.

יעדים עם tags = ["manual"] אינם נכללים בתבניות יעד של תווים כלליים לחיפוש (..., :*, :all וכו') כשמציינים אותם בפקודות כמו bazel build ו-bazel test. יש לציין יעדי בדיקה כאלה עם תבניות יעד מפורשות בשורת הפקודה אם רוצים ש-Bazel תיצור או תבדוק אותם. לעומת זאת, bazel query לא מבצע סינון כזה באופן אוטומטי (שעשויות לפגוע המטרה של bazel query).

אחזור יחסי תלות חיצוניים

כברירת מחדל, Bazel תוריד ותקשר גם קשרי תלות חיצוניים במהלך ההגדרה. עם זאת, זה יכול להיות לא רצוי, כי אתם רוצים לדעת מתי מתווספות יחסי תלות חיצוניים חדשים או אם רוצים לעשות זאת, לפני שעוברים להשתמש במצב אופליין. אם תרצו למנוע הוספה של יחסי תלות חדשים במהלך יצירת ה-build, תוכלו לציין את הדגל --fetch=false. שימו לב שסימון זה חל רק על כללי מאגר נתונים שאינם מצביעים על ספרייה במערכת הקבצים המקומית. שינויים, למשל, ב-local_repository, new_local_repository ובכללי Android SDK וכללי מאגר ה-NDK יכנסו לתוקף תמיד ללא קשר לערך --fetch .

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

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

  • לפני שמתחילים בהגדרה.
  • לאחר הוספת תלות חיצונית חדשה.

לאחר הפעלת הקובץ, אין צורך להפעיל אותו שוב עד שקובץ WORKSPACE ישתנה.

ב-fetch מוצגת רשימה של יעדים שיש לשלוף עבורם יחסי תלות. לדוגמה, פעולה זו תאחזר יחסי תלות הנדרשים כדי לבנות את //foo:bar ואת //bar:baz:

bazel fetch //foo:bar //bar:baz

כדי לאחזר את כל התלות החיצוניות של סביבת עבודה, יש להריץ:

bazel fetch //...

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

מטמון המאגר

Bazel מנסה להימנע מאחזור של אותו קובץ מספר פעמים, גם אם יש צורך באותו קובץ בסביבות עבודה שונות, או אם ההגדרה של מאגר חיצוני השתנתה, אבל הוא עדיין צריך להוריד את אותו הקובץ. לשם כך, מאחר ש-Bazel שומר במטמון את כל הקבצים שהורדו במטמון המאגר, כברירת מחדל, הוא ממוקם ב-~/.cache/bazel/_bazel_$USER/cache/repos/v1/. ניתן לשנות את המיקום באמצעות האפשרות --repository_cache. המטמון משותף לכל סביבות העבודה והגרסאות המותקנות של Bazel. אם ל-Bazel יודעים שיש עותק של הקובץ הנכון, היא מקבלת ערך גם אם בקשת ההורדה כוללת סכום SHA256 של הקובץ שצוין וקובץ עם הגיבוב הזה נמצא במטמון. לכן, גיבוב (hash) של כל קובץ חיצוני הוא לא רק רעיון טוב מנקודת אבטחה, אלא גם עוזר להימנע מהורדות מיותרות.

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

ספריות קובצי הפצה

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

באפשרות --distdir=/path/to-directory אפשר לציין ספריות נוספות לקריאה בלבד כדי לחפש קבצים במקום לאחזר אותן. קובץ נלקח מהספרייה כזו אם שם הקובץ זהה לשם הבסיס של כתובת האתר, וגם הגיבוב של הקובץ זהה לזה שצוין בבקשת ההורדה. האפשרות הזו פועלת רק אם גיבוב (hash) של הקובץ צוין בהצהרת WORKSPACE.

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

ריצה של Bazel בסביבה עם מרווח אווירי

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

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

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

כדי לבנות את יחסי התלות האלה מחוץ לסביבת האוויר שלכם, קודם כול בודקים את עץ המקור של Bazel בגרסה הנכונה:

git clone https://github.com/bazelbuild/bazel "$BAZEL_DIR"
cd "$BAZEL_DIR"
git checkout "$BAZEL_VERSION"

לאחר מכן, בונים את ה-tarball שמכיל את התלויות המשתמעות של זמן הריצה עבור גרסת Bazel הספציפית הזו:

bazel build @additional_distfiles//:archives.tar

ייצאו את ה-Tarball הזה לספרייה שאפשר להעתיק לסביבה שלכם עם חללים משותפים. שימו לב לסימון --strip-components, כי --distdir יכול להיות מסובך מאוד עם רמת הדירוג של הספרייה:

tar xvf bazel-bin/external/additional_distfiles/archives.tar \
  -C "$NEW_DIRECTORY" --strip-components=3

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

build --distdir=path/to/directory

יצירת תצורות ואיסוף חוצה

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

בכל מבנה build נתון, יכולה להיות יותר מהגדרה אחת. כדאי לשקול שילוב בין מערכות, שבו אפשר ליצור קובץ הפעלה מסוג //foo:bin עבור ארכיטקטורה של 64 ביט, אבל תחנת העבודה שלכם היא מכונה בגרסת 32-bit. ברור שה-build מחייב בניין //foo:bin באמצעות כלי עבודה שיכול ליצור קובצי הפעלה של 64 ביט, אבל מערכת ה-build צריכה גם לבנות כלים שונים המשמשים לבנייה עצמה. לדוגמה, כלים שמבוססים על מקור, שאז משתמשים בהם לצורך שחיקה — וצריך לבנות אותם כך שיפעלו בתחנת העבודה שלכם. כך אנחנו יכולים לזהות שתי תצורות: תצורת המארח, המשמשת ליצירת כלים שרצים במהלך ה-build, ותצורת היעד (או בקשת תצורה, אבל אנחנו אומרים "target Configuration" ובתדירות גבוהה יותר, למרות שלמילה הזו כבר יש משמעויות רבות), המשמשות לבנייה של הקובץ הבינארי שביקשת בסופו של דבר.

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

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

--distinct_host_configuration=false

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

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

--distinct_host_configuration=true (ברירת מחדל)

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

  • צריך להשתמש באותה גרסה של Crosstool (--crosstool_top) שצוינה בתצורת הבקשה, אלא אם צוין --host_crosstool_top.
  • שימוש בערך --host_cpu של --cpu (ברירת המחדל: k8).
  • צריך להשתמש באותם ערכים של האפשרויות האלה:
  • שימוש בערך --host_javabase עבור --javabase
  • שימוש בערך --host_java_toolchain עבור --java_toolchain
  • יש להשתמש ב-build עם אופטימיזציה ל-C++ (-c opt).
  • לא ליצור מידע על ניפוי באגים (--copt=-g0).
  • יש להסיר מידע על תוצאות ניפוי הבאגים מקובצי הפעלה וספריות משותפות (--strip=always).
  • מציבים את כל הקבצים הנגזרים במיקום מיוחד, שונה מזה שמשמש את כל הגדרות הבקשה האפשריות.
  • הסתרת חותמות של קבצים בינאריים עם נתוני build (מידע על אפשרויות --embed_*).
  • כל שאר הערכים יישארו בברירות המחדל שלהם.

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

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

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

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

בניית נכון של גרסאות מצטברות

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

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

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

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

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

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

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

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

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

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

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

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

אם זיהית מצב לא עקבי ב-Bazel, דיווח על באג.

הפעלה בארגז חול

Bazel משתמשת בארגזי חול כדי להבטיח שהפעולות יפעלו בצורה הרמטית ובצורה נכונה. Bazel מריץ spens (באופן רופף: פעולות) בארגזי חול שכוללים רק את קבוצת הקבצים המינימלית שנדרשת לכלי כדי לבצע את עבודתו. בשלב זה, ארגז החול פועל ב-Linux 3.12 ומעלה, עם אפשרות CONFIG_USER_NS וב-macOS מגרסה 10.11 ואילך.

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

בפלטפורמות מסוימות, כמו צומתי אשכול של Google Kubernetes Engine או Debian, מרחבי השמות של המשתמשים מושבתים כברירת מחדל בשל בעיות אבטחה. אפשר לבדוק זאת בקובץ /proc/sys/kernel/unprivileged_userns_clone: אם הוא קיים והקובץ מכיל 0, אפשר להפעיל את מרחבי השמות של משתמשים עם sudo sysctl kernel.unprivileged_userns_clone=1.

במקרים מסוימים, ארגז החול של Bazel לא מבצע כללים עקב הגדרת המערכת. בדרך כלל, התסמין הוא פלט של הודעה שדומה ל-namespace-sandbox.c:633: execvp(argv[0], argv): No such file or directory. במקרה כזה, כדאי לנסות להשבית את ארגז החול עבור כללי יצירת זכויות יוצרים באמצעות --strategy=Genrule=standalone ועבור כללים אחרים עם --spawn_strategy=standalone. בנוסף, דווחו על באג בכלי המעקב אחר בעיות וציינו באילו הפצה אתם משתמשים ב-Linux, כדי שנוכל לחקור את הבעיה ולספק פתרון בגרסה הבאה שלכם.

שלבים של גרסת build

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

השלב בטעינה

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

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

שגיאות שדווחו בשלב זה כוללות: החבילה לא נמצאה, היעד לא נמצא, שגיאות דקדוק ושגיאות בקובץ BUILD ושגיאות הערכה.

שלב הניתוח

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

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

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

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

שלב ביצוע

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