הדף הזה מתאר את המסגרת של 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 []
הגדרת ארגזי כלים
כדי להגדיר מחזיקי כלים מסוימים עבור סוג מסוים של מחזיק כלים, צריך שלושה דברים:
כלל ספציפי לשפה שמייצג את סוג הכלי או חבילת הכלים. לפי המוסכמה, שם הכלל הזה מסתיים ב-"_toolschain".
- הערה: הכלל
\_toolchain
לא יכול ליצור פעולות build. במקום זאת, הוא אוסף פריטים מכללים אחרים ומעביר אותם לכלל שמשתמש בשרשרת הכלים. כלל זה אחראי ליצירת כל פעולות הבנייה.
- הערה: הכלל
מספר מטרות מסוג הכלל הזה, המייצגות גרסאות של הכלי או של כלים לפלטפורמות שונות.
לכל יעד כזה, יעד משויך לכלל
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
.
פלטפורמת המארח כלולה באופן אוטומטי כפלטפורמת ביצוע זמינה.
מתבצע מעקב אחר פלטפורמות וארגזי כלים זמינים כרשימות מסודרות לפי סדר נטישה,
עם העדפה לפריטים קודמים ברשימה.
השלבים לפתרון הבעיה:
סעיף
target_compatible_with
אוexec_compatible_with
תואם לפלטפורמה אם, עבור כלconstraint_value
ברשימה שלה, הפלטפורמה כוללת גם אתconstraint_value
זה (או כברירת מחדל).אם הפלטפורמה מכילה
constraint_value
שניות מתוךconstraint_setting
, שהסעיפים לא מפנים אליהן, אין לכך השפעה על התאמה.אם היעד שנוצר מציין
exec_compatible_with
מאפיין (או שהגדרת הכלל שלו מציינת אתexec_compatible_with
ארגומנט ), הרשימה של פלטפורמות הביצוע הזמינות מסוננת כדי להסיר את כל הפלטפורמות שלא תואמות להגבלות הביצוע.לכל פלטפורמת הפעלה זמינה, אתם משייכים כל סוג של רצועת כלים ל כלי הכלים הזמין הראשון, אם יש כזה, שתואם לפלטפורמת ההפעלה הזו ולפלטפורמת היעד.
כל פלטפורמת ביצוע שלא הצליחה למצוא מחזיק כלים תואם לאחד מסוגי הכלים שלה מבוטלת. מבין הפלטפורמות הנותרות, הפלטפורמה הראשונה הופכת לפלטפורמת ההפעלה הנוכחית של היעד, ושרשראות הכלים המשויכים אליה (אם יש כאלה) הופכות לתלויות ביעד.
פלטפורמת הביצוע שנבחרה משמשת להפעלת כל הפעולות שהיעד יוצר.
במקרים שבהם ניתן לבנות את אותו יעד בתצורות מרובות (למשל, עבור מעבדים שונים) באותו בניין, ההליך של החלת פתרון חל על כל גרסה של היעד.
אם הכלל משתמש בקבוצות ביצוע, כל קבוצת הפעלה מבצעת רזולוציה של כלי עבודה נפרד, ולכל אחת מהן יש פלטפורמת הפעלה ושרשרת כלים משלה.
ניפוי באגים באמצעות ארגזי כלים
אם ברצונך להוסיף תמיכה בארגז כלים לכלל קיים, יש להשתמש בסימון --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