ניהול יחסי תלות חיצוניים באמצעות Bzlmod

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

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

מודולי Bazel

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

מודול הוא למעשה פרויקט של Bazel שיכול להיות בעל מספר גרסאות, וכל אחד מהם מפרסם מטא נתונים לגבי מודולים אחרים שהוא תלוי בהם. הדבר דומה למושגים מוכרים במערכות אחרות לניהול תלות: Aventure (מרכיב), חבילת npm, חבילה של מטען , מודול של Go וכו'

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

MODULE.bazel

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

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

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

הקובץ MODULE.bazel דומה לקובצי BUILD מאחר שהוא אינו תומך באף צורה של זרימת בקרה; היא גם אוסרת פרסום של load הצהרות. ההוראות לתמיכה בקבצים מסוג MODULE.bazel הן:

פורמט גרסה

לבזל יש סביבה עסקית מגוונת ופרויקטים שבהם משתמשים בשיטות שונות של ניהול גרסאות. הפופולריות ביותר היא ללא ספק SemVer, אך יש גם פרויקטים בולטים המשתמשים בהונאות שונות כגון Abseil, שהגרסאות שלה מבוסס-תאריך, לדוגמה 20210324.2).

מסיבה זו, Bzlmod מאמץ גרסה רגועה יותר של המפרט של SemVer, ובמיוחד מאפשר מספר כלשהו של רצפים של ספרות בחלק "release" של הגרסה (במקום 3 בדיוק כפי ש-SemVer מציין: MAJOR.MINOR.PATCH). בנוסף, הסמנטיקה של הגדלות במספרים גדולים, קלים ושולי גרסאות אינה נאכפת. (עם זאת, ראה רמת תאימות לפרטים על האופן שבו אנו מציינים תאימות). חלקים אחרים במפרט SemVer, כגון מקף המציין גרסת טרום-השקה, לא עברו שינוי.

רזולוציית גרסה

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

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

באיזו גרסת D להשתמש? כדי לפתור את השאלה הזו, Bzlmod משתמש באלגוריתם מבחר גרסאות מינימלי (MVS) המוצג במערכת מודולים של Go. פרוטוקול MVS מניח שכל הגרסאות החדשות של מודול מסוים תואמות לאחור, וכך בוחרות את הגרסה הגבוהה ביותר שמצוינת על ידי גורם תלוי (D 1.1 בדוגמה שלנו). התמונה נקראת "מינימלית" מאחר ש-D 1.1 כאן היא הגרסה המינימלית שיכולה לעמוד בדרישות שלנו; גם אם קיימת D 1.2 ואילך, אנחנו לא בוחרים אותן. יש לכך יתרון נוסף, שבחירת הגרסה היא אמינות גבוהה ויכולת שחזור.

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

רמת תאימות

שימו לב: ההנחה של MVS בנוגע לתאימות לאחור היא אפשרית, מפני שהיא פשוט מתייחסת לגרסאות קודמות של מודול שאינן תואמות כמודול נפרד. במונחים של SemVer, הפירוש של A 1.x ו-A 2.x הוא מודולים נפרדים, והוא יכול להתקיים בו-זמנית בתרשים תלות המפוענח. באופן זה, הדבר אפשרי על ידי העובדה שהגרסה הראשית מקודדת בנתיב החבילות ב-Go, כך שאין התנגשויות בין קומפילציה או זמן קישור.

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

שמות של מאגר

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

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

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

    • עבור מאגר מודולי Bazel: module_name.version
      (דוגמה. @bazel_skylib.1.0.3)
    • עבור המאגר של תוסף המודול: module_name.version.extension_name.repo_name
      (דוגמה. @rules_cc.0.0.1.cc_configure.local_config_cc)
  • שם של מאגר מקומי: שם המאגר שיש להשתמש בו בקובצי BUILD ו-.bzl בתוך מאגר. לאותו תלות עשויים להיות שמות מקומיים שונים עבור מאגרים שונים.
    הוא נקבע כך:

    • עבור מאגר מודולי Bazel: module_name כברירת מחדל, או השם שצוין על ידי המאפיין repo_name ב- bazel_dep.
    • עבור המאגר של תוסף המודול: שם המאגר שהוצג באמצעות use_repo.

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

צלילים קפדניים

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

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

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

מרשמים

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

רישום אינדקס

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

רישום אינדקס חייב להתאים לפורמט הבא:

  • /bazel_registry.json: קובץ JSON שמכיל מטא-נתונים של המרשם. בשלב זה, יש בו רק מפתח אחד, mirrors, המציין את רשימת המראות שבהן יש להשתמש עבור ארכיוני המקור.
  • /modules: ספרייה המכילה ספריית משנה עבור כל מודול במאגר זה.
  • /modules/$MODULE: ספרייה שמכילה ספריית משנה לכל גרסה של המודול הזה, וכן הקובץ הבא:
    • metadata.json: קובץ JSON שמכיל מידע על המודול הזה, עם השדות הבאים:
      • homepage: כתובת ה-URL של דף הבית של הפרויקט.
      • maintainers: רשימה של אובייקטי JSON, שכל אחד מהם תואם למידע של מתחזק של המודול במרשם. הערה זו לא זהה בהכרח למחברים של הפרויקט.
      • versions: ניתן למצוא רשימה של כל הגרסאות של מודול זה בקובץ הרישום הזה.
      • yanked_versions: רשימה של גרסאותשל מודול זה. בשלב זה אין צורך באתר, אבל בעתיד, גרסאות מוסתרות הן מודעות שידלגו עליהן או יניבו שגיאה.
  • /modules/$MODULE/$VERSION: ספרייה שמכילה את הקבצים הבאים:
    • MODULE.bazel: קובץ ה-MODULE.bazel של גרסת המודול הזו.
    • source.json: קובץ JSON שמכיל מידע על תהליך אחזור המקור של גרסת המודול הזו, עם השדות הבאים:
      • url: כתובת ה-URL של ארכיון המקור.
      • integrity: סיכום התקינות של מקורות המשנה סיכום הארכיון.
      • strip_prefix: קידומת ספרייה להסרה בזמן חילוץ ארכיון המקור.
      • patches: רשימת מחרוזות, שכל אחת מהן נותנת שם לקובץ תיקון שיש להחיל על הארכיון שחולץ. קובצי התיקון נמצאים בספרייה /modules/$MODULE/$VERSION/patches.
      • patch_strip: זהה לארגומנט --strip של תיקון Unix.
    • patches/: ספרייה אופציונלית המכילה קובצי תיקונים.

רישום מרכזי של בזל

Bazel Central Registry (BCR) הוא רישום אינדקס הממוקם בכתובת registry.bazel.build. התוכן שלו מגובה על ידי מאגר GitHub bazelbuild/bazel-central-registry.

ה-BCR נשמר על ידי קהילת בזל; תורמי תוכן מוזמנים להגיש בקשות למשיכה. מידע נוסף זמין במדיניות ובהליכים של רישום מרכזי בבזל.

בנוסף לפורמט של רישום אינדקס רגיל, ה-BCR דורש קובץ presubmit.yml לכל גרסת מודול (/modules/$MODULE/$VERSION/presubmit.yml). קובץ זה מציין מספר מטרות חיוניות לבנייה ובדיקה שניתן להשתמש בהן כדי לבדוק את מידת החוקיות של גרסת מודול זה, ומשמשת את צינורות ה-CI של ה-BCR כדי להבטיח יכולת פעולה הדדית בין מודולים ב-BCR הנתונים.

בחירת מרשמים

אפשר להשתמש בסימון --registry מרשמים קודמים מוקדמים לקדימות. לנוחיותך, אפשר להוסיף רשימה של --registry סימונים בקובץ .bazelrc של הפרויקט.

תוספים למודולים

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

תוספי מודולים מוגדרים בקובצי .bzl, בדיוק כמו כללי repo או פקודות מאקרו של WORKSPACE. הן לא מופעלות באופן ישיר; במקום זאת, כל מודול יכול לציין חלקים של נתונים הנקראים תגים כדי שתוספים יקראו. לאחר מכן, לאחר שמסתיימת רזולוציה של גרסת מודול, תוספי המודול מופעלים. כל תוסף רץ פעם אחת אחרי רזולוציית המודול (עדיין לפני שקורה כל דגם), והוא קורא את כל התגים ששייכים לו בכל תרשים התלות.

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

בתרשים התלות לדוגמה שלמעלה, A 1.1 ו-B 1.2 וכו' הם מודולים של Bazel. ניתן לחשוב על כל אחד מהם כקובץ MODULE.bazel. כל מודול יכול לציין כמה תגים עבור תוספי מודולים; כאן חלקן צוינו עבור התוסף "Maven", וחלקן צוינו עבור " ומטען". אחרי שתרשים התלות הזה סופי (לדוגמה, ייתכן של-B 1.2 יש בפועל bazel_dep ב-D 1.3 אבל הוא שודרג ל-D 1.4 בגלל C), תוספים "Maven" פועל, והוא קורא את כל תגי maven.*, ומשתמש במידע כדי להחליט איזה מאגר ליצור. באופן דומה עבור התוסף "מטען".

שימוש בתוספים

התוספים מתארחים במודולים של Bazel בעצמם, כך שכדי להשתמש בתוסף במודול, תחילה עליך להוסיף bazel_dep במודול הזה, ולאחר מכן לקרוא ל-use_extension פונקציה מובנית שיסייעה לו לתחום את היקף המידע. ניקח לדוגמה את הדוגמה הבאה, קטע טקסט מקובץ MODULE.bazel כדי להשתמש בתוסף היפותטי "Maven" שהוגדר במודול rules_jvm_external:

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

לאחר הגדרת התוסף בהיקף, תוכלו להשתמש בתחביר הנקודה כדי לציין תגים עבורו. שימו לב שהתגים צריכים להתאים לסכימה שהוגדרה על ידי מחלקות התגים התואמות (ראו הגדרת תוסף בהמשך). הנה דוגמה שמציינת כמה תגים של maven.dep ושל maven.pom.

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

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

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

ה-repos שנוצר על ידי תוסף הוא חלק מה-API שלו, ולכן מהתגים שציינת, עליך לדעת שהתוסף "maven" עומד ליצור repo בשם "org_junit_junit", ופרמטר בשם "com_google_guava_guava" עם use_repo, אפשר לבחור לשנות את השם שלהם בהיקף המודול, למשל "גויאבה" כאן.

הגדרה של תוסף

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

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

ההצהרות האלה מבהירות שניתן לציין את התגים maven.dep ו-maven.pom, באמצעות סכימת המאפיינים שהוגדרה למעלה.

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

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

בדוגמה שלמעלה, אנחנו עוברים על כל המודולים בתרשים התלות (ctx.modules), וכל אחד מהם הוא אובייקט bazel_module שה-tags שלו חושף את כל תגי maven.* במודול. לאחר מכן, אנחנו מפעילים את CLI" לבסוף, אנחנו משתמשים ברזולוציה כדי ליצור מספר מאגרים, באמצעות כלל ההיפותזה maven_single_jar.