工具鏈

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

本頁面說明工具鍊架構,規則作者可藉此將規則邏輯與平台工具選取程序分離。建議您先閱讀規則平台頁面,再繼續操作。本頁面說明為何需要工具鍊、如何定義及使用工具鍊,以及 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 會根據適用的平台限制,自動將此依附元件解析為特定目標 (工具鍊)。規則作者和目標作者都不需要瞭解可用的平台和工具鍊完整組合。

編寫使用工具鍊的規則

在工具鍊架構下,規則不再直接依附於工具,而是依附於工具鍊類型。工具鍊類型是簡單的目標,代表一類工具,這些工具在不同平台中扮演相同角色。舉例來說,您可以宣告代表 bar 編譯器的型別:

# 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),
    ],
)

您也可以在同一個規則中混用表單。不過,如果同一種工具鍊類型列出多次,系統會採用最嚴格的版本,其中「必要」比「選用」更嚴格。

使用工具鍊的撰寫層面

Aspect 與規則可存取相同的工具鍊 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 (明確或預設),則 target_compatible_withexec_compatible_with 子句會比對平台。constraint_value

    如果平台有子句未參照的 constraint_value,這些不會影響比對。constraint_setting

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

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

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

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

系統會使用所選執行平台,執行目標產生的所有動作。

如果可在同一個建構作業中,以多種設定建構相同目標 (例如適用於不同 CPU),系統會分別對每個目標版本套用解析程序。

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

偵錯工具鍊

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

舉例來說,如要偵錯 //my:target 直接建立的所有動作的工具鍊選取項目:

$ bazel build //my:all --toolchain_resolution_debug=//my:target

如要針對所有建構目標的所有動作,偵錯工具鍊選取項目:

$ bazel build //my:all --toolchain_resolution_debug=.*

如要查看哪些 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