規則可定義 Bazel 對輸入執行的一系列動作,藉此產生一組輸出內容。在規則的實作函式傳回的 providers 中會參照這些輸出內容。舉例來說,C++ 二進位規則可能會:
- 取得一組
.cpp
來源檔案 (輸入內容)。 - 在來源檔案 (動作) 上執行
g++
。 - 傳回含有可執行輸出內容和其他檔案的
DefaultInfo
提供者,以便在執行階段提供。 - 傳回
CcInfo
供應器,其中包含從目標和其依附元件收集到的 C++ 專屬資訊。
從 Bazel 的角度來看,g++
和標準 C++ 程式庫也是此規則的輸入內容。規則編寫者除了必須考量使用者提供的規則輸入內容,還必須考量執行動作所需的所有工具和程式庫。
在建立或修改任何規則之前,請確認您已熟悉 Bazel 的建構階段。請務必瞭解建構的三個階段 (載入、分析和執行)。瞭解巨集也有助於瞭解規則和巨集之間的差異。首先,請先詳閱規則教學課程。接著,請將這個頁面當做參考。
Bazel 本身內建了幾項規則。這些原生規則 (例如 cc_library
和 java_binary
) 可以針對特定語言提供部分核心支援。定義自己的規則後,您就能為 Bazel 不支援的語言和工具新增類似的支援功能。
Bazel 提供可擴充性模型,可使用 Starlark 語言編寫規則。這些規則會以 .bzl
檔案編寫,可直接從 BUILD
檔案載入。
定義規則時,您可以決定規則支援哪些屬性,以及產生輸出內容的方式。
規則的 implementation
函式會定義在分析階段期間的確切行為。這個函式不會執行任何外部指令。而是註冊動作,這些動作會在稍後的執行階段用於建立規則的輸出內容 (如果需要的話)。
建立規則
在 .bzl
檔案中,使用 rule 函式定義新的規則,並將結果儲存在全域變數中。呼叫 rule
會指定屬性和實作函式:
example_library = rule(
implementation = _example_library_impl,
attrs = {
"deps": attr.label_list(),
...
},
)
這會定義名為 example_library
的規則類型。
對 rule
的呼叫也必須指定規則是建立「可執行」輸出內容 (帶有 executable=True
),或具體來說是測試執行檔 (包含 test=True
)。如果後者是「測試規則」,而規則名稱的結尾必須是 _test
。
目標例項化
您可在 BUILD
檔案中載入並呼叫規則:
load('//some/pkg:rules.bzl', 'example_library')
example_library(
name = "example_target",
deps = [":another_target"],
...
)
每次對建構規則的呼叫都不會傳回任何值,但具有定義目標的副作用。這稱為「執行個體化」規則。這會指定新目標的名稱和目標屬性的值。
您也可以從 Starlark 函式呼叫規則,並在 .bzl
檔案中載入。呼叫規則的 Starlark 函式稱為 Starlark 巨集。Starlark 巨集最終必須從 BUILD
檔案呼叫,且只能在載入階段期間呼叫,因為系統會在該階段評估 BUILD
檔案,以便將目標例項化。
屬性
「屬性」是規則引數。屬性可以為目標的「實作」提供特定值,也可以參照其他目標來建立依附元件圖表。
srcs
或 deps
等規則專屬屬性的定義方式,是將屬性名稱的對應傳送至結構定義 (使用 attr
模組建立) 至 rule
的 attrs
參數。系統會間接將 name
和 visibility
等常見屬性新增至所有規則。額外屬性會明確新增至執行檔和測試規則。隱含新增至規則的屬性,無法納入傳遞至 attrs
的字典中。
依附元件屬性
處理原始碼的規則通常會定義下列屬性,以處理各種依附元件類型:
srcs
會指定目標動作處理的來源檔案。通常,屬性結構定義會指定規則處理的來源檔案排序時,預期的副檔名為何。針對具有標頭檔案的語言規則,通常會針對目標及其取用者處理的標頭指定單獨的hdrs
屬性。deps
會指定目標的程式碼依附元件。屬性結構定義應指定這些依附元件必須提供哪些供應器。(例如,cc_library
提供CcInfo
)。data
會指定在執行階段期間提供檔案給任何依附目標的可執行檔。這樣即可指定任意檔案。
example_library = rule(
implementation = _example_library_impl,
attrs = {
"srcs": attr.label_list(allow_files = [".example"]),
"hdrs": attr.label_list(allow_files = [".header"]),
"deps": attr.label_list(providers = [ExampleInfo]),
"data": attr.label_list(allow_files = True),
...
},
)
以下是依附元件屬性的範例。任何指定輸入標籤的屬性 (使用 attr.label_list
、attr.label
或 attr.label_keyed_string_dict
定義的屬性) 會在定義目標時,指定目標與其標籤 (或對應的 Label
物件) 之間特定類型的依附元件。系統會按照定義的目標解析存放區,也可能是路徑。
example_library(
name = "my_target",
deps = [":other_target"],
)
example_library(
name = "other_target",
...
)
在這個範例中,other_target
是 my_target
的依附元件,因此系統會先分析 other_target
。如果目標的依附元件圖中有循環,就會發生錯誤。
私人屬性和隱含依附元件
含有預設值的依附元件屬性會建立隱含依附元件。此為隱含的,因為這是使用者未在 BUILD
檔案中指定的目標圖表的一部分。隱含依附元件可用於將規則與工具 (建構時的依附元件,例如編譯器) 之間的關係硬式編碼,因為使用者大多不想指定規則使用的工具。在規則的實作函式中,這會與其他依附元件相同。
如果您想提供隱含的依附元件,但不允許使用者覆寫該值,您可以將屬性設為私密,方法是為其指定開頭為底線 (_
) 的名稱。私密屬性必須有預設值。一般來說,只有在隱含依附元件時,才建議使用私人屬性。
example_library = rule(
implementation = _example_library_impl,
attrs = {
...
"_compiler": attr.label(
default = Label("//tools:example_compiler"),
allow_single_file = True,
executable = True,
cfg = "exec",
),
},
)
在這個範例中,每個 example_library
類型的目標都會對編譯器 //tools:example_compiler
有隱含的依附元件。這樣一來,即使使用者未將標籤做為輸入內容傳遞,example_library
的實作函式也能產生可叫用編譯器的動作。由於 _compiler
是私人屬性,因此 ctx.attr._compiler
一律會在這個規則類型的所有目標中指向 //tools:example_compiler
。或者,您也可以將屬性命名為 compiler
(不含底線),並保留預設值。這樣一來,使用者可視需要替換不同的編譯器,但不需要瞭解編譯器的標籤。
隱含依附元件通常用於與規則實作位於同一存放區的工具。如果工具來自執行平台或其他存放區,規則應從工具鍊取得該工具。
輸出屬性
輸出屬性 (例如 attr.output
和 attr.output_list
) 會宣告目標產生的輸出檔案。這與依附元件屬性有兩個不同之處:
- 這些指令會定義輸出檔案目標,而非參照其他位置定義的目標。
- 輸出檔案目標取決於例項化的規則目標,而非反過來。
一般而言,只有在規則需要以使用者定義的名稱建立輸出內容時,才能以目標名稱為依據,才會使用輸出屬性。如果規則有一個輸出屬性,通常會命名為 out
或 outs
。
建議使用輸出屬性建立預先宣告的輸出內容,具體依附或透過指令列要求。
實作函式
每個規則都需要 implementation
函式。這些函式會在分析階段中嚴格執行,並將載入階段中產生的目標圖形轉換成要在執行階段執行的動作圖表。因此,實作函式無法實際讀取或寫入檔案。
規則實作函式通常是私人性質 (以底線命名)。傳統上,這些類別的名稱會與規則相同,但後置字串為 _impl
。
實作函式僅使用一個參數:一種稱為 ctx
的規則結構定義。而會傳回提供者清單。
目標
在分析期間,依附元件會以 Target
物件表示。這些物件包含執行目標實作函式時產生的提供者。
ctx.attr
有與每個依附元件屬性名稱相對應的欄位,其中包含 Target
物件,透過該屬性代表每個直接依附元件。如果是 label_list
屬性,這是 Targets
清單。對於 label
屬性,這會是單一 Target
或 None
。
目標的實作函式會傳回提供者物件的清單:
return [ExampleInfo(headers = depset(...))]
您可以使用索引符號 ([]
) 存取這些值,並將提供者類型做為索引鍵。這些可以是 Starlark 中定義的自訂供應器,或是可做為 Starlark 全域變數使用的原生規則供應器。
舉例來說,如果規則透過 hdrs
屬性取得標頭檔案,並將其提供給目標及其取用者的編譯動作,則該規則可按照以下方式收集:
def _example_library_impl(ctx):
...
transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]
針對從目標實作函式 (而非提供者物件清單) 傳回 struct
的舊版樣式:
return struct(example_info = struct(headers = depset(...)))
您可以從 Target
物件的對應欄位擷取提供者:
transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]
我們強烈建議您不要使用這種樣式,並應將規則從這種樣式中移除。
檔案
檔案會以 File
物件表示。由於 Bazel 不會在分析階段執行檔案 I/O,因此這些物件無法直接讀取或寫入檔案內容。相反地,會將這些值傳遞至動作發布函式 (請參閱 ctx.actions
),以建構動作圖表的部分。
File
可以是來源檔案或產生的檔案。每個產生的檔案都必須是單一動作的輸出內容。來源檔案不得為任何動作的輸出內容。
對於每個依附元件屬性,ctx.files
的對應欄位會列出透過該屬性,所有依附元件的預設輸出內容:
def _example_library_impl(ctx):
...
headers = depset(ctx.files.hdrs, transitive=transitive_headers)
srcs = ctx.files.srcs
...
ctx.file
包含單一 File
或 None
,用於規格設為 allow_single_file=True
的依附元件屬性。ctx.executable
的運作方式與 ctx.file
相同,但只包含依附元件屬性的欄位,且規格設定為 executable=True
。
宣告輸出內容
在分析階段,規則的實作函式可建立輸出內容。由於所有標籤必須在載入階段已知,因此這些額外輸出沒有標籤。您可以使用 ctx.actions.declare_file
和 ctx.actions.declare_directory
建立用於輸出的 File
物件。輸出的名稱通常以目標的名稱 ctx.label.name
為基礎:
def _example_library_impl(ctx):
...
output_file = ctx.actions.declare_file(ctx.label.name + ".output")
...
對於預先宣告的輸出內容 (例如為輸出屬性建立的項目),改為從 ctx.outputs
的對應欄位擷取 File
物件。
動作
動作說明如何從一組輸入內容產生一組輸出內容,例如「run gcc on hello.c and get hello.o」。建立動作時,Bazel 不會立即執行指令。由於動作可能會依附另一個動作的輸出內容,因此該圖表會登錄在依附元件圖表中。例如,在 C 中,連結器必須在編譯器之後呼叫。
用於建立動作的一般用途函式定義於 ctx.actions
:
ctx.actions.run
,用於執行執行檔。ctx.actions.run_shell
,執行殼層指令。ctx.actions.write
,將字串寫入檔案。ctx.actions.expand_template
,使用範本產生檔案。
ctx.actions.args
可用於有效累積動作的引數。這可避免在執行時間之前扁平化 depset:
def _example_library_impl(ctx):
...
transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
headers = depset(ctx.files.hdrs, transitive=transitive_headers)
srcs = ctx.files.srcs
inputs = depset(srcs, transitive=[headers])
output_file = ctx.actions.declare_file(ctx.label.name + ".output")
args = ctx.actions.args()
args.add_joined("-h", headers, join_with=",")
args.add_joined("-s", srcs, join_with=",")
args.add("-o", output_file)
ctx.actions.run(
mnemonic = "ExampleCompile",
executable = ctx.executable._compiler,
arguments = [args],
inputs = inputs,
outputs = [output_file],
)
...
動作會接受輸入檔案的清單或解碼集,並產生輸出檔案的 (非空白) 清單。在分析階段中,必須知道輸入和輸出檔案的組合。它可能會依屬性值而定,包括依附元件的提供者,但不能依執行結果而定。舉例來說,如果您的動作執行瞭解壓縮指令,您必須指定要加載哪些檔案 (在執行解壓縮前)。在內部建立不同數量檔案的動作,可將這些檔案包裝在單一檔案中 (例如 ZIP、tar 或其他封存格式)。
動作必須列出所有輸入內容。您可以列出未使用的輸入內容,但效率不佳。
動作必須建立所有輸出內容。他們可能會寫入其他檔案,但消費者無法存取非輸出項目。所有宣告的輸出內容都必須透過某個動作寫入。
動作可與純函式相提並論:它們應只依賴提供的輸入內容,並避免存取電腦資訊、使用者名稱、時鐘、網路或 I/O 裝置 (讀取輸入內容和寫入輸出內容除外)。這點非常重要,因為系統會快取並重複使用輸出內容。
Bazel 會解析依附元件,並決定要執行哪些動作。如果依附元件圖表中出現循環,就會發生錯誤。建立動作並不保證會執行,實際結果視版本是否需要輸出而定。
提供者
提供者是規則向依附於該規則的其他規則公開的資訊。這類資料包括輸出檔案、程式庫、用於傳遞工具指令列的參數,或是目標使用者應瞭解的任何其他內容。
由於規則的實作函式只能讀取例項化目標的直接依附元件的提供者,因此規則需要將目標消費者需要知道的任何資訊從目標依附元件轉寄給消費者,通常是將這些資訊累積到 depset
中。
目標的供應者會由實作函式傳回的 Provider
物件清單指定。
舊的實作函式也可以以舊版樣式編寫,其中實作函式會傳回 struct
,而非提供者物件清單。強烈建議您不要使用這個樣式,規則應遷出。
預設輸出
目標的「預設輸出」是在指令列要求目標進行建構時,依預設要求的輸出內容。舉例來說,java_library
目標 //pkg:foo
擁有 foo.jar
做為預設輸出,因此這會由指令 bazel build //pkg:foo
建構。
預設輸出內容由 DefaultInfo
的 files
參數指定:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
如果規則實作未傳回 DefaultInfo
,或是未指定 files
參數,DefaultInfo.files
會預設為所有預先宣告的輸出內容 (通常是是由輸出屬性建立的輸出內容)。
執行動作的規則應提供預設輸出內容,即使這些輸出內容不應直接使用也一樣。系統會刪除不在所要求輸出內容圖表中的動作。如果輸出內容只供目標的使用者使用,則在單獨建構目標時,系統不會執行這些動作。這樣會使除錯作業更加困難,因為只重建失敗的目標不會重現失敗情形。
執行檔案
執行檔是目標在執行階段 (而非建構階段) 使用的一組檔案。在執行階段期間,Bazel 會建立目錄樹狀結構,其中包含指向執行檔案的符號連結。這會暫存二進位檔的環境,使其可在執行階段存取執行檔案。
您可以在建立規則時手動新增執行檔。runfiles
物件可透過規則結構定義 ctx.runfiles
上的 runfiles
方法建立,並傳遞至 DefaultInfo
上的 runfiles
參數。可執行規則的可執行輸出內容會以隱含形式加入至執行檔案。
部分規則會指定屬性,通常會命名為 data
,其輸出內容會加入至目標的執行檔。Runfile 也應從 data
,以及任何可能會提供最終執行程式碼的屬性 (通常為 srcs
,其中可能包含與 data
相關聯的 data
目標) 和 deps
合併。filegroup
def _example_library_impl(ctx):
...
runfiles = ctx.runfiles(files = ctx.files.data)
transitive_runfiles = []
for runfiles_attr in (
ctx.attr.srcs,
ctx.attr.hdrs,
ctx.attr.deps,
ctx.attr.data,
):
for target in runfiles_attr:
transitive_runfiles.append(target[DefaultInfo].default_runfiles)
runfiles = runfiles.merge_all(transitive_runfiles)
return [
DefaultInfo(..., runfiles = runfiles),
...
]
自訂供應器
您可以使用 provider
函式定義提供者,以傳送規則特定資訊:
ExampleInfo = provider(
"Info needed to compile/link Example code.",
fields={
"headers": "depset of header Files from transitive dependencies.",
"files_to_link": "depset of Files from compilation.",
})
規則實作函式可建構並傳回供應工具例項:
def _example_library_impl(ctx):
...
return [
...
ExampleInfo(
headers = headers,
files_to_link = depset(
[output_file],
transitive = [
dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
],
),
)
]
自訂供應商初始化
您可以使用自訂前置處理和驗證邏輯,保護供應器的例項化。這可用於確保所有供應器執行個體都遵循特定不變量,或為使用者提供更簡潔的 API 來取得執行個體。
方法是將 init
回呼傳遞至 provider
函式。如果提供這個回呼,provider()
的傳回類型會變更為兩個值的元組:提供者符號 (在未使用 init
時為一般傳回值) 和「原始建構函式」。
在這種情況下,當供應器符號遭到呼叫時,它會將引數轉寄至 init
回呼,而不是直接傳回新例項。回呼的傳回值必須是將欄位名稱 (字串) 對應至值的字典;用於初始化新執行個體的欄位。請注意,回呼可以有任何簽名,如果引數不符合簽名,系統會回報錯誤,就像直接叫用回呼一樣。
相反地,原始建構函式會略過 init
回呼。
以下範例使用 init
來預先處理及驗證其引數:
# //pkg:exampleinfo.bzl
_core_headers = [...] # private constant representing standard library files
# It's possible to define an init accepting positional arguments, but
# keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
if not files_to_link and not allow_empty_files_to_link:
fail("files_to_link may not be empty")
all_headers = depset(_core_headers, transitive = headers)
return {'files_to_link': files_to_link, 'headers': all_headers}
ExampleInfo, _new_exampleinfo = provider(
...
init = _exampleinfo_init)
export ExampleInfo
接著,規則實作可能會將供應器例項化,如下所示:
ExampleInfo(
files_to_link=my_files_to_link, # may not be empty
headers = my_headers, # will automatically include the core headers
)
原始建構函式可用來定義不會通過 init
邏輯的其他公用工廠函式。例如,在 exampleinfo.bzl 中,我們可以定義:
def make_barebones_exampleinfo(headers):
"""Returns an ExampleInfo with no files_to_link and only the specified headers."""
return _new_exampleinfo(files_to_link = depset(), headers = all_headers)
一般來說,原始建構函式會繫結至名稱以底線開頭 (上方 _new_exampleinfo
) 的變數,因此使用者程式碼無法載入該變數,並產生任意提供者例項。
init
的另一個用途是直接防止使用者完全呼叫供應器符號,並強制使用者改用工廠函式:
def _exampleinfo_init_banned(*args, **kwargs):
fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")
ExampleInfo, _new_exampleinfo = provider(
...
init = _exampleinfo_init_banned)
def make_exampleinfo(...):
...
return _new_exampleinfo(...)
可執行的規則和測試規則
可執行規則會定義可由 bazel run
指令叫用的目標。測試規則是一種特殊的可執行規則,其目標也可以透過 bazel test
指令叫用。如要建立可執行和測試規則,請在呼叫 rule
時,將對應的 executable
或 test
引數設為 True
:
example_binary = rule(
implementation = _example_binary_impl,
executable = True,
...
)
example_test = rule(
implementation = _example_binary_impl,
test = True,
...
)
測試規則的名稱必須以 _test
結尾。(測試目標名稱通常也會依慣例以 _test
結尾,但這並非硬性規定)。非測試規則不得使用這個後置字串。
這兩種規則必須產生可由 run
或 test
指令叫用的可執行輸出檔案 (不一定預先宣告)。如要告知 Bazel 將哪一項規則輸出內容做為執行檔,請將該輸出內容做為傳回的 DefaultInfo
提供者的 executable
引數傳遞。該 executable
會新增至規則的預設輸出內容中,因此您不必同時傳遞給 executable
和 files
。此參數也會以隱含形式新增至 runfiles:
def _example_binary_impl(ctx):
executable = ctx.actions.declare_file(ctx.label.name)
...
return [
DefaultInfo(executable = executable, ...),
...
]
產生這個檔案的動作必須設定檔案的可執行位元。如果是 ctx.actions.run
或 ctx.actions.run_shell
動作,應由該動作叫用的基礎工具完成。若是 ctx.actions.write
動作,請傳遞 is_executable=True
。
做為舊版行為,可執行的規則有預先宣告的特殊 ctx.outputs.executable
輸出內容。如果您未使用 DefaultInfo
指定執行檔,系統會將這個檔案做為預設執行檔;否則,請勿使用這個檔案。此輸出機制不支援在分析時自訂可執行檔的名稱,因此已遭淘汰。
除了為所有規則新增的屬性外,可執行規則和測試規則還會隱含定義其他屬性。隱含新增屬性的預設值無法變更,但您可以將私人規則包裝在變更預設值的 Starlark 巨集中,藉此解決這個問題:
def example_test(size="small", **kwargs):
_example_test(size=size, **kwargs)
_example_test = rule(
...
)
執行檔案位置
使用 bazel run
(或 test
) 執行可執行目標時,執行檔案目錄的根層級會相鄰。路徑的關係如下:
# Given launcher_path and runfile_file:
runfiles_root = launcher_path.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
runfiles_root, workspace_name, runfile_path)
runfiles 目錄下 File
的路徑會對應至 File.short_path
。
由 bazel
直接執行的二進位檔與 runfiles
目錄的根目錄相鄰。但是,從執行檔案「從」呼叫的二進位檔無法取得相同的假設。為緩解此問題,每個二進位檔均應提供一種方法,使用環境或指令列引數/標記,接受其執行檔案根目錄做為參數。如此一來,二進位檔就能將正確的標準執行檔根目錄傳送至其呼叫的二進位檔。如未設定,二進位檔可以猜測該是第一個二進位檔,並尋找相鄰的執行檔案目錄。
進階主題
要求輸出檔案
單一目標可以有多個輸出檔案。執行 bazel build
指令時,系統會將指令指定的目標輸出內容視為要求。Bazel 只會建構這些要求的檔案以及直接或間接依附的檔案。(就動作圖表而言,Bazel 只會執行可做為要求檔案間接依附元件的動作)。
除了預設輸出以外,您也可以透過指令列明確要求任何預先宣告的輸出內容。規則可以透過輸出屬性指定預先宣告的輸出內容。在這種情況下,使用者在例項化規則時,會明確選擇輸出的標籤。如要取得輸出屬性的 File
物件,請使用 ctx.outputs
的對應屬性。規則也可以根據目標名稱隱含定義預先宣告的輸出內容,但這項功能已淘汰。
除了預設輸出內容外,還有輸出群組,也就是一組可一起要求的輸出檔案。您可以使用 --output_groups
要求這些指令。舉例來說,如果目標 //pkg:mytarget
是具有 debug_files
輸出群組的規則類型,則可透過執行 bazel build //pkg:mytarget
--output_groups=debug_files
建構這些檔案。由於未預先宣告的輸出內容沒有標籤,因此只能要求顯示在預設輸出或輸出群組中。
您可以使用 OutputGroupInfo
供應器指定輸出群組。請注意,與許多內建供應器不同,OutputGroupInfo
可使用任意名稱的參數,定義具有該名稱的輸出群組:
def _example_library_impl(ctx):
...
debug_file = ctx.actions.declare_file(name + ".pdb")
...
return [
DefaultInfo(files = depset([output_file]), ...),
OutputGroupInfo(
debug_files = depset([debug_file]),
all_files = depset([output_file, debug_file]),
),
...
]
此外,與大多數提供者不同的是,OutputGroupInfo
可由「切面」和要套用該切面的規則目標傳回,前提是其未定義相同的輸出群組。在這種情況下,系統會合併產生的供應者。
請注意,OutputGroupInfo
通常不應用於將特定類型的檔案從目標傳送至其使用者的動作。請改為定義規則專屬供應器。
設定
假設您要為不同架構建構 C++ 二進位檔。建構作業可能相當複雜,並涉及多個步驟。部分中繼二進位檔 (例如編譯器和程式碼產生器) 必須在執行平台 (可以是主機或遠端執行程式) 上執行。某些二進位檔 (例如最終輸出內容) 必須針對目標架構建構。
因此,Bazel 有「設定」和「轉換」的概念。最上層的目標 (在指令列上要求的目標) 會在「target」設定中建構,而應在執行平台上執行的工具則會在「exec」設定中建構。規則可能會根據設定產生不同的動作,例如變更傳遞至編譯器的 CPU 架構。在某些情況下,不同設定可能需要相同的程式庫。如果發生這種情況,系統會分析模型,並可能會多次建構。
根據預設,Bazel 會在與目標本身相同的設定中建構目標依附元件,也就是不進行轉換。如果依附元件是用來建構目標所需的工具,則對應的屬性應指定轉換至執行設定的作業。這會導致工具及其所有依附元件為執行平台建構。
針對每個依附元件屬性,您可以使用 cfg
決定依附元件應在相同設定中建構,還是轉換為執行設定。如果依附元件屬性含有 executable=True
標記,則 cfg
必須明確設定。這可避免意外為錯誤設定建構工具。查看示例
一般而言,執行階段所需的來源、依附程式庫和可執行檔可以使用相同的設定。
在建構作業中執行的工具 (例如編譯器或程式碼產生器),應針對 Exc 設定建構。在此情況下,請在屬性中指定 cfg="exec"
。
否則,應針對目標設定建構執行階段使用的執行檔 (例如做為測試的一部分)。在這種情況下,請在屬性中指定 cfg="target"
。
cfg="target"
實際上並不會執行任何操作,它只是方便規則設計人員明確表達意圖的值。當 executable=False
為 cfg
時,表示 cfg
為選用項目,請只在確實有助於提升可讀性時才設定。
您也可以使用 cfg=my_transition
來利用使用者定義的轉場效果,讓規則作者在變更設定時享有極大的彈性,同時又能放大建構圖表,使其不易理解。
注意:Bazel 以往並沒有執行平台的概念,而是將所有建構動作都視為在主機上執行。6.0 之前的 Bazel 版本會建立獨特的「主機」設定來代表此情況。如果您在程式碼或舊說明文件中看到「主機」的參照,就是這樣。建議您使用 Bazel 6.0 以上版本,避免這項額外的概念額外負擔。
設定片段
規則可能會存取設定片段,例如 cpp
、java
和 jvm
。不過,您必須宣告所有必要片段,才能避免存取錯誤:
def _impl(ctx):
# Using ctx.fragments.cpp leads to an error since it was not declared.
x = ctx.fragments.java
...
my_rule = rule(
implementation = _impl,
fragments = ["java"], # Required fragments of the target configuration
host_fragments = ["java"], # Required fragments of the host configuration
...
)
Runfiles 符號連結
一般來說,執行檔案樹狀結構中檔案的相對路徑與來源樹狀結構或產生的輸出樹狀結構中,該檔案的相對路徑相同。如果這些值因某些原因而需要不同,您可以指定 root_symlinks
或 symlinks
引數。root_symlinks
是檔案的字典對應路徑,其中的路徑與執行檔案目錄的根目錄相關。symlinks
字典相同,但路徑會間接加上主要工作區的名稱 (「並非」包含目前目標的存放區名稱)。
...
runfiles = ctx.runfiles(
root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
)
# Creates something like:
# sometarget.runfiles/
# some/
# path/
# here.foo -> some_data_file2
# <workspace_name>/
# some/
# path/
# here.bar -> some_data_file3
如果使用 symlinks
或 root_symlinks
,請小心不要將兩個不同的檔案對應至 runfiles 樹狀結構中的相同路徑。這會導致建構作業失敗,並收到說明衝突的錯誤訊息。如要修正這個問題,您必須修改 ctx.runfiles
引數,以移除衝突。這項檢查會針對使用您規則的任何目標,以及依附這些目標的任何種類目標執行檢查。如果您的工具可能會由其他工具間接使用,這會特別具有風險;工具的所有執行檔案及其所有依附元件中的符號連結名稱都不得重複。
程式碼涵蓋率
執行 coverage
指令時,建構作業可能需要針對特定目標新增涵蓋範圍檢測。建構作業也會收集已檢測的來源檔案清單。系統會根據 --instrumentation_filter
標記控管要考量的目標子集。除非指定 --instrument_test_targets
,否則系統會排除測試目標。
如果規則實作在建構期間新增涵蓋率檢測功能,就必須在實作函式中考量這項因素。如果目標的來源應進行檢測,ctx.coverage_instrumented 會在涵蓋率模式中傳回 true:
# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
# Do something to turn on coverage for this compile action
無論目標的來源是否經過檢測,都可以在 ctx.configuration.coverage_enabled 上設定一律必須處於涵蓋率模式的邏輯。
如果規則在編譯前直接納入其依附元件的來源 (例如標頭檔案),則如果依附元件的來源需要檢測,規則可能也需要啟用編譯時間檢測:
# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
(ctx.coverage_instrumented() or
any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
# Do something to turn on coverage for this compile action
規則也應提供資訊,說明哪些屬性與 InstrumentedFilesInfo
供應商的涵蓋率相關,並使用 coverage_common.instrumented_files_info
建構。instrumented_files_info
的 dependency_attributes
參數應列出所有執行階段依附元件屬性,包括 deps
等程式碼依附元件和 data
等資料依附元件。如果可能要新增涵蓋率檢測工具,source_attributes
參數應列出規則的來源檔案屬性:
def _example_library_impl(ctx):
...
return [
...
coverage_common.instrumented_files_info(
ctx,
dependency_attributes = ["deps", "data"],
# Omitted if coverage is not supported for this rule:
source_attributes = ["srcs", "hdrs"],
)
...
]
如果未傳回 InstrumentedFilesInfo
,系統會在 dependency_attributes
中使用每個非工具依附元件屬性 (未將 cfg
設為屬性結構定義中的 "host"
或 "exec"
) 建立預設 InstrumentedFilesInfo
。(這並不是理想的行為,因為這會將 srcs
等屬性放入 dependency_attributes
而非 source_attributes
,但您可以避免對依附元件鏈結中的所有規則使用明確涵蓋範圍設定)。
驗證動作
有時您需要驗證建構的內容,而驗證作業所需的資訊也只會出現在構件 (來源檔案或產生的檔案) 中。由於這項資訊位於構件中,因此規則無法在分析時執行這項驗證,因為規則無法讀取檔案。相反地,動作必須在執行時進行這項驗證。驗證失敗時,動作會失敗,因此建構作業也會失敗。
可能執行的驗證範例包括靜態分析、程式碼檢查、依附元件和一致性檢查,以及樣式檢查。
驗證動作也可以將建構構件不需要的動作移至獨立的動作,藉此提升建構效能。舉例來說,如果執行編譯和 lint 的單一動作可分為編譯動作和 lint 動作,則 lint 動作可做為驗證動作執行,並與其他動作並行執行。
這些「驗證動作」通常不會產生在建構其他位置使用的任何項目,因為這類動作只需要斷言輸入內容。但這會產生問題:如果驗證動作並未產生在建構作業的其他位置使用的內容,規則會如何要求執行動作?過去,這個方法的用意是讓驗證動作輸出空白檔案,然後以人工方式將該輸出內容新增至建構作業中其他重要動作的輸入內容:
這麼做是可行的,因為 Bazel 會在編譯動作執行時一律執行驗證動作,但這有重大缺點:
驗證動作在建構的重要路徑中。由於 Bazel 認為執行編譯動作所需的空白輸出內容是必要的,因此會先執行驗證動作,即使編譯動作會忽略輸入內容。這會降低平行作業數量,並減慢建構作業速度。
如果建構中的其他動作可能而非編譯動作,則還需要將驗證動作的空白輸出內容新增至這些動作 (例如
java_library
的來源 jar 輸出內容)。如果之後新增可能會而非編譯動作的新動作,導致空白驗證輸出內容意外消失,這也會問題。
如要解決這類問題,請使用「驗證輸出群組」。
驗證輸出群組
「驗證輸出群組」是一個輸出群組,其設計為保留未使用且未用到的驗證動作輸出內容,因此無需人為地加入其他動作的輸入內容。
這個群組的特別之處在於,無論 --output_groups
標記的值為何,且無論目標的依附方式為何 (例如在指令列上、做為依附項目,或透過目標的隱含輸出),系統一律會要求輸出內容。請注意,系統仍會套用一般快取和遞增功能:如果驗證動作的輸入內容未變更,且先前驗證動作已成功,則系統不會執行驗證動作。
使用這個輸出群組時,驗證動作仍必須輸出某個檔案,即使是空白檔案也一樣。這可能需要包裝一些通常不會產生輸出的工具,以便建立檔案。
在下列三種情況下,目標的驗證動作無法執行:
- 目標依附於工具時
- 目標是做為隱含依附元件 (例如開頭為「_」的屬性) 而依附
- 如果在主機或 exec 設定中建立了目標。
假設這些目標都有各自的獨立建構和測試,可找出任何驗證失敗情形。
使用驗證輸出群組
「驗證輸出群組」的名稱是 _validation
,使用方式類似於任何其他輸出群組:
def _rule_with_validation_impl(ctx):
ctx.actions.write(ctx.outputs.main, "main output\n")
ctx.actions.write(ctx.outputs.implicit, "implicit output\n")
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
ctx.actions.run(
outputs = [validation_output],
executable = ctx.executable._validation_tool,
arguments = [validation_output.path])
return [
DefaultInfo(files = depset([ctx.outputs.main])),
OutputGroupInfo(_validation = depset([validation_output])),
]
rule_with_validation = rule(
implementation = _rule_with_validation_impl,
outputs = {
"main": "%{name}.main",
"implicit": "%{name}.implicit",
},
attrs = {
"_validation_tool": attr.label(
default = Label("//validation_actions:validation_tool"),
executable = True,
cfg = "exec"),
}
)
請注意,驗證輸出檔案不會新增至 DefaultInfo
,也不會新增至任何其他動作的輸入內容。如果目標取決於標籤,或目標的任何隱含輸出內容直接或間接依賴,則該規則種類目標的驗證動作仍會執行。
通常很重要的是,驗證動作的輸出內容只會進入驗證輸出群組,不會加進其他動作的輸入內容,因為這樣可能會擊敗平行處理量的增益。不過請注意,Bazel 目前沒有任何特殊檢查機制來強制執行這項規定。因此,您應該測試驗證動作輸出內容不會新增至 Starlark 規則測試中任何動作的輸入內容。例如:
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
def _validation_outputs_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
target = analysistest.target_under_test(env)
validation_outputs = target.output_groups._validation.to_list()
for action in actions:
for validation_output in validation_outputs:
if validation_output in action.inputs.to_list():
analysistest.fail(env,
"%s is a validation action output, but is an input to action %s" % (
validation_output, action))
return analysistest.end(env)
validation_outputs_test = analysistest.make(_validation_outputs_test_impl)
驗證動作標記
執行驗證動作是由 --run_validations
指令列標記 (預設為 true) 控管。
已淘汰的功能
已淘汰的預先宣告輸出內容
使用預先宣告的輸出內容有兩種已淘汰的方式:
rule
的outputs
參數會指定輸出屬性名稱和字串範本之間的對應關係,以產生預先宣告的輸出標籤。建議使用非預先宣告的輸出內容,並明確將輸出內容新增至DefaultInfo.files
。使用規則目標的標籤做為輸出內容的規則輸入內容,而非預先宣告的輸出內容標籤。針對可執行規則,
ctx.outputs.executable
會參照名稱與規則目標相同的預先宣告可執行檔輸出內容。建議您明確宣告輸出內容,例如使用ctx.actions.declare_file(ctx.label.name)
,並確保產生可執行檔的指令會設定其權限,以便執行。將可執行的輸出內容明確傳遞至DefaultInfo
的executable
參數。
應執行的檔案功能
ctx.runfiles
和 runfiles
類型具有複雜的功能組合,其中許多功能都是基於舊版功能而保留。請參考下列建議,降低複雜度:
避免使用
ctx.runfiles
的collect_data
和collect_default
模式。這些模式會以令人困惑的方式,在特定的硬式編碼依附元件邊緣間隱含地收集執行檔。請改用ctx.runfiles
的files
或transitive_files
參數,或透過runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
合併依附元件的執行檔。避免使用
DefaultInfo
建構函式的data_runfiles
和default_runfiles
。請改為指定DefaultInfo(runfiles = ...)
。基於舊版原因,系統仍會保留「預設」和「資料」執行檔案之間的區別。舉例來說,部分規則會將預設輸出內容放入data_runfiles
,但不會放入default_runfiles
。規則應同時包含預設輸出內容,並從提供執行檔的屬性 (通常是data
) 合併default_runfiles
。請勿使用data_runfiles
。從
DefaultInfo
擷取runfiles
時 (通常只用於合併目前規則及其依附元件之間的執行檔案),請使用DefaultInfo.default_runfiles
,「而非」DefaultInfo.data_runfiles
。
從舊版供應器遷移
以往,Bazel 供應工具是 Target
物件上的簡單欄位。這些函式可使用點號運算子存取,且是透過將該欄位放入規則實作函式傳回的結構中來建立。
這類樣式已淘汰,不應在新的程式碼中使用;請參閱下方資訊,瞭解如何進行遷移。新的供應器機制可避免名稱衝突。它也支援資料隱藏功能,要求任何存取提供者執行個體的程式碼,都必須使用提供者符號擷取資料。
目前仍支援舊版供應商。規則可同時傳回舊版和新型提供者,如下所示:
def _old_rule_impl(ctx):
...
legacy_data = struct(x="foo", ...)
modern_data = MyInfo(y="bar", ...)
# When any legacy providers are returned, the top-level returned value is a
# struct.
return struct(
# One key = value entry for each legacy provider.
legacy_info = legacy_data,
...
# Additional modern providers:
providers = [modern_data, ...])
如果此規則例項產生的 dep
是 Target
物件,則提供者及其內容可以做為 dep.legacy_info.x
和 dep[MyInfo].y
擷取。
除了 providers
之外,傳回的結構體也可以使用具有特殊意義的其他幾個欄位 (因此不會建立相應的舊版提供者):
files
、runfiles
、data_runfiles
、default_runfiles
和executable
欄位對應至DefaultInfo
中同名的欄位。系統不允許在傳回DefaultInfo
提供者時指定任何這些欄位。欄位
output_groups
會採用結構體值,並對應至OutputGroupInfo
。
在規則的 provides
宣告中,以及依附元件屬性的 providers
宣告中,舊版供應器會以字串形式傳入,而新版供應器則會以 *Info
符號傳入。遷移時,請務必從字串變更為符號。如果是複雜或大型規則集,且難以不可分割地更新所有規則,只要按照以下步驟順序操作,可能會比較方便:
請使用上述語法修改產生舊版供應者的規則,以便同時產生舊版和新版供應者。如果規則宣告會傳回舊版供應器,請更新該宣告,讓舊版和新版供應器都包含在內。
修改使用舊版供應器的規則,改為使用新版供應器。如果有任何屬性宣告需要舊版提供者,也請一併將其更新為需要新式供應器。您也可以選擇讓消費者接受/要求供應商:使用
hasattr(target, 'foo')
測試是否存在舊版提供者,或使用FooInfo in target
測試在步驟 1 與步驟 1 之間的工作交錯。從所有規則中完全移除舊版供應器。