規則

回報問題 查看原始碼

「規則」定義了 Bazel 在輸入中執行的一系列「動作」,以產生一組輸出內容,這些輸出內容會在規則的實作函式傳回的提供者中參照。例如,C++ 二進位檔規則可能:

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

從 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 檔案中指定。由於使用者大多不想指定規則使用的工具,因此隱式依附元件適合用於硬式編碼在規則與工具 (一種建構時間依附元件,例如編譯器) 間的關係。在規則的實作函式中,這項作業與其他依附元件相同。

如果您想提供隱含依附元件,但不想讓使用者覆寫該值,可以為其提供以底線開頭 (_) 的名稱,將屬性設為 private。私人屬性必須具備預設值。一般來說,使用不公開屬性處理隱含的依附元件才有意義。

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) 的欄位。

宣告輸出

在分析階段,規則的實作函式可以建立輸出內容。由於所有標籤都必須在載入階段已知,因此這些額外輸出內容沒有標籤。您可以使用 ctx.actions.declare_filectx.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.c on hello.c and get hello.o」建立動作後,Bazel 不會立即執行指令,因為動作可能會依據另一個動作的輸出內容,而以依附元件圖表註冊此項目。例如,在 C 中,您必須在編譯器之後呼叫連結器。

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

ctx.actions.args 可用來有效率地累積動作引數。可避免在執行時間保持分割畫面:

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 中。

目標的提供者由實作函式傳回的提供者物件清單指定。

舊的實作函式也可以以舊版樣式編寫,實作函式會傳回 struct,而不是提供者物件清單。強烈建議不要使用這個樣式,規則應移出這個樣式

預設輸出內容

目標的預設輸出是透過指令列要求建構時預設要求的輸出。舉例來說,java_library 目標 //pkg:foofoo.jar 做為預設輸出,因此將透過 bazel build //pkg:foo 指令建構。

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

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

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

執行動作的規則應提供預設輸出內容,即使這些輸出內容不應直接使用也一樣。不在要求的輸出圖表中的動作會經過修剪。如果輸出僅由目標的消費者使用,則當目標在獨立建構時,系統不會執行這些動作。這會使偵錯變得更困難,因為僅重新建構失敗的目標不會重現失敗。

執行檔案

執行檔是目標在執行階段 (而非建構時間) 使用的一組檔案。在執行階段期間,Bazel 會建立目錄樹狀結構,其中包含指向執行檔案的符號連結。此階段會為二進位檔暫存環境,讓它可以在執行階段存取執行檔案。

您可以在建立規則時手動新增執行檔案。在規則結構定義的 ctx.runfiles 上,可使用 runfiles 方法建立 runfiles 物件,並傳遞至 DefaultInfo 上的 runfiles 參數。可執行規則的執行檔會以隱含方式加入執行檔案。

有些規則會指定屬性 (通常名為 data),其輸出內容會新增至目標的執行檔案。執行檔案也應從 data 和任何可能會提供最終執行程式碼的屬性合併,通常是 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)。也會間接新增至「執行檔案」

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

執行檔位置

當執行檔目標使用 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)

執行檔目錄下 File 的路徑與 File.short_path 對應。

bazel 直接執行的二進位檔會鄰近 runfiles 目錄的根目錄。不過,執行檔案「from」的二進位檔無法將假設做出相同的假設。為減緩此問題,每個二進位檔都應提供一種方式,讓其接受使用環境、指令列引數或標記將執行檔案根目錄當做參數。如此一來,二進位檔就能將正確的標準執行檔案根目錄傳遞給其呼叫的二進位檔。如果未設定,二進位檔可以猜出這是第一個呼叫的二進位檔,並尋找相鄰的執行檔案目錄。

進階主題

要求輸出檔案

單一目標可以有多個輸出檔案。執行 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 具有「設定」和「轉換」的概念。頂層目標 (指令列要求的目標) 為內建「目標」設定,而應在執行平台上執行的工具是內建「exec」設定。規則可能會依據設定產生不同的動作,例如變更傳遞至編譯器的 CPU 架構。在某些情況下,不同的設定可能需要相同的程式庫。發生這種情況時,系統會多次分析,並可能多次建構此資料。

根據預設,Bazel 會採用與目標本身相同的設定來建構目標的依附元件,意即無需轉換。如果依附元件是協助建構目標所需的工具,對應的屬性應指定執行設定的轉換。這會導致工具及其所有依附元件為執行平台建構。

針對各個依附元件屬性,您可以使用 cfg 決定依附元件應使用相同的設定來建構,或是轉換至執行設定。如果依附元件屬性具有 executable = True 標記,則 cfg 必須明確設定。這是為了避免不小心建構了錯誤設定的工具。查看範例

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

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

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

cfg = "target" 實際上不會執行任何動作,這只是便利的值,可協助規則設計人員明確瞭解其意圖。如果 executable = False 表示 cfg 為選用,請只在確實有助可讀性時設定此屬性。

您也可以使用 cfg = my_transition 來利用使用者定義的轉場效果,讓規則作者在變更設定時能享有極大彈性,同時還能放大建構圖並提升理解度

注意:以往 Bazel 沒有執行執行平台的概念,而是將所有建構動作視為在主體機器上執行。6.0 以下版本的 Bazel 版本就建立了不同的「主機」設定來代表這項設定。如果您在程式碼或舊版說明文件中看到「host」的參照,這就是意思。建議您使用 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 是檔案的字典對應路徑,其中路徑是相對於執行檔案目錄的根目錄。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.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_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,系統就會在 dependency_attributes 中,透過每項未將 cfg 設為 "exec" 的非工具依附元件屬性建立預設值。(這不是理想行為,因為會將 srcs 這類屬性置於 dependency_attributes 而非 source_attributes 中,但這樣就不必為依附元件鏈結中的所有規則設定明確的涵蓋範圍設定)。

驗證動作

有時您需要驗證建構作業的相關資訊,而進行驗證所需的資訊僅適用於成果 (來源檔案或產生的檔案)。由於這項資訊位於構件中,因此規則無法在分析期間進行這項驗證,因為規則無法讀取檔案。相反地,動作必須在執行時間執行這項驗證。驗證失敗時,動作會失敗,進而導致建構作業。

例如靜態分析、程式碼檢查、依附元件和一致性檢查,以及樣式檢查。

驗證動作也可將建構構件時不需要的動作部分移至不同動作,藉此改善建構效能。舉例來說,如果將編譯和程式碼檢查的單一動作可分離成一個編譯動作和程式碼檢查動作,那麼此檢查動作可以做為驗證動作執行,並與其他動作一起執行。

由於這些「驗證動作」只需要斷言輸入內容的內容,因此通常不會產生用於建構作業的其他項目。但這會造成問題:如果驗證動作並未產生在建構作業的其他位置使用的任何項目,規則要如何執行動作?以往的方法是將驗證動作輸出空白檔案,然後以人工方式將輸出內容新增至建構作業中其他重要動作的輸入內容:

這項做法很有效,因為 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 參數。

執行檔案功能

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。規則應「同時」加入預設輸出內容,並從提供執行檔案的屬性 (通常為 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. 將舊版供應商從所有規則中完全移除。