規則

回報問題 查看原始碼 Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

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

從 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輸出內容 (使用 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]

針對舊版樣式,其中 struct 是從目標的實作函式傳回,而非提供者物件清單:

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

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

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

我們強烈建議您不要使用這種樣式,並應將規則從這種樣式中移除

檔案

檔案會以 File 物件表示。由於 Bazel 不會在分析階段執行檔案 I/O,因此這些物件無法用於直接讀取或寫入檔案內容。相反地,會將這些值傳遞至動作發布函式 (請參閱 ctx.actions),以建構動作圖表的部分。

File 可以是來源檔案或產生的檔案。每個產生的檔案都必須是單一動作的輸出內容。來源檔案不得是任何動作的輸出內容。

對於每個依附元件屬性,ctx.files 的對應欄位會列出透過該屬性,所有依附元件的預設輸出內容:

def _example_library_impl(ctx):
    ...
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    ...

ctx.file 包含單一 FileNone,用於依附元件屬性,其規格設定為 allow_single_file=Truectx.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 可用於有效累積動作的引數。這可避免在執行時間之前扁平化 depset:

def _example_library_impl(ctx):
    ...

    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    inputs = depset(srcs, transitive=[headers])
    output_file = ctx.actions.declare_file(ctx.label.name + ".output")

    args = ctx.actions.args()
    args.add_joined("-h", headers, join_with=",")
    args.add_joined("-s", srcs, join_with=",")
    args.add("-o", output_file)

    ctx.actions.run(
        mnemonic = "ExampleCompile",
        executable = ctx.executable._compiler,
        arguments = [args],
        inputs = inputs,
        outputs = [output_file],
    )
    ...

動作會採用輸入檔案的清單或 depset,並產生 (非空白) 輸出檔案清單。在分析階段中,必須知道輸入和輸出檔案的組合。它可能會依屬性值而定,包括依附元件的提供者,但不能依執行結果而定。舉例來說,如果動作執行解壓縮指令,您必須指定要膨脹哪些檔案 (在執行解壓縮指令之前)。在內部建立不同數量檔案的動作,可將這些檔案包裝在單一檔案中 (例如 ZIP、tar 或其他封存檔格式)。

動作必須列出所有輸入內容。您可以列出未使用的輸入內容,但效率不佳。

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

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

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 會建立目錄樹狀結構,其中包含指向執行檔案的符號連結。這會為二進位檔設定環境,讓二進位檔在執行期間存取執行檔。

您可以在建立規則時手動新增執行檔。runfiles 物件可透過規則結構定義 ctx.runfiles 上的 runfiles 方法建立,並傳遞至 DefaultInfo 上的 runfiles 參數。可執行規則的可執行輸出內容會隱含新增至執行檔。

部分規則會指定屬性,通常會命名為 data,其輸出內容會加入至目標的執行檔。執行檔也應從 data 合併,以及從任何可能提供最終執行程式碼的屬性合併,通常是 srcs (可能包含與 data 相關聯的 filegroup 目標) 和 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 結尾。(依慣例,測試目標名稱通常會以 _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(
 ...
)

執行檔案位置

使用 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 目錄的根目錄旁邊。不過,從執行檔呼叫的二進位檔無法做出相同假設。為避免這種情況,每個二進位檔都應提供一種方法,可使用環境或指令列引數/標記,將其執行檔根目錄做為參數。這可讓二進位檔將正確的標準執行檔根目錄傳遞至所呼叫的二進位檔。如果未設定,二進位檔可以推測這是第一個呼叫的二進位檔,並尋找相鄰的 runfiles 目錄。

進階主題

要求輸出檔案

單一目標可以有幾個輸出檔案。執行 bazel build 指令時,系統會將指令指定的目標輸出內容視為要求。Bazel 只會建構這些要求的檔案,以及這些檔案直接或間接依附的檔案。(就動作圖表而言,Bazel 只會執行可做為要求檔案間接依附元件的動作)。

除了預設輸出內容之外,您也可以在指令列上明確要求任何預先宣告的輸出內容。規則可以透過輸出屬性指定預先宣告的輸出內容。在這種情況下,使用者在例項化規則時,會明確選擇輸出的標籤。如要取得輸出屬性的 File 物件,請使用 ctx.outputs 的對應屬性。規則也可以根據目標名稱隱含定義預先宣告的輸出內容,但這項功能已淘汰。

除了預設輸出內容外,還有輸出群組,也就是一組可一起要求的輸出檔案。您可以使用 --output_groups 要求這些資訊。舉例來說,如果目標 //pkg:mytarget 是具有 debug_files 輸出群組的規則類型,則可透過執行 bazel build //pkg:mytarget --output_groups=debug_files 建構這些檔案。由於未預先宣告的輸出項目沒有標籤,因此您只能要求預設輸出項目或輸出群組顯示這些項目。

您可以使用 OutputGroupInfo 供應器指定輸出群組。請注意,與許多內建供應器不同,OutputGroupInfo 可使用任意名稱的參數,定義具有該名稱的輸出群組:

def _example_library_impl(ctx):
    ...
    debug_file = ctx.actions.declare_file(name + ".pdb")
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        OutputGroupInfo(
            debug_files = depset([debug_file]),
            all_files = depset([output_file, debug_file]),
        ),
        ...
    ]

與大多數供應商不同的是,OutputGroupInfo 可以由面向和套用該面向的規則目標一併傳回,只要兩者不定義相同的輸出群組即可。在這種情況下,系統會合併產生的供應者。

請注意,OutputGroupInfo 通常不應用於將特定類型的檔案從目標傳送至其使用者的動作。請改為定義規則專屬供應器

設定

假設您想為不同的架構建構 C++ 二進位檔。建構作業可能相當複雜,並涉及多個步驟。部分中繼二進位檔 (例如編譯器和程式碼產生器) 必須在執行平台上執行 (可能是主機或遠端執行程式)。某些二進位檔 (例如最終輸出內容) 必須針對目標架構建構。

因此,Bazel 採用「設定」和轉換的概念。最上層的目標 (在指令列上要求的目標) 會在「target」設定中建構,而應在執行平台上執行的工具則會在「exec」設定中建構。規則可能會根據設定產生不同的動作,例如變更傳遞至編譯器的 CPU 架構。在某些情況下,不同設定可能需要相同的程式庫。發生這種情況時,系統會分析並可能多次建構該項目。

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

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

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

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

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

cfg="target" 實際上並不會執行任何操作,它只是方便規則設計人員明確表達意圖的值。當 executable=Falsecfg 時,表示 cfg 為選用項目,請只在確實有助於提升可讀性時才設定。

您也可以使用 cfg=my_transition 使用使用者定義的轉場效果,讓規則作者在變更設定時擁有極大的彈性,但缺點是會使建構圖表變大,且不易理解

注意:過去 Bazel 並沒有執行平台的概念,而是將所有建構動作視為在主機上執行。因此,您會看到單一「主機」設定,以及可用於在主機設定中建立依附元件的「主機」轉換。許多規則仍會為工具使用「host」轉換,但這項功能目前已淘汰,並正在遷移至盡可能使用「exec」轉換。

「主機」和「執行」設定之間有許多差異:

  • 「host」是終端機,「exec」則不是:一旦依附元件位於「host」設定中,就無法再進行轉換。在「執行」設定中,您可以繼續進行進一步的設定轉換。
  • 「host」是單一層級,「exec」則不是:只有一個「host」設定,但每個執行平台可能會有不同的「exec」設定。
  • 「主機」假設您會在與 Bazel 相同的電腦上執行工具,或在非常相似的電腦上執行。這不再是事實:您可以在本機或遠端執行緒上執行建構動作,而且無法保證遠端執行緒的 CPU 和 OS 與本機相同。

「exec」和「host」設定都會套用相同的選項變更 (例如,從 --host_compilation_mode 設定 --compilation_mode,從 --host_cpu 設定 --cpu 等等)。差異在於「主機」設定會從所有其他旗標的預設值開始,而「執行」設定會根據目標設定,從旗標的目前值開始。

設定片段

規則可以存取 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
    ...
)

ctx.fragments 只會為目標設定提供設定片段。如果您想存取主機設定的片段,請改用 ctx.host_fragments

通常,執行檔案樹狀目錄中檔案的相對路徑,會與來源樹狀目錄或產生的輸出樹狀目錄中該檔案的相對路徑相同。如果這些值因某些原因而需要不同,您可以指定 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,請務必避免將兩個不同的檔案對應至 runfiles 樹狀結構中的相同路徑。這會導致建構作業失敗,並顯示描述衝突的錯誤。如要修正這個問題,您必須修改 ctx.runfiles 引數,以移除衝突。系統會針對使用規則的任何目標,以及依賴這些目標的任何類型目標執行這項檢查。如果工具可能會間接地由其他工具使用,這種做法就特別危險。在工具和所有相依項目的執行檔中,符號連結名稱必須是唯一的。

程式碼涵蓋率

執行 coverage 指令時,建構作業可能需要為特定目標新增涵蓋率檢測工具。建構作業也會收集已檢測的來源檔案清單。系統會根據 --instrumentation_filter 標記控管要考量的目標子集。除非指定 --instrument_test_targets,否則會排除測試目標。

如果規則實作在建構期間新增涵蓋率檢測功能,就必須在實作函式中考量這項因素。如果目標的來源應進行檢測,ctx.coverage_instrumented 會在涵蓋率模式中傳回 true:

# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
  # Do something to turn on coverage for this compile action

在涵蓋率模式中一律需要啟用的邏輯 (無論目標來源是否已具體檢測),可視 ctx.configuration.coverage_enabled 而定。

如果規則在編譯前直接納入其依附元件的來源 (例如標頭檔案),則如果依附元件的來源需要檢測,規則可能也需要啟用編譯時間檢測:

# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
    (ctx.coverage_instrumented() or
     any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
    # Do something to turn on coverage for this compile action

規則也應提供資訊,說明哪些屬性與 InstrumentedFilesInfo 供應商的涵蓋率相關,並使用 coverage_common.instrumented_files_info 建構。instrumented_files_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") 建立預設 InstrumentedFilesInfo。(這不是理想的行為,因為它會將 srcs 等屬性放入 dependency_attributes 而非 source_attributes,但可避免為依附元件鏈結中的所有規則設定明確的涵蓋率設定)。

驗證動作

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

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

驗證動作也可以將建構成果中不需要的部分動作移至個別動作,進而改善建構效能。舉例來說,如果執行編譯和 lint 的單一動作可分為編譯動作和 lint 動作,則 lint 動作可做為驗證動作執行,並與其他動作並行執行。

這些「驗證動作」通常不會產生任何在建構中其他位置使用的內容,因為它們只需要斷言輸入內容。不過,這會導致問題:如果驗證動作不會產生任何可在建構中其他位置使用的內容,規則如何取得要執行的動作?以往的做法是讓驗證動作輸出空白檔案,然後人為地將該輸出內容新增至建構中其他重要動作的輸入內容:

這麼做是可行的,因為 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。規則應同時包含預設輸出內容,並從提供執行檔的屬性 (通常是 data) 合併 default_runfiles。請勿使用 data_runfiles

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

從舊版供應器遷移

以往,Bazel 供應工具是 Target 物件上的簡單欄位。這些欄位是使用點運算子存取,並將欄位放入規則實作函式傳回的結構體中建立。

這類樣式已淘汰,不應在新的程式碼中使用;請參閱下方資訊,瞭解如何進行遷移。新的供應器機制可避免名稱衝突。它也支援資料隱藏功能,要求任何存取提供者執行個體的程式碼,都必須使用提供者符號擷取資料。

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

def _old_rule_impl(ctx):
  ...
  legacy_data = struct(x="foo", ...)
  modern_data = MyInfo(y="bar", ...)
  # When any legacy providers are returned, the top-level returned value is a
  # struct.
  return struct(
      # One key = value entry for each legacy provider.
      legacy_info = legacy_data,
      ...
      # Additional modern providers:
      providers = [modern_data, ...])

如果 dep 是此規則例項的產生 Target 物件,則可將供應器及其內容擷取為 dep.legacy_info.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. 從所有規則中完全移除舊版供應器。