מדריך לכללים

Starlark היא שפה דמוית Python שפותחה במקור לשימוש ב-Bazel ומאז היא אומצה על ידי כלים אחרים. קובצי BUILD ו-.bzl של Bazel כתובים באופן דינמי בניב של Starstark בתור "שפת הבנייה", אך לעתים קרובות היא נקראת "Starlark", במיוחד בעת הדגשה של התכונה מתבטאת בשפת Bazel מרחיב את שפת הליבה עם פונקציות רבות הקשורות ל-build כמו glob, genrule, java_binary וכן הלאה.

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

הכלל הריק

כדי ליצור את הכלל הראשון, צריך ליצור את הקובץ foo.bzl:

def _foo_binary_impl(ctx):
    pass

foo_binary = rule(
    implementation = _foo_binary_impl,
)

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

ניתן לטעון את הכלל ולהשתמש בו בקובץ BUILD.

יש ליצור קובץ BUILD באותה ספרייה:

load(":foo.bzl", "foo_binary")

foo_binary(name = "bin")

עכשיו אפשר לבנות את היעד:

$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)

למרות שהכלל לא עושה דבר, הוא כבר פועל כמו כללים אחרים: יש לו שם, אבל הוא תומך במאפיינים נפוצים כמו visibility, testonly וtags.

הערכת מודל

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

צריך לעדכן את foo.bzl עם כמה הצהרות מודפסות:

def _foo_binary_impl(ctx):
    print("analyzing", ctx.label)

foo_binary = rule(
    implementation = _foo_binary_impl,
)

print("bzl file evaluation")

וגם

load(":foo.bzl", "foo_binary")

print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")

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

שליחת שאילתה לקוד:

$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1

מבצעים כמה תצפיות:

  • "הערכת קובץ bzl" מודפסת תחילה. לפני הערכת הקובץ BUILD, Bazel מבצעת הערכה של כל הקבצים שהם טוענים. אם כמה קובצי BUILD נטענים foo.bzl, יוצג רק אירוע אחד של "bzl file review" מפני ש-Bazel שומרת את תוצאת ההערכה במטמון.
  • לא פונקציית הקריאה החוזרת _foo_binary_impl. השאילתה המקורית נטענת BUILD קבצים, אבל היא לא מנתחת את היעדים.

כדי לנתח את היעדים, משתמשים בפקודה cquery ("שאילתה מוגדרת") או בפקודה build:

$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...

כפי שאפשר לראות, _foo_binary_impl נקרא עכשיו פעמיים – פעם אחת לכל יעד.

חלק מהקוראים יבחינו שוב ב "הערכת קבצים של bzl", למרות שההערכה של foo.bzl נשמרה במטמון אחרי הקריאה ל-bazel query. Bazel לא מעריך מחדש את הקוד, אלא רק מפעיל מחדש את אירועי ההדפסה. אתם מקבלים את אותה פלט בלי קשר למצב המטמון.

יצירת קובץ

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

ctx.actions.declare_file(ctx.label.name)

אם האפליקציה bazel build :all פועלת עכשיו, תופיע הודעת שגיאה:

The following files have no generating action:
bin2

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

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello\n",
    )

הקוד תקף, אבל הוא לא אמור לעשות דבר:

$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)

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

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello!\n",
    )
    return [DefaultInfo(files = depset([out]))]

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

עכשיו, מריצים את Bazel:

$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
  bazel-bin/bin1

$ cat bazel-bin/bin1
Hello!

יצרת בהצלחה קובץ!

מאפיינים

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

יש להוסיף מאפיין מחרוזת בשם username:

foo_binary = rule(
    implementation = _foo_binary_impl,
    attrs = {
        "username": attr.string(),
    },
)

אחר כך מגדירים אותו בקובץ BUILD:

foo_binary(
    name = "bin",
    username = "Alice",
)

כדי לגשת לערך שבפונקציית הקריאה החוזרת, יש להשתמש בפונקציה ctx.attr.username. לדוגמה:

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello {}!\n".format(ctx.attr.username),
    )
    return [DefaultInfo(files = depset([out]))]

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

תלות

מאפייני תלות, כמו attr.label ו-attr.label_list, מצהירים על תלות ביעד שבבעלותו של המאפיין ליעד ש{ תווית 101} מופיעה בערך המאפיין. מאפיין זה יוצר את הבסיס של תרשים היעד.

בקובץ BUILD, תווית היעד תופיע כאובייקט מחרוזת, למשל //pkg:name. בפונקציית ההטמעה, היעד יהיה נגיש כאובייקט Target. לדוגמה, אפשר להציג את הקבצים שהוחזרו באמצעות היעד באמצעות Target.files.

קבצים מרובים

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

"srcs": attr.label_list(allow_files = [".java"]),

ניתן לגשת לרשימת הקבצים באמצעות ctx.files.<attribute name>. לדוגמה, ניתן לגשת לרשימת הקבצים במאפיין srcs

ctx.files.srcs

קובץ יחיד

אם צריך רק קובץ אחד, אפשר להשתמש ב-allow_single_file:

"src": attr.label(allow_single_file = [".java"])

לאחר מכן הקובץ הזה נגיש דרך ctx.file.<attribute name>:

ctx.file.src

יצירת קובץ באמצעות תבנית

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

יש ליצור מאפיין template כדי להצהיר על תלות בקובץ התבנית:

def _hello_world_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name + ".cc")
    ctx.actions.expand_template(
        output = out,
        template = ctx.file.template,
        substitutions = {"{NAME}": ctx.attr.username},
    )
    return [DefaultInfo(files = depset([out]))]

hello_world = rule(
    implementation = _hello_world_impl,
    attrs = {
        "username": attr.string(default = "unknown person"),
        "template": attr.label(
            allow_single_file = [".cc.tpl"],
            mandatory = True,
        ),
    },
)

משתמשים יכולים להשתמש בכלל הבא:

hello_world(
    name = "hello",
    username = "Alice",
    template = "file.cc.tpl",
)

cc_binary(
    name = "hello_bin",
    srcs = [":hello"],
)

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

    "_template": attr.label(
        allow_single_file = True,
        default = "file.cc.tpl",
    ),

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

exports_files(["file.cc.tpl"])

מתקדמים יותר