工具鏈

回報問題 查看來源

本頁將說明工具鍊架構,這項工具可讓規則作者將規則邏輯與平台式工具的所選工具區隔開來。建議您先詳閱規則平台頁面,再繼續操作。本頁說明需要工具鍊的原因、如何定義和使用工具鍊,以及 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.bazelfiles. Additional execution platforms and toolchains may also be specified on the command line via [--extra_execution_platforms](/reference/command-line-reference#flag--extra_execution_platforms) and [--extra_toolchains`](/reference/command-line-reference#flag--extra_toolchains 的 register_execution_platformsregister_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