規則

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

「規則」會定義 Bazel 對輸入內容執行的一系列「動作」,以產生一組輸出內容,這些輸出內容會參照規則的「實作函式」傳回的「供應器」。舉例來說,C++ 二進位規則可能:

  1. 取得一組 .cpp 來源檔案 (輸入內容)。
  2. 對來源檔案執行 g++ (動作)。
  3. 傳回 DefaultInfo 提供者,其中包含可執行輸出內容和其他檔案,以便在執行階段提供。
  4. 傳回 CcInfo 提供者,其中包含從目標及其依附元件收集的 C++ 專屬資訊。

從 Bazel 的角度來看,g++ 和標準 C++ 程式庫也是這項規則的輸入內容。撰寫規則時,您不僅要考量使用者提供的規則輸入內容,還必須考量執行動作所需的所有工具和程式庫。

建立或修改任何規則前,請務必先熟悉 Bazel 的建構階段。請務必瞭解建構的三個階段 (載入、分析和執行)。瞭解巨集也有助於區分規則和巨集。如要開始使用,請先參閱規則教學課程。 然後參考這個頁面。

Bazel 本身內建幾項規則。這些原生規則 (例如 genrulefilegroup) 提供一些核心支援。定義自己的規則,即可新增 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 檔案以例項化目標時。

屬性

屬性是規則引數。屬性可為目標的實作提供特定值,也可以參照其他目標,建立依附元件圖表。

規則專屬屬性 (例如 srcsdeps) 的定義方式,是將屬性名稱對應至結構定義 (使用 attr 模組建立) 的對映,傳遞至 ruleattrs 參數。常見屬性 (例如 namevisibility) 會隱含地新增至所有規則。其他屬性會隱含地新增至可執行檔和測試規則。隱含新增至規則的屬性無法包含在傳遞至 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_listattr.labelattr.label_keyed_string_dict 定義),都會在定義目標時,指定目標與標籤 (或對應的 Label 物件) 列於該屬性的目標之間,存在特定類型的依附元件。這些標籤的存放區 (可能還有路徑) 會相對於定義的目標解析。

example_library(
    name = "my_target",
    deps = [":other_target"],
)

example_library(
    name = "other_target",
    ...
)

在本例中,other_targetmy_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.outputattr.output_list) 會宣告目標產生的輸出檔案。這與依附元件屬性有以下兩點不同:

  • 這些目標會定義輸出檔案目標,而不是參照其他位置定義的目標。
  • 輸出檔案目標取決於例項化的規則目標,而非反過來。

一般來說,只有在規則需要建立具有使用者定義名稱的輸出內容時,才會使用輸出屬性,而這些名稱無法以目標名稱為依據。如果規則有一個輸出屬性,通常會命名為 outouts

輸出屬性是建立預先宣告輸出的首選方式,這些輸出可特別依附或在指令列中要求

實作函式

每條規則都需要 implementation 函式。這些函式會嚴格在分析階段執行,並將載入階段產生的目標圖形,轉換為執行階段要執行的動作圖形。因此,實作函式實際上無法讀取或寫入檔案。

規則實作函式通常是私有函式 (名稱開頭為底線)。按照慣例,這些檔案的名稱會與規則相同,但會加上 _impl 後置字串。

實作函式只會採用一個參數:規則環境,通常命名為 ctx。並傳回供應商清單。

目標

在分析時,依附元件會以 Target 物件表示。這些物件包含執行目標實作函式時產生的供應器

ctx.attr 具有對應每個依附元件屬性名稱的欄位,其中包含 Target 物件,代表使用該屬性的每個直接依附元件。如果是 label_list 屬性,則為 Targets 清單。如果是 label 屬性,則為單一 TargetNone

目標的實作函式會傳回供應商物件清單:

return [ExampleInfo(headers = depset(...))]

您可以使用索引標記法 ([]) 存取這些項目,並以提供者類型做為鍵。這些可以是 Starlark 中定義的自訂供應商,也可以是做為 Starlark 全域變數的原生規則供應商

舉例來說,如果規則使用 hdrs 屬性取得標頭檔案,並將這些檔案提供給目標及其取用者的編譯動作,則可以像這樣收集這些檔案:

def _example_library_impl(ctx):
    ...
    transitive_headers = [hdr[ExampleInfo].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 包含單一 FileNone,適用於規格設定 allow_single_file = True 的依附屬性。ctx.executable 的行為與 ctx.file 相同,但只包含規格設定 executable = True 的依附元件屬性欄位。

宣告輸出

在分析階段,規則的實作函式可以建立輸出內容。 由於所有標籤都必須在載入階段中得知,因此這些額外輸出內容沒有標籤。輸出內容的 File 物件可以使用 ctx.actions.declare_filectx.actions.declare_directory 建立。輸出內容的名稱通常會根據目標的名稱而定,ctx.label.name

def _example_library_impl(ctx):
  ...
  output_file = ctx.actions.declare_file(ctx.label.name + ".output")
  ...

如果是預先宣告的輸出內容 (例如為輸出屬性建立的輸出內容),則可改為從 ctx.outputs 的對應欄位擷取 File 物件。

動作

動作說明如何從一組輸入內容產生一組輸出內容,例如「在 hello.c 上執行 gcc 並取得 hello.o」。建立動作時,Bazel 不會立即執行指令,這項動作會在依附元件圖中註冊,因為動作可能會依附於其他動作的輸出內容。舉例來說,在 C 中,必須在編譯器之後呼叫連結器。

建立動作的一般用途函式定義於 ctx.actions

ctx.actions.args 可用於有效累積動作的引數。這樣可避免在執行時間前將 depsets 扁平化:

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

動作會採用輸入檔案清單或 depset,並產生 (非空白) 輸出檔案清單。在分析階段,必須知道輸入和輸出檔案集。這可能取決於屬性的值,包括來自依附元件的供應商,但不能取決於執行結果。舉例來說,如果動作會執行解壓縮指令,您必須指定要解壓縮的檔案 (在執行解壓縮前)。如果動作會在內部建立數量不定的檔案,可以將這些檔案包裝成單一檔案 (例如 ZIP、TAR 或其他封存格式)。

動作必須列出所有輸入內容。允許列出未使用的輸入內容,但效率不彰。

動作必須建立所有輸出內容。他們可能會寫入其他檔案,但消費者無法存取輸出內容以外的任何項目。所有已宣告的輸出內容都必須由某個動作寫入。

動作可比照純函式:動作應只依據提供的輸入內容,並避免存取電腦資訊、使用者名稱、時鐘、網路或 I/O 裝置 (讀取輸入內容和寫入輸出內容除外)。這點很重要,因為輸出內容會經過快取並重複使用。

Bazel 會解析依附元件,並決定要執行的動作。如果依附元件圖中有週期,就會發生錯誤。建立動作不保證會執行,這取決於建構作業是否需要動作的輸出內容。

提供者

規則會將提供者資訊公開給其他依附於該規則的規則。這類資料包括輸出檔案、程式庫、要傳遞至工具指令列的參數,或目標的消費者應瞭解的任何其他資訊。

由於規則的實作函式只能從已例項化的目標直接依附元件讀取供應器,因此規則需要轉送目標依附元件中的任何資訊,這些資訊通常會累積到 depset 中,供目標的消費者使用。

目標的供應商是由實作函式傳回的供應商物件清單指定。

舊版實作函式也可以採用舊版樣式編寫,其中實作函式會傳回 struct,而非供應商物件清單。強烈建議不要使用這種樣式,並應遷移規則

預設輸出

目標的預設輸出內容是指在指令行要求建構目標時,預設要求輸出的內容。舉例來說,java_library 目標 //pkg:foo 具有 foo.jar 做為預設輸出內容,因此會由 bazel build //pkg:foo 指令建構。

預設輸出內容是由 DefaultInfofiles 參數指定:

def _example_library_impl(ctx):
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        ...
    ]

如果規則實作項目未傳回 DefaultInfo,或未指定 files 參數,DefaultInfo.files 會預設為所有預先宣告的輸出內容 (通常是透過 output 屬性建立的輸出內容)。

即使預期不會直接使用這些輸出內容,執行動作的規則也應提供預設輸出內容。系統會修剪不在所要求輸出內容圖表中的動作。如果輸出內容只供目標的消費者使用,當目標獨立建構時,這些動作就不會執行。這會增加除錯難度,因為重建失敗的目標無法重現失敗情形。

Runfiles

執行檔是目標在執行階段 (而非建構階段) 使用的一組檔案。在執行階段,Bazel 會建立包含指向執行檔案的符號連結目錄樹狀結構。這會為二進位檔準備環境,讓二進位檔在執行階段存取執行檔。

您可以在建立規則時手動新增 Runfile。 runfiles 物件可透過規則環境的 runfiles 方法 ctx.runfiles 建立,並傳遞至 DefaultInforunfiles 參數。可執行檔規則的可執行輸出內容會隱含地新增至 Runfile。

部分規則會指定屬性 (通常命名為 data),其輸出內容會新增至目標的執行檔。也應從 data 合併 Runfile,以及從任何可能提供最終執行程式碼的屬性合併,通常是 srcs (可能包含相關聯 datafilegroup 目標) 和 deps

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

# 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(
    fields = ["files_to_link", "headers"],
    init = _exampleinfo_init,
)

規則實作作業隨後可能會例項化供應商,如下所示:

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 的呼叫中,將對應的 executabletest 引數設為 True 而建立。

example_binary = rule(
   implementation = _example_binary_impl,
   executable = True,
   ...
)

example_test = rule(
   implementation = _example_binary_impl,
   test = True,
   ...
)

測試規則的名稱結尾必須是 _test。(依照慣例,測試目標名稱通常也會以 _test 結尾,但並非必要。)非測試規則不得有這個後置字元。

這兩種規則都必須產生可執行的輸出檔案 (可能預先宣告,也可能未預先宣告),並由 runtest 指令叫用。如要告訴 Bazel 將規則的哪個輸出內容做為這個可執行檔,請將其做為傳回的 DefaultInfo 供應器 executable 引數傳遞。該 executable 會新增至規則的預設輸出內容 (因此您不需要將該內容傳遞至 executablefiles)。該內容也會隱含地新增至 runfiles

def _example_binary_impl(ctx):
    executable = ctx.actions.declare_file(ctx.label.name)
    ...
    return [
        DefaultInfo(executable = executable, ...),
        ...
    ]

產生這個檔案的動作必須在檔案上設定可執行位元。如果是 ctx.actions.runctx.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(
 ...
)

Runfiles 位置

使用 bazel run (或 test) 執行可執行目標時,執行檔旁邊就是 runfiles 目錄的根目錄。路徑的關係如下:

# 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 目錄的根目錄相鄰。不過,從 Runfile 呼叫的二進位檔無法做出相同假設。為減輕這項問題,每個二進位檔都應提供方法,透過環境、指令列引數或旗標,將執行檔根目錄做為參數接受。這樣一來,二進位檔就能將正確的標準執行檔根目錄傳遞至所呼叫的二進位檔。如果未設定,二進位檔可以猜測這是第一個呼叫的二進位檔,並尋找相鄰的 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 會以與目標本身相同的設定 (也就是沒有轉場) 建構目標的依附元件。如果依附元件是建構目標時所需的工具,對應的屬性應指定轉換為 exec 設定。這會導致工具及其所有依附元件建構執行平台。

針對每個依附元件屬性,您可以使用 cfg 決定依附元件是否應建構在相同設定中,或轉換為執行設定。如果依附元件屬性有 executable = True 旗標,則必須明確設定 cfg。這是為了避免意外建構出設定錯誤的工具。查看範例

一般來說,執行階段所需的來源、依附程式庫和可執行檔可以使用相同的設定。

應為 exec 設定建構在建構作業中執行的工具 (例如編譯器或程式碼產生器)。在此情況下,請在屬性中指定 cfg = "exec"

否則,在執行階段使用的可執行檔 (例如測試的一部分) 應針對目標設定建構。在此情況下,請在屬性中指定 cfg = "target"

cfg = "target" 實際上不會執行任何動作,這純粹是方便的值,可協助規則設計人員明確表達意圖。當 executable = False 時,表示 cfg 為選用,只有在確實有助於提高可讀性時,才設定此項目。

您也可以使用 cfg = my_transition 採用使用者定義的轉換,讓規則作者在變更設定時享有極大的彈性,但缺點是建構圖會變得更大,更難以理解

注意:在過去,Bazel 沒有執行平台的概念,而是將所有建構動作視為在主機上執行。6.0 之前的 Bazel 版本會建立不同的「主機」設定來代表這項設定。如果您在程式碼或舊版說明文件中看到「主機」的參照,指的就是這個。建議使用 Bazel 6.0 以上版本,以免產生額外的概念負擔。

設定片段

規則可以存取設定片段,例如 cppjava。不過,您必須宣告所有必要片段,以免發生存取錯誤:

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
    ...
)

一般來說,執行檔案樹狀目錄中的檔案相對路徑,與來源樹狀目錄或產生的輸出樹狀目錄中的檔案相對路徑相同。如果因故需要使用不同值,可以指定 root_symlinkssymlinks 引數。root_symlinks 是將路徑對應至檔案的字典,路徑與 runfiles 目錄的根目錄相關。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

如果使用 symlinksroot_symlinks,請小心不要將兩個不同的檔案對應至執行檔樹狀結構中的相同路徑。這會導致建構失敗,並顯示說明衝突的錯誤。如要修正這個問題,請修改 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.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_infodependency_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,系統會為每個未在屬性結構定義中將 cfg 設為 "exec" 的非工具依附元件屬性,建立預設屬性。dependency_attributes(這並非理想行為,因為這會將 srcs 等屬性放在 dependency_attributes 中,而不是 source_attributes 中,但可避免為依附鏈中的所有規則明確設定涵蓋範圍。)

測試規則

測試規則需要額外設定,才能產生涵蓋範圍報告。規則本身必須新增下列隱含屬性:

my_test = rule(
    ...,
    attrs = {
        ...,
        # Implicit dependencies used by Bazel to generate coverage reports.
        "_lcov_merger": attr.label(
            default = configuration_field(fragment = "coverage", name = "output_generator"),
            executable = True,
            cfg = config.exec(exec_group = "test"),
        ),
        "_collect_cc_coverage": attr.label(
            default = "@bazel_tools//tools/test:collect_cc_coverage",
            executable = True,
            cfg = config.exec(exec_group = "test"),
        )
    },
    test = True,
)

只要不要求涵蓋範圍,使用 configuration_field 即可避免對 Java LCOV 合併工具的依附元件。

執行測試時,系統應會將一或多個LCOV 檔案以不重複的名稱,發送到 COVERAGE_DIR 環境變數指定的目錄。接著,Bazel 會使用 _lcov_merger 工具將這些檔案合併為單一 LCOV 檔案。如果存在,系統也會使用 _collect_cc_coverage 工具收集 C/C++ 涵蓋範圍。

基準涵蓋範圍

由於涵蓋範圍只會針對測試的依附元件樹狀結構中最終的程式碼收集,因此涵蓋範圍報表可能會誤導您,因為這些報表不一定會涵蓋 --instrumentation_filter 標記比對到的所有程式碼。

因此,Bazel 允許規則使用 ctx.instrumented_files_infobaseline_coverage_files 屬性指定基準涵蓋範圍檔案。這些檔案必須由使用者定義的動作以 LCOV 格式產生,且應列出目標來源檔案中的所有行、分支、函式和/或區塊 (根據 sources_attributesextensions 參數)。對於已針對涵蓋範圍進行插樁的目標中的來源檔案,Bazel 會將其基準涵蓋範圍合併至使用 --combined_report 產生的合併涵蓋範圍報表,確保未測試的檔案仍會顯示為未涵蓋。

如果規則未提供任何基準涵蓋範圍檔案,Bazel 會產生合成涵蓋範圍資訊,其中只會提及來源檔案路徑,但不會包含任何內容資訊。

驗證動作

有時您需要驗證建構作業的某些項目,而執行驗證所需的資訊只會出現在構件 (來源檔案或產生的檔案) 中。由於這項資訊位於構件中,規則無法讀取檔案,因此無法在分析時進行這項驗證。動作必須在執行時進行這項驗證。如果驗證失敗,動作就會失敗,建構作業也會失敗。

可能執行的驗證包括靜態分析、Linting、依附元件和一致性檢查,以及樣式檢查。

此外,驗證動作也能將建構構件不需要的動作部分移至個別動作,藉此提升建構效能。舉例來說,如果可將編譯和 Linting 的單一動作分成編譯動作和 Linting 動作,則 Linting 動作可做為驗證動作執行,並與其他動作平行執行。

這些「驗證動作」通常不會產生任何用於建構其他位置的項目,因為這些動作只需要判斷輸入內容。但這會產生問題:如果驗證動作未產生任何用於建構程序其他位置的項目,規則要如何執行動作?過去的做法是讓驗證動作輸出空白檔案,並將該輸出內容人為新增至建構作業中其他重要動作的輸入內容:

這樣做可行,因為 Bazel 一律會在執行編譯動作時執行驗證動作,但這有重大缺點:

  1. 驗證動作位於建構作業的主要路徑中。由於 Bazel 認為執行編譯動作需要空白輸出,因此即使編譯動作會忽略輸入內容,系統仍會先執行驗證動作。這會減少平行處理,並減緩建構速度。

  2. 如果建構中的其他動作可能會取代編譯動作執行,則驗證動作的空白輸出內容也需要新增至這些動作 (例如 java_library 的來源 JAR 輸出內容)。如果稍後新增可能取代編譯動作的新動作,且不小心遺漏空白驗證輸出內容,也會發生這個問題。

如要解決這些問題,請使用驗證輸出群組。

驗證輸出群組

驗證輸出群組是專門用來保存驗證動作中未使用的輸出內容,因此不需要人為新增至其他動作的輸入內容。

這個群組很特別,因為無論 --output_groups 旗標的值為何,以及無論目標的依附方式為何 (例如在指令列上、做為依附元件,或透過目標的隱含輸出),系統一律會要求其輸出內容。請注意,系統仍會套用一般快取和增量:如果驗證動作的輸入內容沒有變更,且驗證動作先前已成功執行,系統就不會執行驗證動作。

使用這個輸出群組時,驗證動作仍須輸出某些檔案,即使是空白檔案也一樣。這可能需要包裝一些通常不會建立輸出的工具,以便建立檔案。

在下列三種情況下,系統不會執行目標的驗證動作:

  • 將目標視為工具時
  • 目標做為隱含依附元件 (例如開頭為「_」的屬性) 時
  • 在執行設定中建構目標時。

我們假設這些目標有各自的建構和測試,可找出任何驗證失敗情形。

使用驗證輸出群組

驗證輸出群組名為「_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。

已淘汰的功能

已淘汰的預先宣告輸出內容

有兩種已淘汰的預先宣告輸出內容使用方式:

  • ruleoutputs 參數會指定輸出屬性名稱與字串範本之間的對應,用於產生預先宣告的輸出標籤。建議使用非預先宣告的輸出內容,並明確將輸出內容新增至 DefaultInfo.files。請使用規則目標的標籤做為輸入內容,供使用輸出的規則使用,而不是預先宣告的輸出內容標籤。

  • 如果是可執行的規則ctx.outputs.executable 是指與規則目標同名的預先宣告可執行輸出內容。建議您明確宣告輸出內容,例如使用 ctx.actions.declare_file(ctx.label.name),並確保產生可執行檔的指令會設定權限,允許執行。明確將可執行檔輸出內容傳遞至 DefaultInfoexecutable 參數。

應避免使用的 Runfiles 功能

ctx.runfilesrunfiles 型別具有複雜的功能集,其中許多功能是為了舊版原因而保留。如要降低複雜度,請參考下列建議做法:

  • 避免使用 ctx.runfilescollect_datacollect_default 模式,這些模式會以令人困惑的方式,在特定硬式編碼的依附元件邊緣隱含地收集執行檔。請改用 ctx.runfilesfilestransitive_files 參數新增檔案,或透過 runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) 合併依附元件中的執行檔。

  • 避免使用 DefaultInfo 建構函式的 data_runfilesdefault_runfiles。請改為指定 DefaultInfo(runfiles = ...)。基於舊版原因,系統會保留「預設」和「資料」執行檔之間的差異。舉例來說,部分規則會將預設輸出內容放在 data_runfiles 中,但不會放在 default_runfiles 中。規則應同時包含預設輸出內容,並從提供 Runfile 的屬性 (通常是 data) 合併 default_runfiles,而非使用 data_runfiles

  • DefaultInfo 擷取 runfiles 時 (通常僅適用於合併目前規則與其依附元件之間的執行檔),請使用 DefaultInfo.default_runfiles而非 DefaultInfo.data_runfiles

從舊版供應商遷移

在過去,Bazel 供應器是 Target 物件上的簡單欄位。這些欄位是透過點運算子存取,並透過將欄位放在規則實作函式傳回的 struct 中建立,而非放在供應商物件清單中:

return struct(example_info = struct(headers = depset(...)))

這類供應商可從 Target 物件的對應欄位擷取:

transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]

這個樣式已淘汰,不應在新程式碼中使用;請參閱下文,瞭解有助於遷移的資訊。新的供應商機制可避免名稱衝突。此外,這個服務也支援資料隱藏功能,任何存取供應商例項的程式碼都必須使用供應商符號擷取例項。

目前仍支援舊版供應商,規則可以同時傳回舊版和新版供應商,如下所示:

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.xdep[MyInfo].y

除了 providers 之外,傳回的結構體也可以採用多個具有特殊意義的其他欄位 (因此不會建立對應的舊版供應商):

  • filesrunfilesdata_runfilesdefault_runfilesexecutable 欄位會對應至 DefaultInfo 的同名欄位。同時傳回 DefaultInfo 提供者時,不得指定任何這些欄位。

  • output_groups 欄位會採用結構體值,並對應至 OutputGroupInfo

在規則的 provides 宣告中,以及在依附屬性的 providers 宣告中,舊版供應器會以字串形式傳遞,新版供應器則會以 Info 符號傳遞。遷移時,請務必將字串改為符號。如果規則集複雜或龐大,難以原子方式更新所有規則,建議按照下列步驟操作,可能會比較容易:

  1. 使用上述語法,修改產生舊版供應商的規則,同時產生舊版和新版供應商。如果規則宣告會傳回舊版供應商,請更新該宣告,同時納入舊版和新版供應商。

  2. 修改使用舊版供應商的規則,改為使用新版供應商。如有任何屬性聲明需要舊版供應商,請一併更新為需要新版供應商。您也可以視需要交錯執行這項工作和步驟 1,讓消費者接受或要求任一供應商:使用 hasattr(target, 'foo') 測試舊版供應商是否存在,或使用 FooInfo in target 測試新供應商是否存在。

  3. 從所有規則中完全移除舊版供應商。