工具鏈

回報問題 查看來源

本頁說明工具鍊架構,這是讓規則作者將規則邏輯與平台式工具選項分離的方式。建議您先詳閱規則平台頁面,再繼續操作。本頁面說明需要使用工具鍊的原因、如何定義及使用工具鍊,以及 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. 代表特定工具或工具套件種類的語言專屬規則。按照慣例,這項規則的名稱開頭會加上「_工具鍊」。

    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()WORKSPACE 檔案中註冊工具鍊,或是使用 --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_toolchains,從 WORKSPACE 檔案收集。指令列中也可透過 --extra_execution_platforms--extra_toolchains 指定其他執行平台和工具鍊。系統會自動將主機平台納入為可用的執行平台。可用平台和工具鍊追蹤為確定性的排序清單,並優先考量清單中較早的項目。

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

  1. 系統會先新增使用 --extra_toolchains 註冊的工具鍊。
    1. 在這些中,「最後一個」工具鍊的優先順序最高。
  2. 使用 register_toolchains 註冊的工具鍊
    1. 在這些中,第一個提到的工具鍊是最高的優先順序。

附註::all:*/... 等虛擬目標是由 Bazel 的套件載入機制 (採用字典編列順序排列) 所排序。

解決步驟如下。

  1. 如果 target_compatible_withexec_compatible_with 子句對於清單中的每個 constraint_value,平台也具有該 constraint_value (明確或為預設值),就會與平台「相符」

    如果平台有來自 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