בסיס הקוד של בזל

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

מבוא

בסיס הקוד של בזל גדול (~350KLOC ייצור קוד ו-~260 בשיטת KLOC) גבעות בכל כיוון.

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

הגרסה הציבורית של קוד המקור של Bazel חיה ב-GitHub בכתובת github.com/bazelbuild/bazel. זה אינו "מקור האמת"; הוא נגזר מעץ מקור פנימי של Google שמכיל פונקציונליות נוספת שאינה שימושית מחוץ ל-Google. המטרה בטווח הארוך היא להפוך את GitHub למקור האמת.

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

ארכיטקטורת לקוח/שרת

רוב של Bazel נמצא בתהליך של שרת שנשאר ב-RAM בין גרסאות build. פעולה זו מאפשרת ל-Bazel לשמור על המצב בין גרסאות ה-build.

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

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

חלק מהאפשרויות (--host_jvm_args=) מופיעות לפני שם הפקודה שיש להריץ, וחלקן מופיעות אחרי (-c opt); הסוג הראשון נקרא "אפשרות הפעלה" ומשפיע על תהליך השרת כולו, בעוד שהסוג השני, "אפשרות הפקודה", משפיע רק על פקודה אחת.

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

Bazel מחולק כקובץ הפעלה יחיד של ELF שהוא גם קובץ .zip חוקי. בעת הקלדת bazel, הרצת ה-ELF שלמעלה הוטמעה ב-C++ ("הלקוח") מקבלת שליטה. היא מגדירה תהליך שרת מתאים באמצעות השלבים הבאים:

  1. בודקת אם הקובץ כבר חולץ בעצמו. אם לא, אין שגיאה. מכאן מגיע יישום השרת.
  2. בודקת אם יש מופע פעיל של השרת שפועל: פועל, מכיל את אפשרויות ההפעלה הנכונות ומשתמש בספרייה של סביבת העבודה המתאימה. הוא מוצא את השרת הפועל על ידי עיון בספרייה $OUTPUT_BASE/server שבה קיים קובץ נעילה עם היציאה שעליה השרת מאזין.
  3. במקרה הצורך, מבטלים את תהליך השרת הישן
  4. במקרה הצורך, מתחיל תהליך חדש של שרת

כשתהליך שרת מתאים יהיה מוכן, הפקודה שיש להפעיל עוברת תקשורת באמצעות ממשק gRPC, ולאחר מכן הפלט של Bazel מוחזר לטרמינל. ניתן להפעיל רק פקודה אחת בו-זמנית. פעולה זו מיושמות באמצעות מנגנון נעילה מורכב שחלקים ב-C++ וחלקים בהם ב-Java. קיימת תשתית להפעלת פקודות מרובות במקביל, כי חוסר היכולת להריץ bazel version במקביל לפקודה אחרת מביך במידה מסוימת. החוסם העיקרי הוא מחזור החיים של BlazeModule ומדינה מסוימת בBlazeRuntime.

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

לאחר לחיצה על Ctrl-C, הלקוח מתרגם את השיחה לשיחת ביטול בחיבור gRPC, שמנסה לסיים את הפקודה בהקדם האפשרי. אחרי ה-Ctrl-C השלישי, הלקוח שולח את ה-SIGKILL לשרת במקום זאת.

קוד המקור של הלקוח הוא מתחת ל-src/main/cpp והפרוטוקול המשמש לתקשורת עם השרת הוא ב-src/main/protobuf/command_server.proto .

נקודת הכניסה הראשית של השרת היא BlazeRuntime.main() והקריאות ל-gRPC מהלקוח מטופלות על ידי GrpcServerImpl.run().

פריסת הספרייה

Bazel יוצר קבוצת ספריות מורכבת למדי במהלך גרסת build. תיאור מלא זמין בפריסה של ספריית הפלט.

"סביבת העבודה" היא עץ המקור שבו מופעל בזל. בדרך כלל היא תואמת למשהו שהסרתם מבקרת המקור.

Bazel מעבירה את כל הנתונים שלה ל "שורש משתמש פלט". בדרך כלל מדובר ב$HOME/.cache/bazel/_bazel_${USER}, אבל ניתן לשנות זאת בעזרת אפשרות ההפעלה --output_user_root.

"בסיס ההתקנות" הוא המקום שאליו נשלפת Bazel. התהליך מתבצע באופן אוטומטי, וכל גרסת Bazel מקבלת ספריית משנה המבוססת על סיכום הביקורת שלה מתחת לבסיס ההתקנה. הקובץ נמצא כברירת מחדל ב-$OUTPUT_USER_ROOT/install ואפשר לשנות אותו באמצעות אפשרות שורת הפקודה --install_base.

"בסיס פלט" הוא המקום שבו כתוב מופע בזל המצורף לסביבת עבודה ספציפית. כל בסיס פלט פועל בכל עת לכל היותר מופע אחד של שרת בזל. המחיר הרגיל הוא בדרך כלל בשעה $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. ניתן לשנות אותה על ידי שימוש באפשרות ההפעלה של --output_base, הכוללת, בין היתר, התחדשות לגבי המגבלה, שרק מופע אחד של בזל יכול לפעול בכל סביבת עבודה בכל רגע נתון.

ספריית הפלט מכילה, בין היתר:

  • המאגרים החיצוניים המאוחזרים ב-$OUTPUT_BASE/external.
  • בסיס ה-exec, ספרייה שמכילה קישורים לכל קודי המקור של ה-build הנוכחי. הוא נמצא בכתובת $OUTPUT_BASE/execroot. במהלך ה-build, ספריית העבודה היא $EXECROOT/<name of main repository>. אנחנו מתכננים לשנות זאת ל$EXECROOT, למרות שזו תוכנית לטווח ארוך כי מדובר בשינוי מאוד לא תואם.
  • קבצים שנבנו במהלך ה-build.

התהליך של ביצוע פקודה

כשהשרת של Bazel מקבל שליטה ומקבל הודעה על פקודה שהוא צריך לבצע, רצף האירועים הבא מתרחש:

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

  2. הפקודה הנכונה נמצאה. כל פקודה חייבת ליישם את הממשק BlazeCommand וחייבת להיות לה ההערה @Command (זה קצת אנטי-דפוס, יהיה נחמד אם כל המטא-נתונים הדרושים לפקודה יתוארו לפי שיטות ב-BlazeCommand)

  3. המערכת מנתחת את האפשרויות של שורת הפקודה. בכל פקודה יש אפשרויות שונות לשורת פקודה, המתוארות באנוטציה של @Command.

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

  5. הפקודה מקבלת שליטה. הפקודות המעניינות ביותר הן פקודות שמציגות build: בדיקה, הפעלה, הרצה, וכן הלאה: הפונקציונליות הזו מיושמת על ידי BuildTool.

  6. קבוצת דפוסי היעד בשורת הפקודה מנותחת ותווים כלליים לחיפוש כגון //pkg:all ו-//pkg/... נפתרים. ההטמעה הזו מתבצעת ב-AnalysisPhaseRunner.evaluateTargetPatterns() והפורמט שלה ב-Skyframe הוא TargetPatternPhaseValue.

  7. שלב הטעינה/הניתוח מופעל כדי ליצור את תרשים הפעולה (תרשים מכובד של פקודות שיש לבצע אותן עבור ה-build).

  8. שלב הביצוע מופעל. המשמעות היא הרצת כל הפעולות הנדרשות כדי לבנות את היעדים ברמה העליונה שהתבקשו.

אפשרויות בשורת הפקודה

אפשרויות שורת הפקודה עבור הפעלה של Bazel מתוארות באובייקט OptionsParsingResult, שיש בו מפה מ "מחלקות" אפשרות לערכי האפשרויות. "מחלקה של אפשרות" היא תת-מחלקה של OptionsBase ועם קיבוץ של אפשרויות בשורת הפקודה הקשורות זו לזו. למשל:

  1. אפשרויות הקשורות לשפת תכנות (CppOptions או JavaOptions). זו צריכה להיות מחלקת משנה של FragmentOptions ולבסוף נעוטפת באובייקט BuildOptions.
  2. אפשרויות הקשורות לאופן שבו Bazel מבצעת פעולות (ExecutionOptions)

ניתן לנצל את האפשרויות האלה בשלב הניתוח (ובאמצעות RuleContext.getFragment() ב-Java או ctx.fragments ב-Starlark). חלקם (לדוגמה, אם צריך לבצע ++ הכללה בסריקה או לא) נקראים בשלב הביצוע, אבל לשם כך יש צורך תמיד בשרברבות מפורשת, מפני ש-BuildConfiguration לא זמין אז. למידע נוסף, עיינו בקטע "הגדרות".

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

Bazel לומדת על אפשרויות השיעורים בדרכים הבאות:

  1. חלקם עם חוט באזל (CommonCommandOptions)
  2. מתוך ההערה @Command לגבי כל פקודה של Bazel
  3. החל מ-ConfiguredRuleClassProvider (אלו הן אפשרויות שורת פקודה הקשורות לשפות תכנות ספציפיות)
  4. גם כללים של Starlark יכולים להגדיר אפשרויות משלהם (ניתן לעיין כאן)

כל אפשרות (לא כולל אפשרויות המוגדרות על ידי Starlark) היא משתנה של קבוצת משנה FragmentOptions שיש לה את ההערה @Option, המציינת את השם ואת הסוג של אפשרות שורת הפקודה ביחד עם כמה טקסט עזרה.

סוג Java של הערך של אפשרות בשורת הפקודה הוא בדרך כלל משהו פשוט (מחרוזת, מספר שלם, בוליאני, תווית וכו'). עם זאת, אנו תומכים גם באפשרויות מסוגים מורכבים יותר; במקרה כזה, העבודה של המרה ממחרוזת הפקודה לסוג הנתונים מתאימה ליישום com.google.devtools.common.options.Converter.

עץ המקור, כפי שהוצג בבזל

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

מאגרי נתונים

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

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

קוד של מאגרים חיצוניים מקושר בצורה דינמית או נמצא להורדה דרך $OUTPUT_BASE/external.

בעת הפעלת ה-build, יש לחבר את עץ המקור כולו; הפעולה הזו מתבצעת על ידי SymlinkForest, המסמל כל חבילה במאגר הראשי ל-$EXECROOT וכל מאגר חיצוני ל-$EXECROOT/external או ל-$EXECROOT/.. (לשעבר, כמובן, גורם לכך לא ניתן לכלול חבילה בשם external במאגר הראשי. זו הסיבה לכך שאנחנו עוברים ממנה

חבילות

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

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

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

רוב המורכבות במהלך טעינת חבילה היא "גלישה". בזל לא דורשת שכל קובץ מקור יופיע במפורש, אלא יכולה להריץ גלובוסglob(["**/*.java"]) ). בשונה מהמעטפת, הוא תומך בגלובוסים חוזרים שהופכים לספריות משנה (אך לא לחבילות משנה). לשם כך נדרשת גישה למערכת הקבצים, ומאחר שהדבר עשוי להיות איטי, אנו מטמיעים כל מיני טריקים כדי שניתן יהיה להפעיל אותו במקביל וביעילות רבה ככל האפשר.

הטמעה על ידי פתיחות מתבצעת בכיתות הבאות:

  • LegacyGlobber, כדור הארץ מהיר ומאושר שלא אושר בשמיים
  • SkyframeHybridGlobber, גרסה שמשתמשת ב-Skyframe ומחזירה את הגלובוס הקודם כדי להימנע מ"הפעלה מחדש של Skyframe" (כפי שמתואר בהמשך)

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

  • המיפויים של המאגר
  • מחזיקי הכלים הרשומים
  • פלטפורמות הפעלה רשומות

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

תוויות, יעדים וכללים

החבילות מורכבות מיעדים, מהסוגים הבאים:

  1. קבצים: פריטים המהווים קלט או פלט של ה-build. במילים אחרות, אנחנו קוראים להן חפצי אומנות (מדברים במקום אחר). לא כל הקבצים שנוצרו במהלך ה-build הם יעדים; מקובל שלפלט לא תהיה תווית משויכת.
  2. כללים: כללים אלה מתארים את השלבים לשליפת התפוקות שלהם מהקלט שלהם. הן בדרך כלל משויכות לשפת תכנות (למשל,cc_library ,java_library אוpy_library ), אך יש גם שפות מסוימות בשפה מסוימת (למשלgenrule אוfilegroup )
  3. קבוצות חבילה: נדון בקטע חשיפה.

השם של היעד נקרא תווית. התחביר של תוויות הוא @repo//pac/kage:name, כאשר repo הוא שם המאגר שבו נמצאת התווית, pac/kage היא הספרייה שבה נמצא קובץ BUILD ו-name הוא נתיב הקובץ (אם התווית מתייחסת לקובץ מקור) ביחס לספרייה של החבילה. בהפניה ליעד בשורת הפקודה, ניתן להשמיט חלקים מסוימים של התווית:

  1. אם המאגר מושמט, התווית נמצאת במאגר הראשי.
  2. אם החלק של החבילה הושמט (למשל, name או :name), התווית נמצאת בחבילה של ספריית העבודה הנוכחית (נתיבים יחסיים המכילים הפניות ברמה גבוהה (.) אינם מורשים)

סוג של כלל (כמו "ספרייה C++") נקרא "סיווג כלל". ניתן להטמיע מחלקות כלל ב-Starlark (הפונקציה rule()) או ב-Java (שנקראת "כללים מקומיים", יש להקליד RuleClass). בטווח הארוך, כל כלל ספציפי לשפה יוטמע ב-Starlark, אך חלק ממשפחות הכללים (כגון Java או C++ ) מדור קודם עדיין נמצאות ב-Java בשלב זה.

יש לייבא כיתות של כללים של Starlark בתחילת קובצי BUILD באמצעות ההצהרה load(), בעוד שמחלקות הכללים של Java "יידועות" ומפורסמות על ידי Bazel, מתוקף הרישום בConfiguredRuleClassProvider הנתונים.

מחלקות של כללים מכילות מידע כגון:

  1. המאפיינים (כמו srcs, deps): הסוגים, ערכי ברירת המחדל, אילוצים וכו'.
  2. שינויי התצורה וההיבטים של המאפיין בכל מאפיין, אם יש כאלה
  3. יישום הכלל
  4. ספקי המידע הזמני מגדירים את הכלל "בדרך כלל"

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

מסגרת גג

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

הצמתים בתרשים נקראים SkyValue והשמות שלהם נקראיםSkyKey. שניהם אינם כה מהותיים; רק אובייקטים שאינם חיוניים צריכים להיות נגישים להם. נתון זה אינו תקף תמיד, ובמקרה כזה הוא לא (לדוגמה, עבור מחלקות האופציות BuildOptions, שהוא חבר ב- BuildConfigurationValue וב-SkyKey שלו) אנחנו מנסים שקשה מאוד לשנות אותן, או לשנות אותן רק בדרכים שאינן גלויות מבחוץ. מכאן נובע שכל דבר שמחושב ב-Skyframe (כגון יעדים מוגדרים) חייב להיות בלתי ניתן לשינוי.

הדרך הנוחה ביותר לצפות בתרשים Skyframe היא להפעיל את bazel dump --skyframe=detailed, שבה ה-Dump של הגרף, SkyValue אחד בכל שורה. רצוי לעשות זאת עבור בניינים קטנים, מכיוון שהוא עלול להיות גדול למדי.

Skyframe נמצא בחבילה של com.google.devtools.build.skyframe. החבילה בעלת השם הדומה com.google.devtools.build.lib.skyframe מכילה את היישום של Bazel מעל Skyframe. מידע נוסף על Skyframe זמין כאן.

יצירה של SkyValue חדש כוללת את השלבים הבאים:

  1. מריץ את ה-SkyFunction המשויך
  2. הצהרה על יחסי תלות (כגון SkyValue) שSkyFunction זקוקה להם כדי לבצע את עבודתו. ניתן לעשות זאת על ידי קריאה לעומס יתר של SkyFunction.Environment.getValue().
  3. אם תלות מסוימת אינה זמינה, אפליקציית Skyframe מאותתת חזרה של getValue() מהפונקציה null. במקרה כזה, SkyFunction צפוי להניב שליטה ב-Skyframe על-ידי החזרת ערך null. לאחר מכן, מערכת Skyframe מבצעת הערכה של סוגי התלות שעדיין לא הוערכו, וקוראת שוב לSkyFunction כלומר, חוזרים ל- (1).
  4. בניית SkyValue מתבצעת

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

  1. הצהרה על יחסי תלות של SkyFunction בקבוצות, כך שאם פונקציה כוללת, לדוגמה, 10 יחסי תלות, יש להפעיל אותה מחדש רק פעם אחת במקום עשר פעמים.
  2. לפצל את SkyFunction כך שלא יהיה צורך להפעיל מחדש פונקציה אחת פעמים רבות. יש לכך תופעות לוואי של שילוב נתונים ב-Skyframe, שיכולות להיות פנימיות ל-SkyFunction וכך להגדיל את השימוש בזיכרון.
  3. שימוש במטמון 'מאחורי הקלעים' כדי לשמור על מצב (לדוגמה, מצב הפעולות המבוצעות ב-ActionExecutionFunction.stateMap). במצב קיצוני, בסופו של דבר כתוב קוד שעובר את סגנון ההמשך (כמו ביצוע פעולה), שאינו קריא יותר.

כמובן, כל אלה הן רק פתרונות עקיפים להגבלות של Skyframe, שהן בעיקר תוצאה של העובדה ש-Java אינו תומך בשרשורים קלים ויש לנו באופן קבוע מאות אלפי Skyframe בטיסה{ 101}צמתים.

סטארלארק

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

Starlark מיושם בחבילה com.google.devtools.build.lib.syntax. בנוסף, יש לה הטמעה עצמאית של Go כאן. הטמעת ה-Java שנמצאת בשימוש ב-Bazel היא כיום מתורגמת.

Starlark משמש בארבעה הקשרים:

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

הניבים הזמינים לקובצי BUILD ו- .bzl שונים מעט מכיוון שהם מבטאים דברים שונים. רשימת ההבדלים זמינה כאן.

ניתן למצוא מידע נוסף על Starlark כאן.

שלב הטעינה/הניתוח

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

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

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

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

  1. הגדרות אישיות. ("איך" לבנות את הכלל הזה; לדוגמה, פלטפורמת הפלטפורמה, אבל גם דברים כמו אפשרויות של שורת פקודה שהמשתמש רוצה להעביר אל מהדר C++ )
  2. התלות הישירה. ספקי המידע העקיפים שלהם זמינים לכלל שנותח. הם נקראים כך משום שהם מספקים "רשימה כללית" של המידע בסגירה העקיפה של היעד המוגדר, כגון כל קובצי ה-char .בנתיב הנתיב או כל קובצי ה-o .ש{ 101}צריכים להיות מקושרים אל קובץ בינארי של C++ )
  3. היעד עצמו. זו התוצאה של טעינת החבילה שבה נמצא היעד. לגבי כללים, המאפיין הזה כולל את המאפיינים שלו, וזה מה שחשוב בדרך כלל.
  4. הטמעה של היעד שהוגדר. לגבי כללים, ניתן לציין זאת ב-Starlark או ב-Java. כל היעדים שלא הוגדרו על ידי כללים מוטמעים ב-Java.

הפלט של ניתוח יעד מוגדר הוא:

  1. ספקי המידע הזמני שהגדירו יעדים שתלויים בו יכולים לגשת
  2. פריטי המידע שנוצרו בתהליך פיתוח (Artifact) והפעולות שנוצרות.

ה-API המוצע לכללי Java הוא RuleContext, שהוא שווה-ערך לארגומנט ctx של כללי Starlark. ה-API שלו חזק יותר, אבל בו-זמנית קל יותר לבצע עם Bad ThingsTM , למשל, לכתוב קוד שהמורכבות של הזמן או הרווח שלו היא ריבועית (או פחות), כדי לגרום לשרת Bazel לקרוס עם חריג ל-Java או הפרת הבדלים (למשל, על ידי שינוי לא מכוון של Options או על ידי הפיכת נייד הניתן לשינוי ליעד)

האלגוריתם שקובע את יחסי התלות הישירים של יעד מוגדר נמצא ב-DependencyResolver.dependentNodeMap().

הגדרות אישיות

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

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

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

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

כאשר יישום של כלל צריך להיות חלק מהתצורה, הוא צריך להצהיר על כך בהגדרה שלו באמצעות RuleClass.Builder.requiresConfigurationFragments(). וזאת כדי למנוע טעויות (כמו כללי Python שמשתמשים בקטע Java) וכדי להקל על חיתוך הגדרות. כך, אם אפשרויות Python ישתנו, אין צורך לנתח מחדש את היעדים C++.

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

  1. בסף תלות. מעברים אלה מצוינים ב-Attribute.Builder.cfg() והם פונקציות מ-Rule (כאשר המעבר מתבצע)BuildOptions (בתצורה המקורית) לאחד או יותרBuildOptions (תצורת הפלט).
  2. בכל נקודת קצה נכנסת ליעד שהוגדר. מציינים אותם בRuleClass.Builder.cfg().

הכיתות הרלוונטיות הן TransitionFactory וConfigurationTransition.

המערכת משתמשת במעברי תצורה, לדוגמה:

  1. כדי להצהיר על תלות מסוימת שנעשה בה שימוש במהלך ה-build, והיא אמורה להיות בנויה כך בארכיטקטורת הביצוע
  2. כדי להצהיר שיש ליצור תלות מסוימת במספר ארכיטקטורה (למשל, עבור קוד מקומי ב-APK שממן של Android)

אם הגדרת תצורה מובילה למספר הגדרות, היא נקראת מעבר מפוצל.

ניתן להטמיע מעברי תצורה גם ב-Starlark (התיעוד כאן)

ספקי מידע טרנזיטיבי

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

יש בדרך כלל התכתובת של 1:1 בין ספקי מידע טרנזיטיבי ב-Java לבין ספקים של Starlark (חריג לכך הוא DefaultInfo, שהוא ערך זמני של FileProvider, FilesToRunProvider ו-RunfilesProvider מכיוון ממשק API זה נחשב ל- Starlark-יותר מתעתיק ישיר של ה-Java). המפתח שלו הוא אחד מהדברים הבאים:

  1. אובייקט מחלקת Java. אפשרות זו זמינה רק לספקים שאינם נגישים מ-Starlark. הספקים האלה הם חלק ממחלקת המשנה של TransitiveInfoProvider.
  2. מחרוזת. זוהי מסורת ומוסרת, מפני שהיא מוכרת להתנגשויות בין שמות. ספקי מידע טרנזיטיביים כאלה הם קטגוריות משנה של build.lib.packages.Info .
  3. סמל של ספק. ניתן ליצור זאת מ-Starlark באמצעות הפונקציהprovider(), והוא הדרך המומלצת ליצירת ספקים חדשים. הסמל מיוצג על ידי מופע של Provider.Key ב-Java.

יש להטמיע ספקים חדשים ב-Java באמצעות BuiltinProvider. NativeProvider הוצא משימוש (עדיין לא היה לנו זמן להסיר אותו) ולא ניתן לגשת ל-TransitiveInfoProvider מחלקות משנה מ-Starlark.

יעדים מוגדרים

יעדים שהוגדרו מיושמים כ-RuleConfiguredTargetFactory. לכל תת-מחלקה המוטמעת ב-Java יש תת-מחלקה. יעדים שהוגדרו על ידי Starlark נוצרים דרך StarlarkRuleConfiguredTargetUtil.buildRule() .

עבור מפעלי יעד מוגדרים, יש להשתמש ב-RuleConfiguredTargetBuilder כדי לבנות את ערך ההחזרה שלהם. הוא מורכב מהדברים הבאים:

  1. filesToBuild, הרעיון המטושטש של "קבוצת הקבצים שהכלל הזה מייצג". אלה הקבצים שנוצרים כאשר היעד המוגדר נמצא בשורת הפקודה או בשורות של דור.
  2. קובצי ההרצה שלהם, קבצים רגילים ונתונים.
  3. קבוצות הפלט שלהם. אלו הן "קבוצות קבצים אחרות" שהכלל יכול ליצור. ניתן לגשת אליהם באמצעות המאפיין Output_group [קבוצת_פלט] של כלל קבוצת הקבצים ב-BUILD ובאמצעות ספק OutputGroupInfo ב-Java.

קובצי הרצה

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

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

  • ברוב המקרים, הנתיב של קובצי הרצה זהה לקובץ ה-execpath שלו. אנחנו משתמשים בזה כדי לחסוך בצריכת ה-RAM.
  • יש סוגים שונים של ערכים בעצי קובץ ריצה, שגם צריך לייצג אותם.

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

קובצי ריצה בינאריים מיוצגים כמופע של RunfilesSupport. זה שונהRunfiles כיRunfilesSupport יש את היכולת לבנות בפועל (בניגודRunfiles, שהוא רק מיפוי). לשם כך נדרשים הרכיבים הנוספים הבאים:

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

היבטים

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

בדיוק כמו יעדים מוגדרים, הם מיוצגים ב-Skyframe כ-SkyValue וצורת הבנייה שלהם דומה מאוד לאופן שבו מוגדרים יעדים: יש להם מחלקה להגדרות היצרן שנקראת ConfiguredAspectFactory גישה אל RuleContext, אך בניגוד למפעלי יעד מוגדרים, הוא יודע גם על היעד המוגדר שאליו הוא מצורף ואל הספקים שלו.

קבוצת הרכיבים שמפוזרת בתרשים התלויות מצוינת לכל מאפיין באמצעות הפונקציה Attribute.Builder.aspects(). יש כמה כיתות בעלות שם מעורב שנוגעות לתהליך:

  1. AspectClass הוא היישום של ההיבט. היא יכולה להיות ב-Java (במקרה זה היא תת-מחלקה) או ב-Starlark (במקרה כזה, היא מופע של StarlarkAspectClass). הפעולה הזו דומה ל- RuleConfiguredTargetFactory.
  2. AspectDefinition היא ההגדרה של ההיבט; היא כוללת את הספקים שהיא דורשת, הספקים שהיא מספקת ומכילה הפניה ליישום שלה, כגון מופע AspectClass המתאים. התוצאה דומה לRuleClass.
  3. AspectParameters היא דרך למיון היבט המופץ בתרשים התלות. כרגע זו מחרוזת למחרוזת מחרוזת. דוגמה טובה ליעילות של מאגרי הנתונים הזמניים: אם לשפה יש מספר ממשקי API, יש להפיץ את המידע על ממשק ה-API של המאגרים כדי ליצור תרשים תלות.
  4. Aspect מייצג את כל הנתונים הדרושים לחישוב היבט המפיץ את תרשים התלות. הוא כולל את מחלקת ההיבט, את הגדרתה ואת הפרמטרים שלה.
  5. RuleAspect היא הפונקציה שקובעת אילו היבטים יש להפיץ כלל מסוים. זוהי פונקציית Aspect -> Rule.

סיבוך לא צפוי במידה מסוימת הוא שהיבטים יכולים להיות מוצמדים להיבטים אחרים. לדוגמה, סביר להניח שהיבט של איסוף ה-classpath ל-Java IDE ירצה לדעת על כל קובצי ה-Jam. בנתיב, אך חלק מהם הם מאגר נתונים זמני. במקרה כזה, המערכת תצטרך לצרף את ההיבט של IDE לזוג (proto_library כללי + היבט אב של Java).

המורכבות של היבטים בהיבטים היא עניין ברור בכיתה AspectCollection.

פלטפורמות ומחזיקי כלים

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

פלטפורמה מתוארת על ידי מיפוי של מפתח-ערך מהגדרות מגבלה (כגון המושג "ארכיטקטורת CPU") לערכי אילוץ (כגון יחידת עיבוד מרכזית (CPU) ספציפית כגון x86_64). יש לנו "מילון" של הגדרות וערכי האילוץ הנפוצים ביותר במאגר הנתונים @platforms.

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

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

  1. כלל toolchain() המתאר את קבוצת הביצוע והיעד, אוכף אילוץ כלים (למשל C++ או Java) של כלי כלים (האחרון מיוצג על ידי toolchain_type() כלל)
  2. כלל ספציפי לשפה שמתאר את מנגנון הכלים עצמו (למשל cc_toolchain())

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

פלטפורמות ביצוע מתוארות באחת מהדרכים הבאות:

  1. בקובץ WORKSPACE עם שימוש בפונקציה register_execution_platforms()
  2. בשורת הפקודה באמצעות האפשרות של שורת הפקודה --extra_execution_platforms

קבוצת פלטפורמות הביצוע הזמינות מחושבת ב- RegisteredExecutionPlatformsFunction .

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

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

  • קבוצת שרשני הכלים הרשומים (בקובץ WORKSPACE ובתצורה)
  • פלטפורמות הביצוע והיעד הרצויות (בתצורה)
  • קבוצה של סוגי כלים הנדרשים על ידי היעד שהוגדר (ב-UnloadedToolchainContextKey))
  • קבוצת האילוצים של פלטפורמת הביצוע של היעד שהוגדר (המאפיין exec_compatible_with) והתצורה (--experimental_add_exec_constraints_to_targets), ב-UnloadedToolchainContextKey

התוצאה היא UnloadedToolchainContext, שהוא למעשה מפה מסוג typechain (המיוצג כמופע ToolchainTypeInfo) לתווית של 'ארגז הכלים' שנבחר. פעולה זו נקראת 'לא נטען' מפני שהיא לא מכילה את כלי הכלים בעצמם, אלא רק את התוויות שלהם.

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

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

מגבלות

לפעמים מישהו רוצה להגדיר יעד כתואם לכמה פלטפורמות בלבד. ל-Bazel (למרבה הצער) יש כמה מנגנונים על מנת להשיג את היעד הזה:

  • מגבלות ספציפיות לכלל
  • environment_group() / environment()
  • אילוצי פלטפורמה

אילוצים ספציפיים לכלל משמשים לרוב ב-Google עבור כללי Java; הם בדרך אליך והם לא זמינים ב-Bazel, אבל קוד המקור עשוי להכיל הפניות אליו. המאפיין שמגדיר זאת נקרא constraints= .

workspace_group() ו-workspace()

כללים אלה הם מנגנון מדור קודם ואינם נמצאים בשימוש נרחב.

כל כללי ה-build יכולים להצהיר עבור אילו "סביבות" ניתן לבנות, כאשר "סביבה" היא מופע של הכלל environment().

יש מגוון דרכים שבהן אפשר להגדיר סביבות נתמכות עבור כלל:

  1. באמצעות המאפיין restricted_to=. זהו הטופס הספציפי ביותר למפרט; הוא מצהיר על קבוצה מדויקת של סביבות שהכלל תומך בהן בקבוצה זו.
  2. באמצעות המאפיין compatible_with=. זוהי הצהרה על סביבות שבהן כלל תומך, בנוסף לסביבות "סטנדרטיות", הנתמכות כברירת מחדל.
  3. באמצעות המאפיינים ברמת החבילה default_restricted_to= ו-default_compatible_with=.
  4. באמצעות מפרטי ברירת המחדל ב-environment_group() כללים. כל סביבה שייכת לקבוצה של אפליקציות דומות הקשורות לנושא מסוים (כגון "ארכיטקטורה יחידת עיבוד מרכזית", "גרסאות JDK" או "מערכות הפעלה לנייד"). ההגדרה של קבוצת סביבה כוללת את הסביבות הבאות שיש לתמוך בהן באמצעות "ברירת מחדל", אם לא צוין אחרת על ידי המאפיינים restricted_to= / environment(). כלל ללא מאפיינים כאלה יורש את כל ברירות המחדל.
  5. באמצעות ברירת מחדל של מחלקת כללים. כלל זה מבטל את ברירות המחדל הגלובליות של כל הסיווגים של מחלקת הכללים הנתונה. ניתן להשתמש בכך, למשל, כדי שכל כללי *_test ניתנים לבדיקה בלי שיהיה צורך להצהיר על יכולת זו במפורש.

environment() מיושם בכלל רגיל, בעוד ש-environment_group() הוא תת-מחלקה של Target אך לא Rule (EnvironmentGroup) ופונקציה הזמינה כברירת מחדל מ-Starlark (StarlarkLibrary.environmentGroup()) שבסופו של דבר יוצר יעד בעל שם זהה. זאת כדי למנוע תלות מחזורית שעלולה להיווצר כי כל סביבה צריכה להצהיר על קבוצת הסביבה שבה היא שייכת, וכל קבוצת סביבה צריכה להצהיר על סביבות ברירת המחדל שלה.

אפשר להגביל גרסת build לסביבה מסוימת באמצעות אפשרות שורת הפקודה --target_environment.

היישום של בדיקת האילוץ הוא בRuleContextConstraintSemantics ובTopLevelConstraintSemantics.

אילוצי פלטפורמה

הדרך ה "רשמית" הנוכחית לתיאור הפלטפורמות שאליהן מטרה היעד היא באמצעות אותם אילוצים המשמשים לתיאור מחזיקי כלים ופלטפורמות. התלונה נמצאת בבדיקה בבקשת ה-שליפה #10945.

חשיפה

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

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

הטמעה זו מתבצעת במקומות הבאים:

  • הממשק של RuleVisibility מייצג הצהרת חשיפה. היא יכולה להיות תמידית (גלויה לכול או פרטית לגמרי) או רשימת תוויות.
  • תוויות יכולות להתייחס לקבוצות של חבילות (רשימה מוגדרת מראש של חבילות), לחבילות ישירות (//pkg:__pkg__) או לעץ משנה של חבילות (//pkg:__subpackages__). הערך הזה שונה מתחביר שורת הפקודה, שבו משתמשים ב-//pkg:* או ב-//pkg/....
  • קבוצות חבילות מוטמעות כיעד משלהן וכסוגי יעדים מוגדרים (PackageGroup ו-PackageGroupConfiguredTarget). סביר להניח שנוכל להחליף אותם בכללים פשוטים אם נרצה.
  • ההמרה מרשימות תוויות חשיפה ליחסי תלות מתבצעת ב-DependencyResolver.visitTargetVisibility ובמספר מקומות שונים נוספים.
  • הבדיקה בפועל מתבצעת ב-CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

קבוצות מקוננות

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

  • קובצי הכותרת C++ המשמשים ל-build
  • קובצי האובייקט המייצגים את הסגירה הזמנית של cc_library
  • קובצי .j
  • קבוצת קובצי ה-Python בסגירה העקיפה של כלל Python

לדוגמה, אם עשינו זאת באופן ימי, List אוSet, בסופו של דבר נשתמש בזיכרון ריבועי: אם יש שרשרת של כללי N וכל כלל מוסיף קובץ, יהיו לנו 1+2+...+N חברי אוסף.

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

אותו מבנה נתונים נקרא depset ב-Starlark.

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

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

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

  1. **פריטים רגילים. **אלה נבדקות כדי להתעדכן על ידי חישוב סיכום הביקורת שלהם, עם קיצור דרך כקיצור זמן; אנחנו לא בודקים את הקובץ אם שעתו לא השתנתה.
  2. פריטים ללא תיאור של קישורים סימבוליים. הם נבדקים כדי לוודא שהם עדכניים על ידי קריאה ל-readlink(). שלא כמו פריטי מידע רגילים שנוצרים בתהליך פיתוח (Artifact), הם יכולים להתמקד בסמלי קישור. בדרך כלל הוא משמש במקרים שבהם אחד מהם אוסף חלק מהקבצים לארכיון מסוג כלשהו.
  3. פריטי עץ. לא מדובר בקבצים בודדים, אלא בעצי ספרייה. הם נבדקים על מנת לוודא שהם מעודכנים על ידי בדיקת קבוצת הקבצים שבה והתוכן שלהם. הם מיוצגים כ-TreeArtifact.
  4. הגדרות קבועות של מטא-נתונים. שינויים בפריטי המידע האלה לא מפעילים בנייה מחדש. משמש אך ורק למידע על חותמת build: אנחנו לא רוצים ליצור מחדש רק בגלל שהשעה הנוכחית השתנתה.

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

קיימים Artifact בולטים כמתווכים. הם מצוינים על ידי Artifactמדגמים שהם הפלטים של MiddlemanAction. הם משמשים במקרים מיוחדים:

  • אנשי הביניים שאוספים משתמשים משמשים לקיבוץ פריטי מידע שנוצרו בתהליך פיתוח (Artifact) יחד. זאת כדי שאם פעולות רבות ישתמשו באותה קבוצת קלט גדולה, לא יהיו לנו קצוות תלות של N*M, אלא רק N+M (הוא יוחלף בקבוצות מקוננות)
  • מתווכים של תזמון צריכים להבטיח שפעולה תפעל לפני פעולה אחרת. הן משמשות בעיקר למיון, אבל גם להידור של ++C (ראו CcCompilationContext.createMiddleman() להסבר)
  • מתווכים של קובצי הרצה נועדו להבטיח נוכחות של עץ קובצי ריצה, כך שאין צורך בנפרד תלוי במניפסט של הפלט ובכל פריט מידע שנוצר בתהליך פיתוח (Artifact) על ידי עץ קובצי ההרצה.

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

  • שורת הפקודה שיש להפעיל
  • פריטי המידע שנוצרו בתהליך פיתוח (Artifact) הנדרשים
  • את משתני הסביבה שצריך להגדיר
  • הערות המתארות את הסביבה (כגון פלטפורמה) שנדרשת כדי להציג אותה \

יש גם כמה מקרים מיוחדים אחרים, כמו כתיבת קובץ שהתוכן שלו ידוע לבזל. הם תת-קבוצה של AbstractAction. רוב הפעולות הן SpawnAction או StarlarkAction (באופן דומה, הן לא צריכות להיות כיתות נפרדות), אם כי ל-Java ול-C++ יש סוגי פעולות משלה (JavaCompileAction, CppCompileAction ו-CppLinkAction).

בסופו של דבר, אנחנו רוצים להעביר הכול אל SpawnAction; JavaCompileAction די קרוב, אבל C++ הוא בגדר מקרה מיוחד עקב ניתוח של קובץ .d בתוספת סריקה.

לרוב, תרשים הפעולה "מוטמע" בתרשים ה-Skyframe: באופן עקרוני, הביצוע של פעולה מיוצג על ידי הפעלת ActionExecutionFunction. המיפוי מסף תלות של תרשים פעולה לקצה של תלות ב-Skyframe מתואר ב-ActionExecutionFunction.getInputDeps() וב-Artifact.key(), וכולל כמה אופטימיזציות כדי לצמצם את מספר הקצוות של Skyframe:

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

פעולות משותפות

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

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

אם שתי פעולות יוצרות אותו קובץ פלט, הן צריכות להיות זהות: להזין אותן קלט, להיות באותו פלט ולהפעיל את אותה שורת פקודה. קשר זה מיושם ב-Actions.canBeShared() והוא מאומת בין שלבי הניתוח והביצוע על ידי בחינה של כל פעולה. יישום זה הוא SkyframeActionExecutor.findAndStoreArtifactConflicts() והוא אחד מהמקומות היחידים בבזל שמחייבים תצוגה "עולמית" של ה-build.

שלב הביצוע

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

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

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

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

לאחר מכן, Bazel מתחילה לחצות את תרשים הפעולות (התרשים המכוון הדו-צדדי המורכב מפעולות ומפריטי קלט ופלט) ופעולות ריצה. הביצוע של כל פעולה מיוצג על ידי מופע של ה-SkyValue מחלקה ActionExecutionValue.

מכיוון שביצוע פעולה הוא מוצר יקר, יש לנו כמה שכבות של שמירה במטמון שיכולות לפגוע ב-Skyframe:

  • ActionExecutionFunction.stateMap מכיל נתונים שמאפשרים לך להפעיל מחדש את Skyframe ב-ActionExecutionFunction זולים
  • המטמון של הפעולה המקומית מכיל נתונים על מצב מערכת הקבצים
  • מערכות הפעלה מרוחקות מכילות בדרך כלל גם מטמון משלהן

מטמון הפעולות המקומיות

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

המטמון הזה נבדק עבור התאמות באמצעות השיטה ActionCacheChecker.getTokenIfNeedToExecute() .

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

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

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

גילוי קלט וחיתוך קלט

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

  • פעולה עשויה לגלות קלט חדש לפני ביצועה או להחליט שחלק מהקלטים שלה אינם נחוצים בפועל. הדוגמה הקנונית היא C++ , שבה עדיף לנחש בצורה מושכלת את קובצי הכותרות שבהם משתמש קובץ C++ כדי לסגור אותו, כדי להימנע משליחת כל קובץ למנהלים מרוחקים; לכן יש לנו אפשרות לא לרשום כל קובץ כותרת כ-"input", אלא לסרוק את קובץ המקור כדי להציג כותרות נכללות באופן עקרוני, ולסמן רק את קובצי הכותרות כקלטים המוזכרים ב-#include (אנחנו מבצעים הערכת יתר כדי שלא נצטרך ליישם מעבד מלא מסוג C) בשלב זה, האפשרות הזו מחוברת ל"שלי"
  • פעולה עשויה להבין שלא נעשה שימוש בחלק מהקבצים במהלך ההפעלה. ב-C++, נקרא "קובצי d.": המהדר מספר באילו קובצי כותרות נעשה שימוש לאחר העובדה, וכדי להימנע מהמבוכה של הפרעה מפני החרגה, Bazel משתמשת של עובדה זו. הוא מספק הערכה טובה יותר מסורק הכללה מאחר שהוא מסתמך על המהדר.

הם מוטמעים באמצעות שיטות בפעולה:

  1. Action.discoverInputs() נקרא. עליה להחזיר קבוצה מקננת של פריטי מידע שנוצרו בתהליך פיתוח. אלו צריכים להיות פריטי מידע שנוצרו על ידי מקור מידע (Artifact), כך שאין בתרשים יחסי תלות שאין להם ערך מקביל בתרשים היעד שהוגדר.
  2. הפעולה מתבצעת על ידי התקשרות ל-Action.execute().
  3. בסוף האירוע Action.execute(), הפעולה יכולה להתקשר אל Action.updateInputs() כדי לומר ל-Bazel שלא כל הקלט שלה נחוץ. מצב זה עלול לגרום לגרסאות שגויות שגויות אם הדיווח על קלט משומש אינו בשימוש.

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

פעולות של Starlark יכולות להשתמש במתקן כדי להצהיר על קלט מסוים כ'לא בשימוש', באמצעות הארגומנט unused_inputs_list= של ctx.actions.run().

דרכים שונות לביצוע פעולות: אסטרטגיות/הקשרים

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

מחזור החיים של הקשר לפעולה הוא כזה:

  1. בתחילת שלב הביצוע, המערכת שולחת מופע של BlazeModule מופעים של הקשרי פעולה. זה מתרחש בבונה של ExecutionTool. סוגי הקשר לפעולה מזוהים על ידי מופע ClassJava המתייחס לממשק משנה של ActionContext, והממשק חייב להיות מבוסס על ממשק הפעולה הזה.
  2. ההקשר של הפעולה המתאימה נבחר מבין אלו הזמינים ומועבר אל ActionExecutionContext ואל BlazeExecutor .
  3. פעולות לבקשת הקשר באמצעותActionExecutionContext.getContext() וגםBlazeExecutor.getStrategy() (צריכה להיות רק דרך אחת לעשות זאת...)

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

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

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

מידע נוסף על אסטרטגיות (או על הקשרי פעולה!):

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

מנהל המשאבים המקומיים

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

זה מיושם במחלקה ResourceManager: כל פעולה צריכה להיות מסומנת בהערכה של המשאבים המקומיים הנדרשים לשימוש במופע של ResourceSet (יחידת CPU ו-RAM). לאחר מכן, כשפעולות הקשר מבצעות פעולה שמחייבת משאבים מקומיים, הן קריאות ל-ResourceManager.acquireResources() ונחסמות עד שקיימים המשאבים הדרושים.

תיאור מפורט יותר של ניהול המשאבים המקומיים זמין כאן.

המבנה של ספריית הפלט

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

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

כיצד נקבע שם הספרייה המשויכת לתצורה מסוימת? יש שני מאפיינים רצויים:

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

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

הגישה הנוכחית היא שפלח הנתיב של התצורה הוא <CPU>-<compilation mode> עם סיומות שונות, כך שמעברי הגדרות המיושמים ב-Java לא יגרמו להתנגשות בין פעולות. בנוסף, סיכום של קבוצת המעברים בתצורת Starlark מתווסף, כך שמשתמשים לא יוכלו לגרום להתנגשויות בפעולה. הוא רחוק מאוד משלמות. זה מיושם ב-OutputDirectories.buildMnemonic() ומסתמך על כל קטע תצורה שמוסיף חלק משלו לשם של ספריית הפלט.

בדיקות

ל-Bazel יש תמיכה עשירה בהרצת בדיקות. הוא תומך ב:

  • הרצת בדיקות מרחוק (אם קצה עורפי מרוחק זמין)
  • הרצת בדיקות מספר פעמים במקביל (לצורך איסוף או איסוף של נתוני תזמון)
  • בדיקות הפיצול (פיצול של מקרי בדיקה באותה בדיקה על פני מספר תהליכים) לצורך מהירות)
  • הרצת בדיקות רעועות
  • קיבוץ הבדיקות בחבילות הבדיקה

בדיקות הן יעדים קבועים המוגדרים עם TestProvider, שמתארת כיצד הבדיקה צריכה לפעול:

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

בדיקה אילו בדיקות לבצע

ההחלטה אילו בדיקות פועלות היא תהליך מורכב.

תחילה, במהלך ניתוח תבנית היעד, חבילות הבדיקה מתרחבות באופן רקורסיבי. ההרחבה מיושמת בTestsForTargetPatternFunction. קמט קצת מפתיע הוא שאם חבילת בדיקה מצהירה שאין בחינות, היא מתייחסת לכל בדיקה באריזתה. הטמעה זו תתבצע על ידי Package.beforeBuild() על ידי הוספת מאפיין מרומז שנקרא $implicit_tests לבדיקת כללי החבילה.

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

העיבוד נוסף מתבצע בעוד BuildView.createResult(): יעדים שהניתוח שלהם נכשל מסוננים ובדיקות מחולקות לבדיקות בלעדיות ולא בלעדיות. לאחר מכן הנתונים מועברים אל AnalysisResult, וכך ExecutionTool יודעת אילו בדיקות להפעיל.

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

הרצת בדיקות

כדי להריץ את הבדיקות, צריך לבקש מפריטי סטטוס מהמטמון. לאחר מכן התוצאה תהיה ביצוע של TestRunnerAction, שבסופו של דבר קורא ל-TestActionContext שנבחר על ידי אפשרות שורת הפקודה --test_strategy שמריצה את הבדיקה באופן המבוקש.

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

בנוסף לקובץ סטטוס של המטמון, כל תהליך בדיקה פולט מספר קבצים נוספים. הם נשמרים ב "ספריית יומני הבדיקה". זוהי ספריית המשנה המכונה testlogs מספריית הפלט של תצורת היעד:

  • test.xml, קובץ XML בסגנון JUnit שמפרט את מקרי הבדיקה הנפרדים בפיצול הבדיקה
  • test.log, פלט המסוף של הבדיקה. stdout ו-stdrt אינם מופרדים.
  • test.outputs, "ספריית הפלטים הלא מוצהרת"; המאפיין הזה משמש לבדיקות שברצונן להוציא קבצים בנוסף לפריטים שמדפיסים במסוף.

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

חלק מהבדיקות צריך להתבצע במצב בלעדי, לדוגמה, שלא במקביל לבדיקות אחרות. אפשר לעשות זאת על ידי הוספת tags=["exclusive"] לכלל הבדיקה או הפעלת הבדיקה עם --test_strategy=exclusive . כל בדיקה בלעדית מופעלת באמצעות הפעלה נפרדת של Skyframe, המבקשת לבצע את הבדיקה לאחר הבנייה "הראשית". פעולה זו בתוך SkyframeExecutor.runExclusiveTest().

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

ההטמעה מתבצעת במחלקה הנקראת בשם StreamedTestOutput. היא פועלת על ידי עריכת שינויים בקובץ test.log של הבדיקה המדוברת והשלכת בייטים חדשים למסוף שבו Bazel שולטת.

תוצאות הבדיקות שבוצעו זמינות באוטובוס אירועים על ידי צפיה באירועים שונים (כגון TestAttempt, TestResult או TestingCompleteEvent). הם נטענים כשמורים בפרוטוקול לבניית אירועים, והם יונפקו במסוף לפי AggregatingTestListener.

איסוף כיסוי

כיסוי מדווח על ידי בדיקות בפורמט LCOV בקבצים bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

כדי לאסוף את הכיסוי, כל פעולת בדיקה מתרחשת בסקריפט שנקרא collect_coverage.sh .

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

השילוב של collect_coverage.sh מתבצע על ידי שיטות הבדיקה ומחייב את collect_coverage.sh לכלול את הקלט של הבדיקה. נתון זה מסופק על ידי המאפיין המרומז :coverage_support המזוהה לערך של סימון התצורה --coverage_support (ראה TestConfiguration.TestOptions.coverageSupport)

חלק מהשפות מבצעות אינסטרומנטציה באופליין, כלומר אינסטרומנטציה של הכיסוי מתבצעת בזמן ההידור, (למשל, C++ ) ושפות אחרות אינסטרומנטציה באינטרנט. כלומר, אינסטרומנטציה של הכיסוי מתווסף בזמן הביצוע.

עיקרון בסיסי נוסף הוא כיסוי בסיסי. זהו הכיסוי של ספרייה, בינארי או בדיקה אם לא הופעל בהם קוד. הבעיה שהיא פותרת היא שאם אתם רוצים לחשב את כיסוי הבדיקות של קובץ בינארי, זה לא מספיק כדי למזג את הכיסוי של כל הבדיקות, כי ייתכן שיש קוד בבינארי שאינו{101 }מקושר לכל בדיקה. לכן, המטרה שלנו היא לפרוס קובץ כיסוי לכל קובץ בינארי שמכיל רק את הקבצים שאנחנו אוספים עבורם כיסוי ללא שורות כיסוי. קובץ הכיסוי הבסיסי עבור יעד הוא bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . הוא נוצר גם עבור בינאריות וספריות בנוסף לבדיקות אם תעביר את הדגל --nobuild_tests_only ל-Bazel.

הכיסוי של קבוצת הבסיס לא תקין.

אנחנו עוקבים אחר שתי קבוצות של קבצים לצורך איסוף נתוני כיסוי לכל כלל: קבוצת הקבצים האינסטרומנטלים וקבוצה של מטא-נתונים של אינסטרומנטציה.

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

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

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

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

אנחנו גם מפיקים "דוח כיסוי" שממזג את הכיסוי שנאסף לכל בדיקה בהפעלת בזל. מטופל על ידי CoverageReportActionFactory ומתקשר מ-BuildView.createResult() . הוא מקבל גישה לכלים הנחוצים לו על ידי סקירת המאפיין :coverage_report_generator בבדיקה הראשונה שבוצעה.

מנוע השאילתות

לבזל יש שפה קטנה ששואלים אותה מגוון תרשימים. סוגי השאילתות הבאים מסופקים:

  • נעשה שימוש ב-bazel query כדי לחקור את תרשים היעד
  • נעשה שימוש ב-bazel cquery כדי לחקור את תרשים היעד שהוגדר
  • bazel aquery משמש לבדיקת תרשים הפעולות

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

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

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

מערכת המודול

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

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

  • ממשקים למערכות הפעלה מרוחקות
  • פקודות חדשות

קבוצת נקודות התוסף ש-BlazeModule מציעה אינה שגרתית. אל תשתמשו בו כדוגמה לעקרונות עיצוב טובים.

אוטובוס האירועים

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

  • רשימת יעדי ה-build שאמורה להיווצר הוגדרה (TargetParsingCompleteEvent)
  • נקבעו התצורות ברמה העליונה (BuildConfigurationEvent)
  • נבנה יעד, בהצלחה או לא (TargetCompleteEvent)
  • הבדיקה הופעלה (TestAttempt, TestSummary)

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

ניתן ליישם זאת בחבילות ה-Java של build.lib.buildeventserviceו-build.lib.buildeventstream.

מאגרים חיצוניים

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

הקובץ WORKSPACE

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

    local_repository(name="foo", path="/foo/bar")

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

כדי לטפל במקרה כזה, הניתוח של קובץ WORKSPACE (ב-WorkspaceFileFunction) מחולק לפי נתחים המקובצים באמצעות הצהרות load(). אינדקס הגוש מצוין על ידיWorkspaceFileKey.getIndex() מחשובWorkspaceFileFunction עד שאינדקס X משמעותו מתבצעת הערכה עד ה-Xload() הצהרה.

מאחזר מאגרים

לפני שקוד המאגר זמין ל-Bazel, צריך לאחזר אותו. בעקבות זאת, בזל יוצרת ספרייה תחת $OUTPUT_BASE/external/<repository name>.

שליפת המאגר מתבצעת בשלבים הבאים:

  1. PackageLookupFunction מבינה שדרוש לה מאגר ויוצרת RepositoryName בתור SkyKey, שמפעילה את RepositoryLoaderFunction
  2. RepositoryLoaderFunction מעביר את הבקשה אל RepositoryDelegatorFunction מסיבות לא ברורות (לפי הקוד, אין להימנע מהורדה מחדש של פריטים במקרה של אתחול ב-Skyframe, אבל זו לא סיבה מוצקה מאוד)
  3. RepositoryDelegatorFunction מגלה את כלל המאגר שהתבקשה על ידי חזרה על קטעי קובץ WORKSPACE עד שהמאגר המבוקש נמצא
  4. נמצא כי ה-RepositoryFunction המתאים מיישם את שליפת המאגר; זהו יישום של Starlark של המאגר או מפה בתוך הקוד של מאגרים שמוטמעים ב-Java.

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

  1. קיים מטמון של קבצים שהורדו, עם סיכום המפתח שלהם (RepositoryCache). לשם כך, צריך שהבדיקה תהיה זמינה בקובץ WORKSPACE, אבל בכל מקרה זה תקין. הוא משותף על ידי כל מופע של שרת Bazel באותה תחנת עבודה, ללא קשר לסביבת העבודה או לבסיס הפלט שבהם הם פועלים.
  2. "קובץ סמן" נכתב לכל מאגר לפי $OUTPUT_BASE/external, שמכיל סיכום של הכלל ששימש לאחזור הקובץ. אם שרת Bazel מופעל מחדש אך סיכום הסיכום אינו משתנה, הוא לא מאוחזר. פעולה זו מיושמת בRepositoryDelegatorFunction.DigestWriter .
  3. אפשרות שורת הפקודה --distdir מציינת מטמון נוסף המשמש לחיפוש פריטי מידע שנוצרו בתהליך פיתוח (Artifact) להורדה. אפשרות זו שימושית בהגדרות הארגון שבהן ב-Bazel לא צריכה לאחזר מהאינטרנט נתונים אקראיים. דירוג זה מיושם על ידי DownloadManager .

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

ספריות מנוהלות

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

  1. מאפשרת למשתמש לציין ספריות משנה של סביבת העבודה Bazel אינה מורשית להגיע אליהן. הם רשומים בקובץ שנקרא .bazelignore, והפונקציונליות מיושמת ב-BlacklistedPackagePrefixesFunction.
  2. אנחנו מקודדים את המיפוי מספריית המשנה של סביבת העבודה למאגר החיצוני שמטפלים בו ב-ManagedDirectoriesKnowledge, ומטפלים בFileStateValueהפניות האלה באותו אופן כמו בתוויות במאגרים חיצוניים.

מיפויי מאגרים

ייתכן מצב שבו מספר מאגרים ירצו להיות תלויים באותו מאגר, אבל בגרסאות שונות (זהו מופע של "בעיה של תלות יהלום"). לדוגמה, אם שני בינאריים במאגרים נפרדים ב-build רוצים להסתמך על Guava, סביר להניח ש-Gava מתייחסת גם לתוויות החלות מ-@guava// וצפים שמשמעות הדבר היא גרסאות שונות שלה.

לכן, Bazel מאפשרת למפות מחדש תוויות של מאגר חיצוני כך שהמחרוזת @guava// תוכל להפנות למאגר נתונים אחד של Guava (כמו @guava1//) במאגר של מאגר בינארי אחד ומאגר Guava אחר (כגון @guava2//) המאגר של האחר.

לחלופין, ניתן להשתמש באפשרות הזו גם כדי להצטרף ליהלומים. אם מאגר תלוי ב-@guava1//, ומאגר אחר תלוי ב-@guava2//, מיפוי של מאגרים יאפשר מיפוי מחדש של שני המאגרים כך שישמשו כמאגר קנוני של @guava//.

המיפוי מצוין בקובץ WORKSPACE כמאפיין repo_mapping של הגדרות מאגר בודדות. לאחר מכן היא מוצגת ב-Skyframe כחברה בWorkspaceFileValue, שם היא צנרת אל:

  • Package.Builder.repositoryMapping המשמש לשינוי מאפיינים של תוויות בערכי תוויות בחבילה על ידי RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping המשמש בשלב הניתוח (לפתרון דברים כמו $(location), שלא מנותחים בשלב הטעינה)
  • BzlLoadFunction לצורך פתרון תוויות בדוחות (load)

JNI ביטים

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

קוד C++ נמצא תחת src/main/Native ושיעורי Java עם שיטות מקומיות הם:

  • NativePosixFiles ו-NativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations ו-WindowsFileProcesses
  • com.google.devtools.build.lib.platform

פלט של הקונסולה

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

מיד לאחר שהקריאה ל-RPC מגיעה מהלקוח, נוצרים שני RpcOutputStream שלבים (עבור stdout ו-stderr) שמעבירים את הנתונים המודפסים אליהם אל הלקוח. לאחר מכן, הסרטונים האלה מופיעים בצמד OutErr (stdout, stderr). כל מה שצריך להדפיס בקונסולה עובר בשידורים האלה. לאחר מכן, השידורים האלה מועברים אל BlazeCommandDispatcher.execExclusively().

כברירת מחדל, הפלט מודפס עם רצפי בריחה מסוג ANSI. אם הן לא רצויות (--color=no), הן מוסרות על ידי AnsiStrippingOutputStream. בנוסף, System.outו-System.err יופנו אל מקורות הפלט האלה. כל זאת כדי לאפשר הדפסה של פרטי ניפוי באגים באמצעות System.err.println() ועדיין הם יוצאים בפלט הטרמינל של הלקוח (והוא שונה מזה של השרת). חשוב לקחת בחשבון שאם תהליך ההפקה מפיק פלט בינארי (כמו bazel query --output=proto), לא מתבצע שילוב של תזוזות יצירתיות.

הודעות קצרות (שגיאות, אזהרות ועוד) מבוטאות באמצעות הממשק של EventHandler. חשוב לשים לב שאלה שונות מהודעה אחת שמופיעה בEventBus (זה מבלבל). לכל Event יש EventKind (שגיאה, אזהרה, מידע, וכמה אחרים) וייתכן שיש לו Location (המקום בקוד המקור שגרם לאירוע ).

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

חלק מאירועי EventHandler מאפשרים גם לפרסם אירועים שבסופו של דבר יגיעו לאוטובוס האירוע (האירועים הרגילים Event לא _מופיעים שם). אלו הן הטמעות של ExtendedEventHandler והשימוש העיקרי בהן הוא הפעלה מחדש של אירועים שמורים במטמון של EventBus. EventBus אירועים אלה מטמיעים את Postable, אבל לא כל מה שמתפרסם ב-EventBus מיישם את הממשק הזה בלבד. רק אירועים ששמורים במטמון ExtendedEventHandler (הוא נחמד ורוב הדברים עושים, אבל הוא לא נאכף

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

  • אוטובוס האירועים
  • השידור של האירוע נוסף לאירוע דרך גורם מדווח

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

בזל ליצירת פרופילים

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

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

יוצר הפרופילים מתחיל ונעצר ב-BlazeRuntime.initProfiler() וב-BlazeRuntime.afterCommand(), בהתאמה, ומנסה להיות פעיל ככל האפשר כדי שנוכל לסדר את כל הפרופילים. כדי להוסיף פריט לפרופיל, ניתן להתקשר אל Profiler.instance().profile(). היא מחזירה Closeable, שהסגירה שלו מייצגת את סוף המשימה. עדיף להשתמש במשפטי ניסיון עם משאבים.

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

מסגרת בדיקה

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

בבדיקות השילוב, יש לנו שני סוגים:

  1. תהליכים שמוטמעים באמצעות מסגרת מורכבת מאוד של בדיקות התאמה לשימוש בשיטה src/test/shell
  2. שירותים שמוטמעים ב-Java. הם מוטמעים כמחלקות משנה של AbstractBlackBoxTest.

AbstractBlackBoxTest הוא יתרון שהוא פועל גם ב-Windows, אבל רוב בדיקות השילוב שלנו נכתבות בבאש.

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