工具鏈

回報問題 查看原始碼

本頁將說明工具鍊架構,這項工具可讓規則作者將其規則邏輯與平台式工具選項中分離。建議您先詳閱規則平台頁面,再繼續操作。本頁面將說明需要工具鍊的原因、定義及使用工具的方式,以及 Bazel 如何依據平台限制選取適當的工具鍊。

動機

我們先來看看問題工具鍊有哪些經過設計,可以解決。假設您正在撰寫規則來支援「bar」程式設計語言,您的 bar_binary 規則會使用 barc 編譯器編譯 *.bar 檔案,這項工具本身是以工作區中的另一個目標的形式建構而成。由於寫入 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 會根據適用的平台限制,自動將這個依附元件解析為特定目標 (工具鍊)。規則作者和目標作者都不需要瞭解完整的可用平台與工具鍊集。

編寫使用工具鍊的規則

在工具鍊架構下,規則並非直接依附於工具,而是依附於工具鍊類型。工具鍊類型是一個簡單的目標,代表某一種工具類別,針對不同平台扮演相同角色。舉例來說,您可以宣告代表長條編譯器的類型:

# 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"] 會針對 Bazel 解析工具鍊依附元件的任何目標,傳回 ToolchainInfo 提供者ToolchainInfo 物件的欄位是由基礎工具的規則設定;在下一節中,我們將定義這項規則,使 barcinfo 欄位納入 BarcInfo 物件。

下方說明 Bazel 將工具鍊解析成目標的程序。實際上只有已解析的工具鍊目標實際上是 bar_binary 目標的依附元件,而非候選工具鍊的整個空間。

必要和選用工具鍊

根據預設,當規則使用裸標籤表示工具鍊類型依附元件 (如上所示) 時,系統會將工具鍊類型視為「必要」。如果 Bazel 找不到必要工具鍊類型的比對工具鍊 (請參閱下方的工具鍊解析),那麼就會發生錯誤並停止分析。

您可以改為宣告「選用」工具鍊類型依附元件,如下所示:

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:您可以定義所需的工具鍊類型、透過結構定義存取工具鍊,並使用工具鍊來產生新動作。

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. 代表工具或工具套件種類的語言專屬規則。按照慣例,這項規則的名稱後面會加上「_toolchain」。

    1. 注意:\_toolchain 規則無法建立任何建構動作。而是會收集其他規則的構件,並轉送至使用工具鍊的規則。這個規則負責建立所有建構動作。
  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",
)

最後,為兩個 bar_toolchain 目標建立 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 參數的意義略有不同。

透過工具鍊解析度從目標 (稱為「父項」) 到工具鍊的依附元件,會使用稱為「工具鍊轉換」的特殊設定轉換作業。工具鍊轉換將採用相同的設定,但會強制讓工具鍊的執行平台與父項相同 (否則,工具鍊的工具鍊解決方案可能會選擇任何執行平台,並不一定與父項相同)。如此一來,工具鍊的所有 exec 依附元件也可為父項的建構動作執行。任何使用 cfg = "target" 的工具鍊依附元件 (或並未指定 cfg,因為「target」為預設值) 是針對與父項相同的目標平台建構。這可讓工具鍊規則,將程式庫 (上述 system_lib 屬性) 和工具 (compiler 屬性) 提供給需要這些程式庫的建構規則。系統程式庫會連結至最終成果,因此必須針對相同的平台建構;編譯器則是在建構期間叫用的工具,需要能在執行平台上執行。

使用工具鍊註冊及建構

此時,系統會組合所有建構區塊,您只需要將工具鍊提供給 Bazel 的解析程序即可。方法是使用 register_toolchains()MODULE.bazel 檔案中註冊工具鍊,或是使用 --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",
    # or even
    # "//bar_tools/...",
)

使用目標模式註冊工具鍊時,個別工具鍊的註冊順序取決於下列規則:

  • 在套件子套件中定義的工具鍊,會先註冊在套件本身中定義的工具鍊。
  • 在套件中,工具鍊會按照名稱的字母順序註冊。

現在當您建構依附於工具鍊類型的目標時,系統會根據目標和執行平台選取適當的工具鍊。

# 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 的工具鍊解析程序會決定目標的具體工具鍊依附元件。此程序將輸入一組必要的工具鍊類型、目標平台、可用執行平台清單,以及可用工具鍊清單。其輸出內容是每種工具鍊類型的選定的工具鍊,以及目前目標所選取的執行平台。

可用的執行平台和工具鍊是透過 MODULE.bazel 檔案中的 register_execution_platformsregister_toolchains 呼叫,從外部依附元件圖收集而來。您也可以透過 --extra_execution_platforms--extra_toolchains 在指令列中指定其他執行平台和工具鍊。系統會自動將主機平台納入為可用執行平台。系統會將可用的平台和工具鍊做為判定順序的清單進行追蹤,並優先提供給清單中較早的項目。

可用的工具鍊組合會按照優先順序建立,而且是從 --extra_toolchainsregister_toolchains 建立:

  1. 系統會先新增使用 --extra_toolchains 註冊的工具鍊。(在這些類別中,「最後一個」工具鍊的優先順序最高)。
  2. 在遞移外部依附元件圖表中使用 register_toolchains 註冊的工具鍊,順序如下:(在這些項目中,所提及的「第一個」工具鍊會優先處理)。
    1. 由根模組註冊的工具鍊 (如工作區根目錄中的 MODULE.bazel);
    2. 使用者 WORKSPACE 檔案中註冊的工具鍊,包括從該位置叫用的任何巨集;
    3. 由非根模組註冊的工具鍊 (例如根模組指定的依附元件及其依附元件等);
    4. 以「WORKSPACE 後置字串」註冊的工具鍊;只有與 Bazel 安裝套件隨附的特定原生規則才能使用。

注意::all:*/... 等虛擬目標會依照 Bazel 的套件載入機制排序,該機制採用詞法排序。

解決步驟如下。

  1. 如果 target_compatible_withexec_compatible_with 子句清單中的每個 constraint_value,平台也會有該 constraint_value (明確或預設為預設值),就「符合」平台條件。

    如果平台有來自子句未參照的 constraint_settingconstraint_value,這些動作不會影響比對。

  2. 如果正在建構的目標指定 exec_compatible_with 屬性 (或其規則定義指定 exec_compatible_with 引數),系統會篩選可用執行平台清單,移除不符合執行限制的任何項目。

  3. 系統會篩選可用工具鍊清單,移除指定 target_settings 與目前設定不符的所有工具鍊。

  4. 對於每個可用的執行平台,您會將每個工具鍊類型連結至第一個與此執行平台和目標平台相容的工具鍊 (如果有的話)。

  5. 針對其中一種工具鍊類型,找不到相容必要工具鍊的執行平台都會排除。在其餘平台中,第一個平台會成為目前目標的執行平台,而相關工具鍊 (如有) 則成為目標的依附元件。

選擇的執行平台將用於執行目標產生的所有動作。

如果同一個目標可在同一個建構中的多種設定 (例如不同的 CPU) 中建構,解析度程序會獨立套用至目標的每個版本。

如果規則使用執行群組,每個執行群組都會分別執行工具鍊解析,且每個執行群組都有自己的執行平台和工具鍊。

偵錯工具鍊

如要為現有規則新增工具鍊支援,請使用 --toolchain_resolution_debug=regex 旗標。在工具鍊解析期間,標記會為符合規則運算式變數的工具鍊類型或目標名稱提供詳細輸出內容。您可以使用 .* 輸出所有資訊。Bazel 將輸出要在解決程序中檢查和略過的工具鍊名稱。

如要查看哪些 cquery 依附元件來自工具鍊解析,請使用 cquery--transitions 標記:

# 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