規則

回報問題 查看來源

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

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

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

建立或修改任何規則前,請務必熟悉 Bazel 的建構階段。請務必瞭解建構的三個階段,分別是載入、分析和執行。如要瞭解規則和巨集之間的差異,也很適合瞭解巨集。如要開始使用,請先詳閱規則教學課程。 然後參考本頁內容。

Bazel 本身內建了一些規則,這些原生規則 (例如 cc_libraryjava_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 檔案以將目標例項化時,才能在載入階段呼叫。

屬性

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

規則專屬的屬性 (例如 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]

針對從目標的實作函式 (而非供應器物件清單) 傳回的舊版樣式:struct

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

您可以從 Target 物件的對應欄位擷取提供者:

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

強烈建議不要採用這個樣式,因此規則應移出此樣式

Files

檔案是以 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 物件。

動作

動作說明如何從一組輸入產生一組輸出,例如「在 hello.c 上執行 gcc 並取得 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],
    )
    ...

動作會使用輸入檔案清單或解壓縮,並產生 (非空白) 輸出檔案清單。您必須在分析階段中得知輸入和輸出檔案的組合。它可能取決於屬性的值,包括來自依附元件的提供者,但不能取決於執行結果。舉例來說,如果您的動作會執行 unzip 指令,就必須指定預計加載的檔案 (執行解壓縮前)。如果在內部建立可變數檔案的動作,即可將這些檔案納入單一檔案 (例如 zip、tar 或其他封存格式)。

動作必須列出所有輸入內容。未使用的商店資訊輸入內容會 被允許,但效率不佳。

動作必須建立所有輸出內容。這些檔案可能會寫入其他檔案,但任何未包含在輸出內容中的項目都無法存取。所有宣告的輸出內容都必須透過某些動作寫入。

動作與純函式非常相似,這些動作應只依附於提供的輸入內容,並避免存取電腦資訊、使用者名稱、時鐘、網路或 I/O 裝置 (讀取輸入和寫入輸出除外)。由於系統會快取並重複使用輸出內容,因此這一點非常重要。

Bazel 會解析依附元件,而 Bazel 會決定要執行哪些動作。如果依附元件圖表中有循環,則視為錯誤。建立動作並不保證會執行該動作,具體取決於建構是否需要輸出。

供應商外掛程式

提供者是規則對依附的其他規則公開的資訊。這類資料可能包含輸出檔案、程式庫、傳遞工具指令列的參數,或是目標使用者應瞭解的任何其他資訊。

由於規則的實作函式只能從執行個體化目標的立即依附元件讀取提供者,因此規則必須從目標的依附元件轉送任何資訊,而目標取用者需要知道的資訊,一般做法是將該資訊累積至 depset

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

舊版實作函式也可以舊版樣式編寫,其中實作函式會傳回 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 會預設為所有已預先宣告的輸出內容 (一般是由輸出屬性建立的輸出內容)。

即使不應直接使用這些輸出,執行動作的規則應提供預設輸出內容。不在要求的輸出圖表中的動作會縮減。如果只有目標使用者使用輸出,則在獨立建構目標時,系統不會執行這些動作。這可使偵錯更加困難,因為僅重新建構失敗的目標並不會重現失敗。

執行檔案

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

您可以在建立規則時手動新增執行檔案。可由規則內容 ctx.runfilesrunfiles 方法建立 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

# 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 的呼叫中分別將 executabletest 引數設為 True

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

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

測試規則的名稱必須以 _test 結尾。(測試 target 名稱通常也會在 _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)

Runfiles 目錄底下的 File 路徑會對應至 File.short_path

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

進階主題

要求輸出檔案

一個目標可以有多個輸出檔案。執行 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 過去並未就執行平台的概念,而是將所有建構動作視為在主機上執行。Bazel 6.0 之前的版本建立了專屬的「主機」設定來代表這一點。如果您在程式碼或舊版說明文件中看到「host」一詞,這就是所謂的「主機」。建議您使用 Bazel 6.0 以上版本,以免發生額外的概念負擔。

設定片段

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

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

一般來說,執行檔案樹狀結構中檔案的相對路徑,與來源樹狀結構或產生的輸出樹狀結構中檔案的相對路徑相同。如果因為某些原因而需要不同,您可以指定 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

規則也應針對使用 coverage_common.instrumented_files_info 建構的 InstrumentedFilesInfo 提供者提供與涵蓋範圍相關的屬性資訊。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 設為 "host""exec") 建立預設值。(這不是理想的行為,因為此屬性會將 srcs 這類屬性放在 dependency_attributes (而非 source_attributes) 中,但可避免依附元件鏈中所有規則的明確涵蓋範圍設定)。

驗證動作

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

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

「驗證動作」也有助於將建構構件所需的部分動作移到不同動作中,藉此改善建構效能。舉例來說,如果用於編譯和程式碼檢查的單一動作可分為編譯動作和程式碼檢查動作,則該程式碼可以做為驗證動作執行,並與其他動作同時執行。

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

這是有效的做法,因為 Bazel 一律會在執行編譯動作時執行驗證動作,但這有明顯的缺點:

  1. 驗證動作位於建構的關鍵路徑。由於 Bazel 認為執行編譯動作需要空白的輸出內容,因此仍會先執行驗證動作,即使編譯動作會忽略輸入內容也一樣。這樣可以減少平行處理任務數量,並拖慢建構速度。

  2. 如果建構作業中的其他動作可能會執行,而非編譯動作,那麼這些動作的空白驗證動作也必須加入這些動作 (例如 java_library 的來源 jar 輸出內容)。如果之後新增原本可能執行 (而非編譯動作) 的新動作,且不小心漏掉空白的驗證輸出內容,也算是問題。

這些問題的解決方法是使用 Validations Output Group。

驗證輸出群組

「Validations Output Group」(驗證輸出群組) 是輸出群組,專門保存驗證動作中其他未使用的輸出,因此無需手動將這些輸出加入其他動作的輸入內容。

這個群組是特別的,無論 --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_runfiles,規則應「同時」包含預設輸出內容,並透過提供執行檔案的屬性 (通常是 data) 合併 default_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.xdep[MyInfo].y

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

  • filesrunfilesdata_runfilesdefault_runfilesexecutable 欄位會對應至 DefaultInfo 的相同名稱欄位。但無法指定任何這些欄位,同時傳回 DefaultInfo 提供者。

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

在規則的 provides 宣告和依附元件屬性的 providers 宣告中,舊版供應器會以字串的形式傳遞,而新型供應器則會以其 *Info 符號傳入。遷移時,請務必從字串變更為符號。對於較複雜或大型的規則集,難免難以統一更新所有規則,只要按照以下步驟順序操作,就能更輕鬆完成更新:

  1. 使用上述語法修改產生舊版提供者以產生舊版提供者的規則。如果是宣告傳回舊版提供者的規則,請更新該宣告,納入舊版和新型提供者。

  2. 修改使用舊版提供者改為使用現代化提供者的規則。如果任何屬性宣告需要舊版提供者,您也應該更新這些宣告,改為要求新式提供者。您也可以視需要讓取用者接受/要求供應商:使用 hasattr(target, 'foo') 測試舊版供應器是否存在,或使用 FooInfo in target 測試新供應商是否存在,藉此在步驟 1 與步驟 1 交錯。

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