本頁面說明工具鍊架構,規則作者可藉此將規則邏輯與平台工具選取程序分離。建議您先閱讀規則和平台頁面,再繼續操作。本頁面說明為何需要工具鍊、如何定義及使用工具鍊,以及 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 []
定義工具鍊
如要為特定工具鍊類型定義一些工具鍊,您需要下列三項條件:
代表工具或工具套件類型的語言專屬規則。按照慣例,這項規則的名稱會加上「_toolchain」後置字元。
- 注意:
\_toolchain
規則無法建立任何建構動作。 而是從其他規則收集構件,並轉送至使用工具鍊的規則。這項規則負責建立所有建構動作。
- 注意:
這類規則有多個目標,代表不同平台的工具或工具套件版本。
針對每個這類目標,一般
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 的解析程序使用工具鍊即可。方法是在 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_platforms
和 register_toolchains
呼叫,從外部依附元件圖表收集而來。您也可以透過指令列,使用 --extra_execution_platforms
和 --extra_toolchains
指定其他執行平台和工具鍊。主機平台會自動列為可用的執行平台。
系統會將可用的平台和工具鍊追蹤為排序清單,以確保確定性,並優先處理清單中較早的項目。
可用的工具鍊組合會依優先順序,從 --extra_toolchains
和 register_toolchains
建立:
- 使用
--extra_toolchains
註冊的工具鍊會優先新增。(在這些工具鍊中,最後一個工具鍊的優先順序最高)。 - 使用遞移外部依附元件圖表中的
register_toolchains
登錄的工具鍊,順序如下:(在這些工具鍊中,第一個提及的工具鍊優先順序最高)。- 根模組 (即工作區根層級的
MODULE.bazel
) 註冊的工具鍊。 - 在使用者
WORKSPACE
檔案中註冊的工具鍊,包括從該處叫用的任何巨集; - 非根模組註冊的工具鍊 (也就是根模組指定的依附元件,以及這些依附元件的依附元件等);
- 在「WORKSPACE 後置字串」中註冊的工具鍊;這只會由與 Bazel 安裝作業一併封裝的特定原生規則使用。
- 根模組 (即工作區根層級的
注意::all
、:*
和 /...
等虛擬目標會依據 Bazel 的套件載入機制排序,該機制採用字典順序。
解決步驟如下。
如果平台也具有清單中的每個
constraint_value
(明確或預設),則target_compatible_with
或exec_compatible_with
子句會比對平台。constraint_value
如果平台有子句未參照的
constraint_value
,這些不會影響比對。constraint_setting
如果建構的目標指定
exec_compatible_with
屬性 (或其規則定義指定exec_compatible_with
引數),系統會篩選可用執行平台清單,移除不符合執行限制的平台。系統會篩選可用的工具鍊清單,移除指定
target_settings
但與目前設定不符的工具鍊。針對每個可用的執行平台,將每個工具鍊類型與第一個可用的工具鍊 (如有) 建立關聯,該工具鍊必須與這個執行平台和目標平台相容。
如果執行平台無法為其中一個工具鍊類型找到相容的必要工具鍊,就會遭到排除。在剩餘的平台中,第一個平台會成為目前目標的執行平台,而其相關聯的工具鍊 (如有) 會成為目標的依附元件。
系統會使用所選執行平台,執行目標產生的所有動作。
如果可在同一個建構作業中,以多種設定建構相同目標 (例如適用於不同 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