בסיס הקוד של Bazel

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

מבוא

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

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

הגרסה הציבורית של קוד המקור של 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). הסוג הקודם נקרא "startup&&quot, והוא משפיע על תהליך השרת כולו, ואילו הסוג השני, "command option&quot, משפיע רק על פקודה אחת.

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

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

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

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

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

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

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

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

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

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

הסימן "workspace" הוא עץ המקור ש-Bazel פועל בו. הוא בדרך כלל תואם למשהו שביצעתם באמצעי הבקרה של המקור.

מאחר ש-Bazel מאחסנת את כל הנתונים שלה בשורש ה&&שורש; בדרך כלל בדרך זו $HOME/.cache/bazel/_bazel_${USER}, אבל ניתן לעקוף אותה באמצעות אפשרות ההפעלה --output_user_root.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

עץ המקור, כפי שנראה על ידי בזל

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

מאגרי נתונים

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

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

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

במהלך בניית ה-build, יש לחבר את עץ המקור כולו.

חבילות

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

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

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

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

השימוש בגלובוס מתבצע בכיתות הבאות:

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

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

  • מיפוי המאגר
  • ערכות הכלים הרשומים
  • פלטפורמות הביצוע הרשומות

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

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

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

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

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

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

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

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

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

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

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

מסגרת עילית

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

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

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

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

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

בכל פעם שכתובת ה-SkyFunction מבקשת תלות שאינה זמינה, getValue() יחזיר null. הפונקציה אמורה להחזיר את הפקד ל-Skyframe, כי היא מחזירה ללא ערך. בשלב מאוחר יותר, Skyframe יעריך את התלות שאינה זמינה, ולאחר מכן יפעיל מחדש את הפונקציה מההתחלה – רק שפעם זו, השיחה של getValue() תצלח עם תוצאה שאינה אפס.

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

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

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

סטארלרק

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

Starstark מוטמע בחבילה של net.starlark.java. יש גם הטמעה עצמאית של Go כאן. ההטמעה של Java ב-Bazel היא כרגע תרגום.

Starlark משמשת בכמה הקשרים, כולל:

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

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

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

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

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

הוא נקרא שלב "Load/analysis& ; כי ניתן לפצל אותו לשני חלקים נפרדים, שפעם היו סדרים, אבל עכשיו הם יכולים לחפוף בזמן:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ספקי מידע עקיפים

ספקי מידע מעבר הם דרך (ו-___ בלבד) עבור יעדים מוגדרים, שמאפשרים לספר דברים על יעדים מוגדרים אחרים שתלויים בהם. הסיבה לכך ש-"transitive" היא בשם הזה היא בדרך כלל סוג של סיכום של הסגירה העקיפה של יעד שהוגדר.

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

  1. אובייקט Java Class. האפשרות הזו זמינה רק לספקים שאי אפשר לגשת אליהם מ-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, הקונספט המעורפל של "קבוצת הקבצים שהכלל הזה מייצג." אלה הקבצים שנוצרים כאשר היעד שהוגדר נמצא בשורת הפקודה או ב- src של ג'ונגל.
  2. קובצי הריצה שלהם, הנתונים הרגילים והנתונים שלהם.
  3. קבוצות הפלט שלהן. אלה קבוצות שונות של קבצים&&;הכלל יכול לבנות. אפשר לגשת אליהם באמצעות המאפיין record_group [קבוצת_פלט] של כלל קבוצת הקבצים ב-BUILD באמצעות הספק OutputGroupInfo ב-Java.

קובצי Runrun

צריך להפעיל קובצי בינאריים מסוימים. דוגמה בולטת היא בדיקות הדורשות קובצי קלט. הנתון הזה מיוצג בבזל לפי הקונספט "runfiles" קובץ A&&runrun הוא עץ של ספריית הקבצים הבינארית הספציפית. הוא נוצר במערכת הקבצים כעץ קישור סימבולי עם קישורים סימבוליים ספציפיים שמכוונים לקבצים שבמקור של עצי הפלט.

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

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

קובצי Runrun נאספים באמצעות RunfilesProvider: מופע של המחלקה הזו מייצג את קובצי ה-runנה של יעד שהוגדר (כגון ספרייה) ואת דרישות הסגירה העקיפות שלו. המערכת אוספת אותם כקבוצה מקוננת (למעשה, הם מטמיעים אותם באמצעות קבוצות מקוננות מתחת לכיסוי): כל איחוד של נקודות הריצה מכיל את ה-runfiles שלו, מוסיף חלק משלו ומפעיל את הקבוצה שבתוכה. מופע של RunfilesProvider מכיל שתי מופעים של Runfiles, פעם שבה הכלל תלוי באמצעות המאפיין "data" ופרמטר אחד לכל סוג אחר של תלות נכנסת. הסיבה לכך היא שבמקרים מסוימים, המאפיין 'טירגוט' מציג קובצי runload שונים כשתלויים במאפיין נתונים. זו התנהגות לא רצויה שעדיין לא עברנו להסרתה.

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

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

היבטים

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

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

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

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

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

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

פלטפורמות וערכות כלים

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

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

הקונספט של כלי כלים נובע מהעובדה שפלטפורמות התוכן שפועלות והפלטפורמות שאליהן הן מטורגטות עשויות לדרוש שימוש במהדרים שונים. לדוגמה, כלי 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, שהיא למעשה מפה מסוג סוג כלי עבודה (המיוצג כמופע ToolchainTypeInfo) לתווית של כלי הרכב שנבחר. הוא נקרא "unloaded" מכיוון שהוא לא מכיל את שרשרת הכלים, אלא רק את התוויות שלהן.

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

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

מגבלות

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

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

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

workspace_group() ו-workspace()

כללים אלה הם מנגנון ישן ואינם נמצאים בשימוש נפוץ.

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

יש כמה דרכים לציין סביבות נתמכות לכלל:

  1. באמצעות המאפיין restricted_to=. זוהי צורת הפירוט הישירה ביותר. היא מציינת את קבוצת הסביבות המדויקת שבה הכלל תומך בקבוצה הזו.
  2. באמצעות המאפיין compatible_with=. היא מצהירה על סביבות שהכלל תומך בהן בנוסף ל "standard"
  3. באמצעות המאפיינים ברמת החבילה default_restricted_to= ו-default_compatible_with=.
  4. באמצעות מפרטי ברירת המחדל ב-environment_group() כללים. כל סביבה שייכת לקבוצה של עמיתים בתחום מסוים (למשל "CPU ארכיטקטורות", "JDK גרסאות" או "מערכות הפעלה לנייד"). ההגדרה של קבוצת סביבה כוללת את הסביבות שבהן צריך לספק תמיכה במאפייני ה-"default" אם לא צוין אחרת באמצעות המאפיינים 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). סביר להניח שנשנה את הכללים האלה בכללים, אם תרצו. אופן הלוגיקה שלהם הוא הטמעה: PackageSpecification, שתואמת לדפוס אחד, כמו //pkg/..., PackageGroupContents, שתואם למאפיין package_group יחיד של package_group' ול-PackageSpecificationProvider שמצטבר ב-package_group.
  • ההמרה מרשימות של תוויות חשיפה נעשית ב-DependencyResolver.visitTargetVisibility ובמספר מקומות אחרים.
  • הבדיקה מתבצעת בפועל ב-CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

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

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

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

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

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

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

חפצים ופעולות

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

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

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

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

סוג מפורסם של Artifact הוא מתווך. הם מסומנים ב-Artifact מופעים שהם הפלט של MiddlemanAction. הן משמשות למקרים מיוחדים:

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

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

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

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

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

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

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

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

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

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

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

שלב הביצוע

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

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

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

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

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

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

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

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

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

המטמון הזה נבדק אם יש בו היטים בשיטה ActionCacheChecker.getTokenIfNeedToExecute() .

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

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

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

גילוי וקיצור קלט של קלט

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

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

ההגדרות האלה מיושמות באמצעות שיטות בפעולה:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

בדיקות

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

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

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

  • פריטי המידע שנוצרו בתהליך פיתוח והבדיקה שלהם מתבצעת. זהו קובץ "cache status" שמכיל הודעת 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 ו-stderr אינם מופרדים.
  • test.outputs, "ספריית פלטים לא מוצהרים;; משמש למטרות בדיקה שרוצים להוציא קבצים בנוסף למה שהם מדפיסים למסוף.

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

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

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

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

תוצאות הבדיקות שבוצעו באוטובוס האירועים זמינות לאירועי תצפית שונים (כמו TestAttempt, TestResult או TestingCompleteEvent). הן מושלכות לפרוטוקול build של אירוע, והן מועברות למסוף על ידי 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++ ) והאחרות כוללות אינסטרומנטציה באינטרנט, כלומר האינסטרומנטציה של הכיסוי מתווספת בזמן הביצוע.

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

נכון לעכשיו, הכיסוי הבסיסי לא תקין.

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

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

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

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

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

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

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

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

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

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

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

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

מערכת המודול

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

הם משמשים בעיקר להטמעה של חלקים שונים של "ללא ליבה

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

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

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

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

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

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

האפשרות הזו מוטמעת בחבילות build.lib.buildeventservice ו-Java של build.lib.buildeventstream.

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

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

הקובץ WORKSPACE

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

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

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

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

אחזור המאגרים

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

אחזור המאגר מתרחש בשלבים הבאים:

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

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

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

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

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

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

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

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

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

לכן, Bazel מאפשרת למפות מחדש תוויות של מאגר חיצוני, כדי שהמחרוזת @guava// תוכל להפנות למאגר אחד של גואבה (כמו @guava1//) במאגר של קובץ בינארי אחד ולמאגר אחר של גויאבה (כמו @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/primary/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), לא מתבצע ערבול של stdout.

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

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

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

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

  • אוטובוס האירועים
  • זרם האירועים נכנס אליו דרך מדווח

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

בזלת

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

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

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

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

מרכז בדיקות

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

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

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

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

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