工具鏈

回報問題 查看原始碼 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 找不到必要工具鍊類型的相符工具鍊 (請參閱下文的工具鍊解析),這屬於錯誤且分析作業停止。

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

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 的解析程序即可。方法是註冊工具鏈,方法是在 WORKSPACE 檔案中使用 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 的工具鍊解析程序會決定目標的具體工具鍊依附元件。此程序會將一組必要工具鍊類型、目標平台、可用的執行平台清單和可用的工具鍊清單做為輸入內容。其輸出內容為每種工具鍊類型選定的工具鍊,以及為目前目標所選的執行平台。

可用的執行平台和工具鍊會透過 register_execution_platformsregister_toolchainsWORKSPACE 檔案收集。您也可以透過 --extra_execution_platforms--extra_toolchains 在指令列上指定其他執行平台和工具鏈。主機平台會自動納入可用的執行平台。可用的平台和工具鍊會以排序清單的形式追蹤,以便確定性,並將偏好設定給清單中的前幾項項目。

可用的工具鏈組會依優先順序,從 --extra_toolchainsregister_toolchains 建立:

  1. 使用 --extra_toolchains 註冊的工具鏈會優先新增。
    1. 其中,最後工具鏈的優先順序最高。
  2. 使用 register_toolchains 註冊的工具鍊
    1. 其中,首先提到的工具鏈優先順序最高。

注意::all:*/... 等偽目標的順序是由 Bazel 的套件載入機制決定,該機制會使用字典順序。

解決步驟如下:

  1. 如果清單中的每個 constraint_value 都與平台的 constraint_value 相符 (明確或預設),target_compatible_withexec_compatible_with 子句就會相符該平台。

    如果平台有來自 constraint_settingconstraint_value,而且這些 constraint_value 未被子句參照,則不會影響比對作業。

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

  3. 針對每個可用的執行平台,您可以將每個工具鍊類型與第一個可用的工具鍊 (如有) 建立關聯,該工具鍊必須與此執行平台和目標平台相容。

  4. 任何執行平台若無法為其中一種工具鍊類型找到相容的必要工具鍊,就會遭到排除。在其餘平台上,第一個平台會成為目前目標的執行平台,相關聯的工具鍊 (如有) 會成為目標的依附元件。

所選的執行平台可用來執行目標產生的所有動作。

如果同一個目標可以在同一建構中以多種設定 (例如針對不同 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