מחזיקי כלים

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

מוטיבציה

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

bar_binary = rule(
    implementation = _bar_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        ...
        "_compiler": attr.label(
            default = "//bar_tools:barc_linux",  # the compiler running on linux
            providers = [BarcInfo],
        ),
    },
)

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

BarcInfo = provider(
    doc = "Information about how to invoke the barc compiler.",
    # In the real world, compiler_path and system_lib might hold File objects,
    # but for simplicity they are strings for this example. arch_flags is a list
    # of strings.
    fields = ["compiler_path", "system_lib", "arch_flags"],
)

def _bar_binary_impl(ctx):
    ...
    info = ctx.attr._compiler[BarcInfo]
    command = "%s -l %s %s" % (
        info.compiler_path,
        info.system_lib,
        " ".join(info.arch_flags),
    )
    ...

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

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

bar_binary(
    name = "myprog_on_linux",
    srcs = ["mysrc.bar"],
    compiler = "//bar_tools:barc_linux",
)

bar_binary(
    name = "myprog_on_windows",
    srcs = ["mysrc.bar"],
    compiler = "//bar_tools:barc_windows",
)

אפשר לשפר את הפתרון הזה על ידי שימוש ב-select כדי לבחור את compiler בהתאם לפלטפורמה:

config_setting(
    name = "on_linux",
    constraint_values = [
        "@platforms//os:linux",
    ],
)

config_setting(
    name = "on_windows",
    constraint_values = [
        "@platforms//os:windows",
    ],
)

bar_binary(
    name = "myprog",
    srcs = ["mysrc.bar"],
    compiler = select({
        ":on_linux": "//bar_tools:barc_linux",
        ":on_windows": "//bar_tools:barc_windows",
    }),
)

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

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

כללי כתיבה שמשתמשים בכלים

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

# By convention, toolchain_type targets are named "toolchain_type" and
# distinguished by their package path. So the full path for this would be
# //bar_tools:toolchain_type.
toolchain_type(name = "toolchain_type")

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

bar_binary = rule(
    implementation = _bar_binary_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        ...
        # No `_compiler` attribute anymore.
    },
    toolchains = ["//bar_tools:toolchain_type"],
)

מעכשיו, פונקציית ההטמעה ניגשת לתלות הזו תחת ctx.toolchains במקום ctx.attr, ומשתמשת בסוג המפתח מחזיק כלים.

def _bar_binary_impl(ctx):
    ...
    info = ctx.toolchains["//bar_tools:toolchain_type"].barcinfo
    # The rest is unchanged.
    command = "%s -l %s %s" % (
        info.compiler_path,
        info.system_lib,
        " ".join(info.arch_flags),
    )
    ...

ctx.toolchains["//bar_tools:toolchain_type"] מחזיר את ספק ToolchainInfo של כל יעד ש-Bazel פתר את התלות בכלי הכלים. השדות של האובייקט ToolchainInfo מוגדרים על ידי הכלל של הכלי הבסיסי; בקטע הבא, הכלל הזה מוגדר כך שיש שדה barcinfo שעוטף אובייקט BarcInfo.

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

מחזיקי כלים אופציונליים ואופציונליים

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

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

bar_binary = rule(
    ...
    toolchains = [
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

כשלא ניתן לפתור סוג אופציונלי של ארגז כלים, הניתוח ממשיך, והתוצאה של ctx.toolchains[""//bar_tools:toolchain_type"] היא None.

ברירת המחדל של הפונקציה config_common.toolchain_type היא חובה.

ניתן להשתמש בטפסים הבאים:

  • סוגי חובה של כלי עבודה:
    • toolchains = ["//bar_tools:toolchain_type"]
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type")]
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = True)]
  • סוגים אופציונליים של מחזיקי כלים:
    • toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False)]
bar_binary = rule(
    ...
    toolchains = [
        "//foo_tools:toolchain_type",
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

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

היבטים בכתיבה שמשתמשים בערכות כלים

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

bar_aspect = aspect(
    implementation = _bar_aspect_impl,
    attrs = {},
    toolchains = ['//bar_tools:toolchain_type'],
)

def _bar_aspect_impl(target, ctx):
  toolchain = ctx.toolchains['//bar_tools:toolchain_type']
  # Use the toolchain provider like in a rule.
  return []

הגדרת ארגזי כלים

כדי להגדיר מחזיקי כלים מסוימים עבור סוג מסוים של מחזיק כלים, צריך שלושה דברים:

  1. כלל ספציפי לשפה שמייצג את סוג הכלי או חבילת הכלים. לפי המוסכמה, שם הכלל הזה מסתיים ב-"_toolschain".

    1. הערה: הכלל \_toolchain לא יכול ליצור פעולות build. במקום זאת, הוא אוסף פריטים מכללים אחרים ומעביר אותם לכלל שמשתמש בשרשרת הכלים. כלל זה אחראי ליצירת כל פעולות הבנייה.
  2. מספר מטרות מסוג הכלל הזה, המייצגות גרסאות של הכלי או של כלים לפלטפורמות שונות.

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

בדוגמה שלנו לריצה, יש הגדרה לכלל bar_toolchain. בדוגמה שלנו יש רק מהדר, אבל אפשר לקבץ מתחתיו כלים אחרים כמו קישור.

def _bar_toolchain_impl(ctx):
    toolchain_info = platform_common.ToolchainInfo(
        barcinfo = BarcInfo(
            compiler_path = ctx.attr.compiler_path,
            system_lib = ctx.attr.system_lib,
            arch_flags = ctx.attr.arch_flags,
        ),
    )
    return [toolchain_info]

bar_toolchain = rule(
    implementation = _bar_toolchain_impl,
    attrs = {
        "compiler_path": attr.string(),
        "system_lib": attr.string(),
        "arch_flags": attr.string_list(),
    },
)

הכלל צריך להחזיר ספק ToolchainInfo, שמגדיר את האובייקט שהכלל שמאחזר מאחזר באמצעות ctx.toolchains ואת התווית של סוג שרשרת הכלים. ToolchainInfo, כמו struct, יכול להכיל זוגות שרירותיים של ערכי-ערך. התיעוד המדויק של השדות שנוספו ל-ToolchainInfo צריך להיות מתועד באופן ברור בסוג הכלי. בדוגמה הזו, הערכים המוחזרים באובייקט BarcInfo משמשים לשימוש מחדש בסכימה שהוגדרה למעלה; הסגנון הזה עשוי להיות שימושי לאימות ולשימוש חוזר בקוד.

מעכשיו אפשר להגדיר יעדים של מהדרים ספציפיים מסוג barc.

bar_toolchain(
    name = "barc_linux",
    arch_flags = [
        "--arch=Linux",
        "--debug_everything",
    ],
    compiler_path = "/path/to/barc/on/linux",
    system_lib = "/usr/lib/libbarc.so",
)

bar_toolchain(
    name = "barc_windows",
    arch_flags = [
        "--arch=Windows",
        # Different flags, no debug support on windows.
    ],
    compiler_path = "C:\\path\\on\\windows\\barc.exe",
    system_lib = "C:\\path\\on\\windows\\barclib.dll",
)

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

toolchain(
    name = "barc_linux_toolchain",
    exec_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    target_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":barc_linux",
    toolchain_type = ":toolchain_type",
)

toolchain(
    name = "barc_windows_toolchain",
    exec_compatible_with = [
        "@platforms//os:windows",
        "@platforms//cpu:x86_64",
    ],
    target_compatible_with = [
        "@platforms//os:windows",
        "@platforms//cpu:x86_64",
    ],
    toolchain = ":barc_windows",
    toolchain_type = ":toolchain_type",
)

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

כדי לראות דוגמה מהמציאות, כדאי לעיין בgo_toolchain.

ארגזי כלים והגדרות

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

הנה גרסה מורכבת יותר של bar_toolchain:

def _bar_toolchain_impl(ctx):
    # The implementation is mostly the same as above, so skipping.
    pass

bar_toolchain = rule(
    implementation = _bar_toolchain_impl,
    attrs = {
        "compiler": attr.label(
            executable = True,
            mandatory = True,
            cfg = "exec",
        ),
        "system_lib": attr.label(
            mandatory = True,
            cfg = "target",
        ),
        "arch_flags": attr.string_list(),
    },
)

השימוש ב-attr.label זהה לכלל סטנדרטי, אבל המשמעות של הפרמטר cfg שונה מעט.

התלות מיעד (שנקרא "ההורה") למשאב כלים דרך רזולוציה של Toolchain משתמש במעבר מיוחד של הגדרות שנקרא "מעבר כלי". המעבר של Toolchain שומר את ההגדרה ללא שינוי, מלבד כך היא מאלצת את פלטפורמת ההפעלה להיות זהה ליכולת השימוש ברכיב Toolchain (אחרת, רזולוציית כלי הכלים יכולה לבחור כל פלטפורמת ביצוע, ולא בהכרח יהיה זהה לזה של ההורה). פעולה זו מאפשרת ביצוע של פעולות תלויות exec בכלי גם בפעולות הבנייה של ההורה. כל אחת מהתלויות של הכלי שבו נעשה שימוש ב-cfg = "target" (או שלא מציינים את cfg, כי ברירת המחדל היא 'יעד'), נוצרות עבור אותה פלטפורמת יעד כמו ההורה. כך כללים ל'כלים' יכולים לתרום ספריות (המאפיין system_lib שלמעלה) וגם כלים (המאפיין compiler) לכללי הבנייה שצריכים אותם. ספריות המערכת מקושרות לפריטי המידע הסופיים, ולכן צריך לבנות אותן לאותה פלטפורמה, בעוד שהמהדר הוא כלי שמופעל במהלך ה-build, וצריך להפעיל אותו על פלטפורמת הפעלה.

רישום ובנייה באמצעות מחזיקי כלים

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

register_toolchains(
    "//bar_tools:barc_linux_toolchain",
    "//bar_tools:barc_windows_toolchain",
    # Target patterns are also permitted, so you could have also written:
    # "//bar_tools:all",
)

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

# my_pkg/BUILD

platform(
    name = "my_target_platform",
    constraint_values = [
        "@platforms//os:linux",
    ],
)

bar_binary(
    name = "my_bar_binary",
    ...
)
bazel build //my_pkg:my_bar_binary --platforms=//my_pkg:my_target_platform

מאת Bazel יראו ש//my_pkg:my_bar_binary נבנה באמצעות פלטפורמה שיש בה @platforms//os:linux, ולכן מתבצע פתרון לבעיה //bar_tools:toolchain_type של ההפניה אל //bar_tools:barc_linux_toolchain. בסופו של דבר, המבנה הזה יהיה //bar_tools:barc_linux אך לא //bar_tools:barc_windows.

רזולוציית כלים

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

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

השלבים לפתרון הבעיה:

  1. סעיף target_compatible_with או exec_compatible_with תואם לפלטפורמה אם, עבור כל constraint_value ברשימה שלה, הפלטפורמה כוללת גם את constraint_value זה (או כברירת מחדל).

    אם הפלטפורמה מכילה constraint_value שניות מתוך constraint_setting, שהסעיפים לא מפנים אליהן, אין לכך השפעה על התאמה.

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

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

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

פלטפורמת הביצוע שנבחרה משמשת להפעלת כל הפעולות שהיעד יוצר.

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

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

ניפוי באגים באמצעות ארגזי כלים

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

אם ברצונך לראות אילו יחסי תלות של cquery מגיעים מהרזולוציה של Toolchain, יש להשתמש בסימון --transitions של cquery:

# Find all direct dependencies of //cc:my_cc_lib. This includes explicitly
# declared dependencies, implicit dependencies, and toolchain dependencies.
$ bazel cquery 'deps(//cc:my_cc_lib, 1)'
//cc:my_cc_lib (96d6638)
@bazel_tools//tools/cpp:toolchain (96d6638)
@bazel_tools//tools/def_parser:def_parser (HOST)
//cc:my_cc_dep (96d6638)
@local_config_platform//:host (96d6638)
@bazel_tools//tools/cpp:toolchain_type (96d6638)
//:default_host_platform (96d6638)
@local_config_cc//:cc-compiler-k8 (HOST)
//cc:my_cc_lib.cc (null)
@bazel_tools//tools/cpp:grep-includes (HOST)

# Which of these are from toolchain resolution?
$ bazel cquery 'deps(//cc:my_cc_lib, 1)' --transitions=lite | grep "toolchain dependency"
  [toolchain dependency]#@local_config_cc//:cc-compiler-k8#HostTransition -> b6df211