規則

規則會定義 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 參數。可執行規則的可執行輸出內容會隱含地新增至執行檔案。

部分規則會指定屬性 (通常命名為 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 具有「設定」和轉換的概念。最上層目標 (在指令列中要求的目標) 會建構在「目標」設定中,而應在執行平台執行的工具則會建構在「執行」設定中。規則可能會根據設定產生不同的動作,例如變更傳遞至編譯器的 CPU 架構。在某些情況下,不同設定可能需要使用同一個程式庫。如果發生這種情況,系統會分析並多次建構。

根據預設,Bazel 會在與目標本身相同的設定中建構目標的依附元件,也就是不使用轉換。如果依附元件是建構目標時需要的工具,對應的屬性應指定轉換至執行設定。這會導致工具及其所有依附元件建構執行平台。

針對每個依附元件屬性,您可以使用 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 是將路徑對應至檔案的字典,路徑相對於 Runfile 目錄的根目錄。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 標記的值為何,以及無論目標的依附方式為何 (例如在指令列上、做為依附元件,或透過目標的隱含輸出內容),系統一律會要求其輸出內容。請注意,系統仍會照常快取資料並計算增量:如果驗證動作的輸入內容沒有變更,且驗證動作先前已成功執行,系統就不會再次執行驗證動作。

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

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

  • 將目標視為工具時
  • 目標做為隱含依附元件時 (例如開頭為「_」的屬性)
  • 在 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。

已淘汰的功能

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

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

  • 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 欄位會採用 struct 值,並對應至 OutputGroupInfo

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

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

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

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