כללים

כלל מגדיר סדרה של פעולות ש-Bazel מבצעת על קלטות כדי ליצור קבוצה של תוצרי קלט, המוזכרים לדוגמה, כלל C+++ יכול:

  1. מגדירים קבוצה של .cpp קובצי מקור (קלט).
  2. הפעלת g++ בקובצי המקור (פעולה).
  3. יש להחזיר את הספק DefaultInfo עם פלט קובץ ההפעלה וקבצים אחרים כדי שיהיו זמינים בזמן הריצה.
  4. יש להחזיר את הספק CcInfo עם מידע ספציפי ל-C++ שנאסף מהיעד ומהתלויים בו.

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

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

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

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

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

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

יצירת כלל

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

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "deps": attr.label_list(),
        ...
    },
)

פעולה זו מגדירה סוג של כלל בשם example_library.

כמו כן, הקריאה ל-rule צריכה לציין אם הכלל יוצר פלט קובץ הפעלה (עם executable=True), או באופן ספציפי קובץ הפעלה (עם test=True). אם הכלל האחרון הוא כלל לבדיקה, ושם הכלל חייב להסתיים ב-_test.

מדידת יעד

ניתן לטעון כללים ולהפעיל אותם ב-BUILD קבצים:

load('//some/pkg:rules.bzl', 'example_library')

example_library(
    name = "example_target",
    deps = [":another_target"],
    ...
)

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

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

מאפיינים

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

כדי להגדיר מאפיינים ספציפיים לכלל, כמו srcs או deps, אפשר להעביר מפה משמות מאפיינים אל סכימות (שנוצרו באמצעות המודול attr) אל הפרמטר attrs של rule. מאפיינים נפוצים, כגון name ו-visibility, נוספים באופן לא מפורש לכל הכללים. מאפיינים נוספים מתווספים באופן לא מפורש לכללי הפעלה ובדיקות באופן ספציפי. אי אפשר לכלול במילון את המאפיינים שנוספו לכלל באופן מרומז ל-attrs.

מאפייני תלות

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

  • srcs מציין קובצי מקור שעובדו על ידי פעולות של יעד. ברוב המקרים, סכימת המאפיינים מציינת אילו סיומות קבצים צפויות עבור סוג קובץ המקור שהכלל מעבד. כללים לשפות עם קובצי כותרת בדרך כלל מציינים מאפיין hdrs נפרד לכותרות שעובדו על ידי יעד ועל ידי הצרכנים שלו.
  • המדיניות deps מציינת יחסי תלות של קוד עבור יעד. בסכימת המאפיינים צריך לציין אילו ספקים יחסי תלות אלה צריכים לספק. (לדוגמה, cc_library מספק את CcInfo).
  • ב-data מצוין אילו קבצים יהיו זמינים בזמן ריצה לכל קובץ הפעלה, שתלוי ביעד. כך יש לציין קבצים שרירותיים.
example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = [".example"]),
        "hdrs": attr.label_list(allow_files = [".header"]),
        "deps": attr.label_list(providers = [ExampleInfo]),
        "data": attr.label_list(allow_files = True),
        ...
    },
)

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

example_library(
    name = "my_target",
    deps = [":other_target"],
)

example_library(
    name = "other_target",
    ...
)

בדוגמה הזו, other_target תלויה ב-my_target, ולכן other_target נותח קודם. זו שגיאה אם יש מחזור בתרשים התאימות.

מאפיינים פרטיים וקשרים מרומזים

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

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

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        ...
        "_compiler": attr.label(
            default = Label("//tools:example_compiler"),
            allow_single_file = True,
            executable = True,
            cfg = "exec",
        ),
    },
)

בדוגמה זו, לכל יעד מסוג example_library יש תלות מרומזת במהדר //tools:example_compiler. כך, פונקציית ההטמעה של example_library יכולה ליצור פעולות שמפעילות את המהדר, למרות שהמשתמש לא העביר את התווית שלו כקלט. מאחר ש-_compiler הוא מאפיין פרטי, ctx.attr._compiler תמיד מצביע על //tools:example_compiler בכל היעדים של סוג הכלל הזה. לחלופין, תוכלו לתת שם למאפיין compiler ללא קו תחתון ולשמור על ערך ברירת המחדל. כך המשתמשים יכולים להחליף מהדר אחר במידת הצורך, אבל לא נדרשת מודעוּת לתווית של המהדר.

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

מאפייני הפלט

מאפייני פלט, כגון attr.output ו-attr.output_list, מצהירים על קובץ פלט שהיעד יוצר. ההבדלים בין המאפיינים האלה תלויים בשתי דרכים:

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

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

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

פונקציית הטמעה

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

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

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

יעדים

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

ב-ctx.attr יש שדות שתואמים לשמות של כל מאפיין תלות, והוא מכיל Target אובייקטים שמייצגים כל תלות ישירה דרך המאפיין הזה. למאפיינים של label_list, זוהי רשימה של Targets. במאפיינים label, צריך לציין Target או None יחידים.

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

return [ExampleInfo(headers = depset(...))]

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

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

def _example_library_impl(ctx):
    ...
    transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]

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

return struct(example_info = struct(headers = depset(...)))

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

transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]

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

קבצים

הקבצים מיוצגים על ידי אובייקטים מסוג File. מכיוון ש-Bazel לא מבצעת I/O של הקובץ בשלב הניתוח, לא ניתן להשתמש באובייקטים האלה כדי לקרוא או לכתוב ישירות תוכן. במקום זאת, הם מועברים לפונקציות פולטות פעולה (ראו ctx.actions) כדי לבנות חלקים בתרשים הפעולות.

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

בכל מאפיין תלות, השדה המתאים של ctx.files מכיל רשימה של פלטי ברירת מחדל מכל יחסי תלות דרך המאפיין הזה:

def _example_library_impl(ctx):
    ...
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    ...

ctx.file מכיל פרמטר File או None יחיד למאפייני תלות שהמפרטים שלהם מגדירים allow_single_file=True. ctx.executable פועל באותו אופן כמו ctx.file, אבל הוא מכיל רק שדות עבור מאפייני תלות שהמפרטים שלהם מגדירים executable=True.

הצהרה על פלט

במהלך שלב הניתוח, פונקציית הטמעה של כלל יכולה ליצור פלט. מכיוון שכל התוויות צריכות להיות ידועות בשלב הטעינה, לפלטים הנוספים האלה אין תוויות. אפשר ליצור File אובייקטים באמצעות פלט באמצעות ctx.actions.declare_file ו-ctx.actions.declare_directory. לעיתים קרובות, שמות הפלט מבוססים על שם היעד&#39, ctx.label.name:

def _example_library_impl(ctx):
  ...
  output_file = ctx.actions.declare_file(ctx.label.name + ".output")
  ...

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

פעולות

פעולה מתארת איך ליצור קבוצת פלטים מקבוצה של קלט, לדוגמה "run gcc on hello.c ומקבלים hello.o". כשיוצרים פעולה, Bazel לא מפעילה את הפקודה מיד. היא רושמת אותה בתרשים של יחסי תלות, כי פעולה מסוימת יכולה להיות תלויה בפלט של פעולה אחרת. לדוגמה, ב-C, צריך לקשר את המקשר אחרי המהדר.

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

אפשר להשתמש ב-ctx.actions.args כדי לצבור ביעילות את הארגומנטים של פעולות. היא נמנעת משטפונות עד לשעת הביצוע:

def _example_library_impl(ctx):
    ...

    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    inputs = depset(srcs, transitive=[headers])
    output_file = ctx.actions.declare_file(ctx.label.name + ".output")

    args = ctx.actions.args()
    args.add_joined("-h", headers, join_with=",")
    args.add_joined("-s", srcs, join_with=",")
    args.add("-o", output_file)

    ctx.actions.run(
        mnemonic = "ExampleCompile",
        executable = ctx.executable._compiler,
        arguments = [args],
        inputs = inputs,
        outputs = [output_file],
    )
    ...

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

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

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

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

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

ספקים

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

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

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

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

פלט ברירת מחדל

פלט ברירת מחדל של יעד הוא התפוקה שנדרשת כברירת מחדל כאשר המטרה היא בקשת build בשורת הפקודה. לדוגמה, בשדה java_library //pkg:foo, היעד foo.jar הוא פלט ברירת מחדל, כך שהפקודה תיבנה על ידי הפקודה bazel build //pkg:foo.

פלט ברירת המחדל מצוין על ידי הפרמטר files של DefaultInfo:

def _example_library_impl(ctx):
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        ...
    ]

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

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

קובצי Runrun

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

אפשר להוסיף קובצי Runrun באופן ידני במהלך יצירת הכלל. ניתן ליצור runfiles אובייקטים בשיטת runfiles בהקשר של הכלל, ctx.runfiles ולהעביר אותם לפרמטר runfiles ב-DefaultInfo. פלט ההפעלה של כללי ההפעלה נוסף באופן לא מפורש בקובצי הריצה.

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

def _example_library_impl(ctx):
    ...
    runfiles = ctx.runfiles(files = ctx.files.data)
    transitive_runfiles = []
    for runfiles_attr in (
        ctx.attr.srcs,
        ctx.attr.hdrs,
        ctx.attr.deps,
        ctx.attr.data,
    ):
        for target in runfiles_attr:
            transitive_runfiles.append(target[DefaultInfo].default_runfiles)
    runfiles = runfiles.merge_all(transitive_runfiles)
    return [
        DefaultInfo(..., runfiles = runfiles),
        ...
    ]

ספקים בהתאמה אישית

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

ExampleInfo = provider(
    "Info needed to compile/link Example code.",
    fields={
        "headers": "depset of header Files from transitive dependencies.",
        "files_to_link": "depset of Files from compilation.",
    })

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

def _example_library_impl(ctx):
  ...
  return [
      ...
      ExampleInfo(
          headers = headers,
          files_to_link = depset(
              [output_file],
              transitive = [
                  dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
              ],
          ),
      )
  ]
אתחול מותאם אישית של ספקים

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

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

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

לעומת זאת, הבנאי הגולמי יעקוף את הקריאה החוזרת (init) של השיחה.

הדוגמה הבאה משתמשת ב-init כדי לעבד מראש ולאמת את הארגומנטים שלה:

# //pkg:exampleinfo.bzl

_core_headers = [...]  # private constant representing standard library files

# It's possible to define an init accepting positional arguments, but
# keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
    if not files_to_link and not allow_empty_files_to_link:
        fail("files_to_link may not be empty")
    all_headers = depset(_core_headers, transitive = headers)
    return {'files_to_link': files_to_link, 'headers': all_headers}

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init)

export ExampleInfo

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

    ExampleInfo(
        files_to_link=my_files_to_link,  # may not be empty
        headers = my_headers,  # will automatically include the core headers
    )

אפשר להשתמש בבנייה הגולמית כדי להגדיר פונקציות ציבוריות חלופיות שלא עברו דרך הלוגיקה של init. לדוגמה, ב-exampleinfo.bzl אנחנו יכולים להגדיר:

def make_barebones_exampleinfo(headers):
    """Returns an ExampleInfo with no files_to_link and only the specified headers."""
    return _new_exampleinfo(files_to_link = depset(), headers = all_headers)

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

שימוש נוסף ב-init הוא פשוט למנוע מהמשתמש להתקשר לספק באופן גורף ולאלץ אותו להשתמש בפונקציית מפעל במקום זאת:

def _exampleinfo_init_banned(*args, **kwargs):
    fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init_banned)

def make_exampleinfo(...):
    ...
    return _new_exampleinfo(...)

כללי הפעלה וכללי בדיקה

כללי ההפעלה מגדירים יעדים שניתן להפעיל באמצעות פקודת bazel run. כללי בדיקה הם סוג ייחודי של כלל הפעלה, שניתן להפעיל גם את היעדים שלו באמצעות פקודת bazel test. כדי להפעיל את כללי ההפעלה והבדיקה יש להגדיר את הארגומנט התואם executable או את הארגומנט test כ-True בקריאה ל-rule:

example_binary = rule(
   implementation = _example_binary_impl,
   executable = True,
   ...
)

example_test = rule(
   implementation = _example_binary_impl,
   test = True,
   ...
)

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

שני סוגי הכללים חייבים ליצור קובץ פלט להפעלה (שניתן, או לא ניתן להצהיר עליו מראש), שיופעל על ידי הפקודות run או test. כדי לקבוע ל-Bazal איזה כלל של פלט צריך לשמש כקובץ ההפעלה הזה, צריך להעביר אותו כארגומנט של ספק executable שהוחזר DefaultInfo. הפונקציה executable מתווספת לפלט ברירת המחדל של הכלל (כך שאין צורך להעביר אותה גם ל-executable וגם ל-files). היא גם משתמעת באופן לא מפורש ל-runfiles:

def _example_binary_impl(ctx):
    executable = ctx.actions.declare_file(ctx.label.name)
    ...
    return [
        DefaultInfo(executable = executable, ...),
        ...
    ]

הפעולה שיוצרת את הקובץ הזה צריכה להגדיר את קטע ההפעלה בקובץ. פעולה ctx.actions.run או ctx.actions.run_shell צריכה להתבצע על ידי הכלי הבסיסי שמפעיל את הפעולה. עבור פעולה מסוג ctx.actions.write, צריך לעבור את is_executable=True.

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

דוגמאות של כלל הפעלה וכלל לבדיקה.

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

def example_test(size="small", **kwargs):
  _example_test(size=size, **kwargs)

_example_test = rule(
 ...
)

מיקום ה-runfiles

כשיעד להפעלה מופעל עם bazel run (או test), השורש של ספריית קובצי הרצה פועל בסמוך לקובץ ההפעלה. המסלולים קשורים באופן הבא:

# Given executable_file and runfile_file:
runfiles_root = executable_file.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
    runfiles_root, workspace_name, runfile_path)

הנתיב אל File בספריית קובצי הרצה תואם לדומיין File.short_path.

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

נושאים מתקדמים

בקשה לקובצי פלט

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

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

בנוסף לפלט המוגדר כברירת מחדל, יש קבוצות פלט שהן אוספים של קובצי פלט שעשויים להישלח יחד. אפשר לבקש את זה באמצעות --output_groups. לדוגמה, אם יעד //pkg:mytarget הוא מסוג כלל שיש לו קבוצת פלט של debug_files, אפשר ליצור את הקבצים האלה על ידי הפעלה של bazel build //pkg:mytarget --output_groups=debug_files. מאחר שלא הוגדרו תוויות לפלטים שלא הוצהרו, ניתן לבקש אותם רק אם הם מופיעים בתוצאות ברירת המחדל או בקבוצת הפלט.

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

def _example_library_impl(ctx):
    ...
    debug_file = ctx.actions.declare_file(name + ".pdb")
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        OutputGroupInfo(
            debug_files = depset([debug_file]),
            all_files = depset([output_file, debug_file]),
        ),
        ...
    ]

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

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

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

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

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

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

בכל מאפיין תלות, אפשר להשתמש ב-cfg כדי לקבוע אם יחסי תלות צריכים להיקבע באותה הגדרה או לעבור לתצורת exe. אם למאפיין תלות יש את הדגל executable=True, חובה להגדיר את cfg באופן מפורש. זאת כדי למנוע התקנה שגויה של כלי עבור הגדרה שגויה. לצפייה בדוגמה

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

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

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

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

ניתן גם להשתמש ב-cfg=my_transition כדי להשתמש במעברים שהוגדרו על ידי המשתמש, שמאפשרים למחברים גמישות רבה בשינוי תצורות, והיתרון של הפיכת תרשים ה-build לגדול ופחות מובן.

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

יש הבדלים רבים בין הגדרות "host" לבין "exec"

  • "host&PLURAL; אפשר להמשיך לבצע העברות נוספות של תצורה אחרי'בתצורה של "exec"
  • "host" הוא מונוליתי, "exec" is't: יש רק &הגדרה אחת של "host" אבל לכל פלטפורמה להפעלה יש תצורה אחרת.
  • "host" ההנחה היא שאתם מפעילים כלים באותו מכונה כמו Bazel, או במכונה בעלת דמיון משמעותי. זה כבר לא נכון: יש לך אפשרות להריץ פעולות build במחשב המקומי שלך, או בהפעלה מקומית, ואין ערובה לכך שהמוציא לפועל הוא אותו מעבד ו-OS כמו במחשב המקומי שלך.

ההגדרות "exec" ו-"host" קובעות את אותן אפשרויות (למשל, מגדירים את --compilation_mode מ---host_compilation_mode, מגדירים את --cpu מ---host_cpu, וכו'). ההבדל הוא שהתצורה "host" מתחילה בערכי ברירת המחדל של כל הדגלים האחרים, ואילו ההגדרה &"exec" ההגדרה מתחילה בערכי הנוכחיים של הסימונים, על סמך הגדרת היעד.

מקטעים של תצורה

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

def _impl(ctx):
    # Using ctx.fragments.cpp leads to an error since it was not declared.
    x = ctx.fragments.java
    ...

my_rule = rule(
    implementation = _impl,
    fragments = ["java"],      # Required fragments of the target configuration
    host_fragments = ["java"], # Required fragments of the host configuration
    ...
)

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

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

    ...
    runfiles = ctx.runfiles(
        root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
        symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
    )
    # Creates something like:
    # sometarget.runfiles/
    #     some/
    #         path/
    #             here.foo -> some_data_file2
    #     <workspace_name>/
    #         some/
    #             path/
    #                 here.bar -> some_data_file3

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

כיסוי קוד

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

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

# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
  # Do something to turn on coverage for this compile action

הלוגיקה שתמיד צריכה להיות פועלת במצב כיסוי (גם אם המקור של יעד מסוים נוסף או לא מוגדר) יכולה להיות מותנית ב-ctx.Configuration.coverage_Enabled.

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

# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
    (ctx.coverage_instrumented() or
     any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
    # Do something to turn on coverage for this compile action

הכללים צריכים גם לספק מידע על המאפיינים הרלוונטיים לכיסוי של ספק InstrumentedFilesInfo, שנבנה באמצעות coverage_common.instrumented_files_info. הפרמטר dependency_attributes של instrumented_files_info צריך לכלול את כל המאפיינים התלויים בזמן ריצה, כולל יחסי תלות של קוד כמו deps ותלויים בנתונים כמו data. הפרמטר source_attributes צריך לכלול את המאפיינים של קובצי המקור של הכלל, אם ניתן להוסיף את אמצעי הכיסוי:

def _example_library_impl(ctx):
    ...
    return [
        ...
        coverage_common.instrumented_files_info(
            ctx,
            dependency_attributes = ["deps", "data"],
            # Omitted if coverage is not supported for this rule:
            source_attributes = ["srcs", "hdrs"],
        )
        ...
    ]

אם הפונקציה InstrumentedFilesInfo לא מוחזרת, המערכת יוצרת מאפיין ברירת מחדל עם כל מאפיין תלות שלא מוגדר בכלי, אשר לא מגדיר את cfg בתור "host" או "exec" בסכימת המאפיינים ב-dependency_attributes. (התנהגות זו אינה אידיאלית, משום שהיא מגדירה מאפיינים כמו srcs ב-dependency_attributes במקום source_attributes, אך מונעת את הצורך להגדיר כיסוי מדויק לכל הכללים בשרשרת התלות).

פעולות אימות

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

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

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

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

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

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

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

הפתרון לבעיות האלה הוא להשתמש בקבוצה 'פלט אימותים'.

קבוצת פלט של אימותים

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

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

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

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

  • כאשר היעד תלוי בכלי
  • כשהיעד תלוי בתור תלות משתמעת (לדוגמה, מאפיין שמתחיל ב-"_")
  • כשהיעד מובנה במארח או בתצורת exe.

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

שימוש בקבוצת פלט של אימותים

קבוצת פלט האימות נמצאת בשם _validation ומשמשת כמו כל קבוצת פלט אחרת:

def _rule_with_validation_impl(ctx):

  ctx.actions.write(ctx.outputs.main, "main output\n")

  ctx.actions.write(ctx.outputs.implicit, "implicit output\n")

  validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
  ctx.actions.run(
      outputs = [validation_output],
      executable = ctx.executable._validation_tool,
      arguments = [validation_output.path])

  return [
    DefaultInfo(files = depset([ctx.outputs.main])),
    OutputGroupInfo(_validation = depset([validation_output])),
  ]


rule_with_validation = rule(
  implementation = _rule_with_validation_impl,
  outputs = {
    "main": "%{name}.main",
    "implicit": "%{name}.implicit",
  },
  attrs = {
    "_validation_tool": attr.label(
        default = Label("//validation_actions:validation_tool"),
        executable = True,
        cfg = "exec"),
  }
)

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

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

load("@bazel_skylib//lib:unittest.bzl", "analysistest")

def _validation_outputs_test_impl(ctx):
  env = analysistest.begin(ctx)

  actions = analysistest.target_actions(env)
  target = analysistest.target_under_test(env)
  validation_outputs = target.output_groups._validation.to_list()
  for action in actions:
    for validation_output in validation_outputs:
      if validation_output in action.inputs.to_list():
        analysistest.fail(env,
            "%s is a validation action output, but is an input to action %s" % (
                validation_output, action))

  return analysistest.end(env)

validation_outputs_test = analysistest.make(_validation_outputs_test_impl)

סימון פעולות אימות

הפעלת פעולות האימות נשלטת על ידי סימון שורת הפקודה --run_validations, שמוגדר כברירת מחדל כ-true.

תכונות שהוצאו משימוש

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

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

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

  • עבור כללי הפעלה, ctx.outputs.executable מתייחס לפלט הפעלה צפוי שהוצהר עם אותו שם כמו יעד הכלל. יש להצהיר על הפלט באופן מפורש, למשל באמצעות ctx.actions.declare_file(ctx.label.name), ולוודא שהפקודה שיוצרת את קובץ ההפעלה מגדירה את ההרשאות שלה כדי לאפשר הפעלה. יש להעביר באופן מפורש את פלט ההפעלה לפרמטר executable של DefaultInfo.

אילו תכונות כדאי להריץ כדי להימנע?

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

  • יש להימנע משימוש במצבים collect_data ו-collect_default של ctx.runfiles. המצבים האלה אוספים באופן לא מפורש קבצים בקצהי תלות מסוימים בתוך הקוד באופן מבלבל. במקום זאת, אפשר להוסיף קבצים באמצעות הפרמטרים files או transitive_files של ctx.runfiles, או על ידי מיזוג בקובצי ריצה מקשרי תלות עם runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles).

  • הימנעו משימוש ב-data_runfiles וב-default_runfiles של ה-constructorDefaultInfo. יש לציין את DefaultInfo(runfiles = ...) במקום זאת. ההבחנה בין "default" &"data" Runfiles מנוהלת מסיבות מדור קודם. לדוגמה, חלק מהכללים מגדירים את פלט ברירת המחדל בשדה data_runfiles, אבל לא את default_runfiles. במקום להשתמש ב-data_runfiles, כללים צריכים גם לכלול פלט ברירת מחדל ולמזג אותו ב-default_runfiles ממאפיינים שמספקים קובצי runfile (בדרך כלל data).

  • בעת אחזור של runfiles מ-DefaultInfo (בדרך כלל רק למיזוג קבצים בין הכלל הנוכחי לתלויים שלו), צריך להשתמש ב-DefaultInfo.default_runfiles, ולא ב-DefaultInfo.data_runfiles.

מעבר מספקים מדור קודם

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

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

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

def _old_rule_impl(ctx):
  ...
  legacy_data = struct(x="foo", ...)
  modern_data = MyInfo(y="bar", ...)
  # When any legacy providers are returned, the top-level returned value is a
  # struct.
  return struct(
      # One key = value entry for each legacy provider.
      legacy_info = legacy_data,
      ...
      # Additional modern providers:
      providers = [modern_data, ...])

אם dep הוא אובייקט Target שמתקבל עבור מופע של הכלל הזה, ניתן לאחזר את הספקים והתוכן שלהם כ-dep.legacy_info.x ו-dep[MyInfo].y.

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

  • השדות files, runfiles, data_runfiles, default_runfiles וגם executable תואמים לשדות בעלי השם זהה ב-DefaultInfo. לא ניתן לציין אף אחד מהשדות האלה ולהחזיר ספק של DefaultInfo.

  • השדה output_groups מקבל ערך מבנה והוא תואם לערך OutputGroupInfo.

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

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

  2. שנו את הכללים שצורכים את הספק הקודם כדי לצרוך את הספק המודרני. אם בהצהרות על מאפיינים קיימת דרישה לספק מדור קודם, תצטרכו לעדכן גם את הספק החדש. לחלופין, אפשר לשלב את העבודה הזו עם שלב 1. כדי לעשות זאת, יש לבקש מהצרכנים לאשר או לדרוש את הספק: לבדוק את נוכחות הספק הקודם באמצעות hasattr(target, 'foo'), או להשתמש בספק החדש באמצעות FooInfo in target.

  3. יש להסיר באופן מלא את הספק הקודם מכל הכללים.