巨集

回報問題 查看來源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本頁面說明巨集的使用基礎,包括常見用途、偵錯和慣例。

巨集是從 BUILD 檔案呼叫的函式,可例項化規則。 巨集主要用於封裝現有規則和其他巨集,並重複使用這些規則和巨集。

巨集可分成兩種類型:符號巨集 (本頁說明) 和舊版巨集。建議盡可能使用符號巨集,讓程式碼更清楚明瞭。

符號巨集提供型別引數 (字串到標籤的轉換,相對於巨集呼叫的位置),並可限制及指定所建立目標的顯示狀態。這些函式旨在適用於延遲評估 (將在日後的 Bazel 版本中新增)。Bazel 8 預設提供符號巨集。本文件提及 macros 時,指的是符號巨集

如需符號巨集的執行範例,請參閱範例存放區

用量

巨集是在 .bzl 檔案中定義,方法是呼叫 macro() 函式,並提供兩個必要參數:attrsimplementation

屬性

attrs 接受屬性名稱至屬性型別的字典,代表巨集的引數。所有巨集都會隱含新增 namevisibility 這兩個常見屬性,且不會納入傳遞至 attrs 的字典。

# macro/macro.bzl
my_macro = macro(
    attrs = {
        "deps": attr.label_list(mandatory = True, doc = "The dependencies passed to the inner cc_binary and cc_test targets"),
        "create_test": attr.bool(default = False, configurable = False, doc = "If true, creates a test target"),
    },
    implementation = _my_macro_impl,
)

屬性型別宣告接受 參數mandatorydefaultdoc。大多數屬性類型也會接受 configurable 參數,這個參數會決定屬性是否接受 select。如果屬性為 configurable,系統會將非 select 值剖析為無法設定的 select"foo" 會變成 select({"//conditions:default": "foo"})。詳情請參閱選取器

屬性繼承

巨集通常會包裝規則 (或其他巨集),而巨集的作者通常會使用 **kwargs,將包裝符號的大部分屬性轉送至巨集的主要目標 (或主要內部巨集),且不進行任何變更。

如要支援這個模式,巨集可以透過將 規則巨集符號傳遞至 macro()inherit_attrs 引數,從規則或其他巨集繼承屬性。(您也可以使用特殊字串 "common",而非規則或巨集符號,藉此沿用為所有 Starlark 建構規則定義的通用屬性)。只有公開屬性會遭到覆寫,且巨集本身 attrs 字典中的屬性會覆寫同名的繼承屬性。您也可以在 attrs 字典中使用 None 做為值,移除繼承的屬性:

# macro/macro.bzl
my_macro = macro(
    inherit_attrs = native.cc_library,
    attrs = {
        # override native.cc_library's `local_defines` attribute
        "local_defines": attr.string_list(default = ["FOO"]),
        # do not inherit native.cc_library's `defines` attribute
        "defines": None,
    },
    ...
)

無論原始屬性定義的預設值為何,非必要繼承屬性的預設值一律會覆寫為 None。如要檢查或修改繼承的非必要屬性 (例如想在繼承的 tags 屬性中加入標記),請務必在巨集的實作函式中處理 None 案例:

# macro/macro.bzl
def _my_macro_impl(name, visibility, tags, **kwargs):
    # Append a tag; tags attr is an inherited non-mandatory attribute, and
    # therefore is None unless explicitly set by the caller of our macro.
    my_tags = (tags or []) + ["another_tag"]
    native.cc_library(
        ...
        tags = my_tags,
        **kwargs,
    )
    ...

導入作業

implementation 接受包含巨集邏輯的函式。實作函式通常會呼叫一或多項規則來建立目標,而且通常是不公開的 (名稱開頭有底線)。按照慣例,這些函式的名稱與巨集相同,但會加上 _ 前置字串和 _impl 後置字串。

與規則實作函式不同,巨集實作函式會接受每個引數的參數,而規則實作函式只會接受包含屬性參照的單一引數 (ctx)。

# macro/macro.bzl
def _my_macro_impl(name, visibility, deps, create_test):
    cc_library(
        name = name + "_cc_lib",
        deps = deps,
    )

    if create_test:
        cc_test(
            name = name + "_test",
            srcs = ["my_test.cc"],
            deps = deps,
        )

如果巨集會繼承屬性,其實作函式必須含有 **kwargs 剩餘關鍵字參數,該參數可轉送至呼叫,以叫用繼承的規則或子巨集。(這樣一來,如果繼承的規則或巨集新增屬性,巨集就不會中斷)。

宣告

巨集是透過載入並呼叫 BUILD 檔案中的定義來宣告。


# pkg/BUILD

my_macro(
    name = "macro_instance",
    deps = ["src.cc"] + select(
        {
            "//config_setting:special": ["special_source.cc"],
            "//conditions:default": [],
        },
    ),
    create_tests = True,
)

這會建立目標 //pkg:macro_instance_cc_lib//pkg:macro_instance_test

如同規則呼叫,如果巨集呼叫中的屬性值設為 None,系統會將該屬性視為巨集呼叫端省略的屬性。舉例來說,以下兩個巨集呼叫的作用相同:

# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])

這通常在 BUILD 檔案中沒有用處,但如果以程式輔助方式將巨集包裝在另一個巨集中,這就很有幫助。

詳細資料

建立指定目標的命名慣例

符號巨集建立的任何目標或子巨集名稱,都必須符合巨集的 name 參數,或以 name 為前置字串,後面接 _ (建議)、.-。舉例來說,my_macro(name = "foo") 可能只會建立名為 foo 的檔案或目標,或是以 foo_foo-foo. 為前置字串的檔案或目標,例如 foo_bar

違反巨集命名慣例的目標或檔案可以宣告,但無法建構,也無法做為依附元件使用。

與巨集例項位於同一套件中的非巨集檔案和目標,不得與潛在的巨集目標名稱衝突,但系統不會強制執行這項專屬性。我們正在實作延遲評估,以提升符號巨集的效能,但如果套件違反命名結構,就會受到影響。

限制

與舊版巨集相比,符號巨集有一些額外限制。

符號巨集

  • 必須採用 name 引數和 visibility 引數
  • 必須有 implementation 函式
  • 可能不會傳回值
  • 可能不會改變引數
  • 除非是特殊 finalizer 巨集,否則不得呼叫 native.existing_rules()
  • 可能無法撥打 native.package()
  • 可能無法撥打 glob()
  • 可能無法撥打 native.environment_group()
  • 必須建立名稱符合命名架構的目標
  • 無法參照未宣告或以引數形式傳遞的輸入檔案
  • 無法參照來電者私人目標 (詳情請參閱可見度和巨集)。

可見度和巨集

可見度系統有助於保護 (符號) 巨集及其呼叫端的實作詳細資料。

根據預設,在符號巨集中建立的目標會顯示在巨集本身中,但不一定會顯示在巨集的呼叫端。巨集可以轉送自身 visibility 屬性的值,藉此「匯出」目標做為公開 API,如 some_rule(..., visibility = visibility) 所示。

巨集可見度的主要概念如下:

  1. 系統會根據宣告目標的巨集檢查可視度,而非呼叫巨集的套件。

    • 換句話說,即使目標位於同一個套件中,也不會自動對其他目標顯示。這可避免巨集的內部目標成為套件中其他巨集或頂層目標的依附元件。
  2. 規則和巨集的所有 visibility 屬性,都會自動納入規則或巨集的呼叫位置。

    • 因此,目標會無條件顯示在同一巨集中宣告的其他目標 (或 BUILD 檔案,如果不在巨集中)。

實際上,這表示巨集宣告目標時,如果沒有設定目標的 visibility,目標預設會是巨集的內部目標。(套件的預設瀏覽權限不適用於巨集內)。匯出目標表示目標會顯示給巨集呼叫端在巨集的 visibility 屬性中指定的任何項目,以及巨集呼叫端本身的套件和巨集本身的程式碼。換句話說,巨集的顯示設定決定了誰 (巨集本身除外) 可以查看巨集匯出的目標。

# tool/BUILD
...
some_rule(
    name = "some_tool",
    visibility = ["//macro:__pkg__"],
)
# macro/macro.bzl

def _impl(name, visibility):
    cc_library(
        name = name + "_helper",
        ...
        # No visibility passed in. Same as passing `visibility = None` or
        # `visibility = ["//visibility:private"]`. Visible to the //macro
        # package only.
    )
    cc_binary(
        name = name + "_exported",
        deps = [
            # Allowed because we're also in //macro. (Targets in any other
            # instance of this macro, or any other macro in //macro, can see it
            # too.)
            name + "_helper",
            # Allowed by some_tool's visibility, regardless of what BUILD file
            # we're called from.
            "//tool:some_tool",
        ],
        ...
        visibility = visibility,
    )

my_macro = macro(implementation = _impl, ...)
# pkg/BUILD
load("//macro:macro.bzl", "my_macro")
...

my_macro(
    name = "foo",
    ...
)

some_rule(
    ...
    deps = [
        # Allowed, its visibility is ["//pkg:__pkg__", "//macro:__pkg__"].
        ":foo_exported",
        # Disallowed, its visibility is ["//macro:__pkg__"] and
        # we are not in //macro.
        ":foo_helper",
    ]
)

如果使用 visibility = ["//other_pkg:__pkg__"] 呼叫 my_macro,或 //pkg 套件已將 default_visibility 設為該值,則 //pkg:foo_exported 也可在 //other_pkg/BUILD//other_pkg:defs.bzl 中定義的巨集內使用,但 //pkg:foo_helper 仍會受到保護。

巨集可以傳遞 visibility = ["//some_friend:__pkg__"] (適用於內部目標) 或 visibility = visibility + ["//some_friend:__pkg__"] (適用於匯出的目標),宣告目標對友善套件可見。請注意,巨集宣告具有公開瀏覽權限 (visibility = ["//visibility:public"]) 的目標是反模式,因為這會讓目標無條件對每個套件顯示,即使呼叫端指定了更受限的瀏覽權限也是如此。

所有瀏覽權限檢查都是針對目前執行的最內層符號巨集完成。不過,有可見度委派機制:如果巨集將標籤做為屬性值傳遞至內部巨集,系統會根據外部巨集檢查內部巨集中的任何標籤用法。詳情請參閱曝光度頁面

請注意,舊版巨集對可見度系統完全透明,且行為與呼叫巨集的 BUILD 檔案或符號巨集的位置相同。

終結器和可見性

除了遵循一般符號巨集顯示規則的目標,規則終止程式中宣告的目標可查看終止程式目標套件可見的所有目標。

也就是說,如果您將以 native.existing_rules() 為基礎的舊版巨集遷移至終結器,終結器宣告的目標仍可查看舊的依附元件。

不過請注意,您可以在符號巨集中宣告目標,這樣一來,即使終結器可以使用 native.existing_rules() 內省其屬性,終結器的目標也無法在可見度系統下看到該目標。

選取

如果屬性為 configurable (預設值),且值不是 None,則巨集實作函式會將屬性值視為包裝在微不足道的 select 中。這樣巨集作者就能更輕鬆地找出錯誤,因為他們未預期屬性值可能是 select

舉例來說,請參考下列巨集:

my_macro = macro(
    attrs = {"deps": attr.label_list()},  # configurable unless specified otherwise
    implementation = _my_macro_impl,
)

如果使用 deps = ["//a"] 叫用 my_macro,系統會叫用 _my_macro_impl,並將其 deps 參數設為 select({"//conditions:default": ["//a"]})。如果這導致實作函式失敗 (例如,程式碼嘗試將值編入索引,如 deps[0],但 select 不允許這麼做),巨集作者可以選擇重新編寫巨集,只使用與 select 相容的作業,也可以將屬性標示為不可設定 (attr.label_list(configurable = False))。後者可確保使用者無法傳遞 select 值。

規則目標會反向執行這項轉換,並將微不足道的 select 儲存為無條件值;在上述範例中,如果 _my_macro_impl 宣告規則目標 my_rule(..., deps = deps),該規則目標的 deps 會儲存為 ["//a"]。這可確保 select 包裝不會導致巨集例項化的所有目標中,都儲存微不足道的 select 值。

如果可設定屬性的值為 None,則不會包裝在 select 中。這可確保 my_attr == None 等測試仍可運作,且當屬性轉送至具有計算預設值的規則時,規則會正常運作 (也就是說,就像屬性完全未傳入一樣)。屬性不一定能採用 None 值,但 attr.label() 類型和任何繼承的非必要屬性可以。

Finalizers

規則終結器是一種特殊的符號巨集,無論在 BUILD 檔案中的詞彙位置為何,都會在載入套件的最後階段進行評估,也就是在定義所有非終結器目標之後。與一般符號巨集不同,終結器可以呼叫 native.existing_rules(),但行為與舊版巨集略有不同:只會傳回非終結器規則目標的集合。最終處理程序可能會對該集合的狀態進行判斷,或定義新目標。

如要宣告終結器,請使用 finalizer = True 呼叫 macro()

def _my_finalizer_impl(name, visibility, tags_filter):
    for r in native.existing_rules().values():
        for tag in r.get("tags", []):
            if tag in tags_filter:
                my_test(
                    name = name + "_" + r["name"] + "_finalizer_test",
                    deps = [r["name"]],
                    data = r["srcs"],
                    ...
                )
                continue

my_finalizer = macro(
    attrs = {"tags_filter": attr.string_list(configurable = False)},
    implementation = _impl,
    finalizer = True,
)

懶散

重要事項:我們正在實作巨集延遲擴展和評估功能。這項功能尚未推出。

目前,系統會在載入 BUILD 檔案後立即評估所有巨集,這可能會對套件中的目標造成負面影響,因為這些目標也含有成本高昂的不相關巨集。日後,只有在建構作業需要非終結符號巨集時,系統才會評估這類巨集。前置字串命名結構定義可協助 Bazel 判斷要展開哪個巨集 (如果要求目標)。

排解遷移問題

以下列出一些常見的遷移問題及解決方法。

  • 舊版巨集呼叫 glob()

glob() 呼叫移至 BUILD 檔案 (或從 BUILD 檔案呼叫的舊版巨集),然後使用標籤清單屬性,將 glob() 值傳遞至符號巨集:

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • 舊版巨集含有無效的 Starlark attr 類型參數。

盡可能將邏輯拉入巢狀符號巨集,但請將頂層巨集保留為舊版巨集。

  • 舊版巨集呼叫的規則會建立目標,但目標違反命名結構定義

沒關係,只要不要依附「違規」目標即可。系統會忽略命名檢查。