工具鏈

回報問題 查看原始碼 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

本頁面說明工具鍊架構,這是規則作者將規則邏輯與以平台為基礎的工具選取方式分離的方法。建議您先閱讀「規則」和「平台」頁面,再繼續操作。本頁說明為何需要工具鍊、如何定義及使用工具鍊,以及 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 無法為必要工具鍊類型找到相符的工具鍊 (請參閱下方的「Toolchain resolution」),則會發生錯誤,且分析作業會停止。

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

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 和工具鍊類型標籤擷取使用規則的物件。ToolchainInfostruct 一樣,可以容納任意欄位值組合。工具鍊類型應明確記錄 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 的解析程序即可。方法是註冊工具鍊,方法是在 MODULE.bazel 檔案中使用 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",
    # 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. 如果清單中的每個 constraint_value 都與平台的 constraint_value 相符 (明確或預設),target_compatible_withexec_compatible_with 子句就會相符該平台。

    如果平台有 constraint_value 來自 constraint_setting,但未由子句參照,則不會影響比對結果。

  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