Bazel 專案版面配置和機構

Bazel 專案

Bazel 專案是一組來源和建構檔案,用於描述:

  • 如何建構二進位檔或資料檔案等構件,以及相關依附元件。
  • 如何run特定指令,例如指令碼或執行檔。
  • 如何測試上述建構構件。

Bazel 專案會由頂層目錄具體化,此目錄的內容遵循特定的版面配置慣例

Bazel 工作區

Bazel 工作區是包含頂層 WORKSPACE.bazel1 檔案的目錄樹狀結構。這會定義一組來源和相關建構檔案的根目錄。Bazel 專案可使用數個工作區:

  • 專案的根目錄稱為 工作區,因此必須包含 WORKSPACE.bazel 檔案。

  • 專案也可以參照「外部工作區」,這些工作區會對應到第三方專案依附元件

範例如下:

/home/user/project/
    WORKSPACE.bazel
    src/
        BUILD.bazel
        extra/
            extra.cc
        lib/
            BUILD.bazel
            foo.cc
            foo.h
        main.cc

/home/user/project 目錄是根 Bazel 工作區,內有其中所有檔案。

WORKSPACE.bazel 檔案可以空白,但也可以包含參照其他工作區的指令,如後文所述。

Bazel 套件

在工作區中,含有 BUILD.bazel2 檔案的目錄會定義「套件」,這是 Bazel 已知來源檔案和項目集合的邊界。

例如,下列檔案版面配置:

/home/user/project/
  WORKSPACE.bazel
  BUILD.bazel
  main.cc

定義位於 /home/user/project 的根工作區,具有單一頂層套件,其中包含 BUILD.bazelmain.cc 檔案。

BUILD.bazel 檔案也可以包含用於定義具名項目的指令,例如目標、設定條件等,嚴格來說同樣屬於套件。

單一工作區中可有多個套件,且每個檔案只能屬於一個套件。以下列檔案版面配置為例:

/home/user/project/
  WORKSPACE.bazel
  BUILD.bazel
  main.cc
  lib/
    BUILD.bazel
    foo.cc

根工作區包含兩個不同的套件:

  • 頂層套件,仍包含檔案 BUILD.bazelmain.cc (相對於根目錄目錄)。

  • 這是第二個套件,包含 lib/BUILD.bazellib/foo.cc 檔案。

請注意,/home/user/project/lib/foo.cc 的檔案只屬於第二個套件,不屬於第一個套件。這是因為套件 邊界「永不」重疊

BUILD.bazelBUILD

Bazel 出自 Google 的 Blaze,該 Blaze 只能在 Linux 上區分大小寫的檔案系統中運作。Blaze 只使用 BUILD 檔案名稱儲存建構指令。

不過,Bazel 也必須在具有不區分大小寫檔案系統的 Windows 和 MacOS 上執行,而且許多 Google 專案或非 Google 專案都已使用名為「build」的目錄,然後在這類系統上與名為「BUILD」的檔案衝突。

為解決這個問題,Bazel 使用 BUILD.bazelWORKSPACE.bazel 做為預設檔案名稱,同時支援 BUILDWORKSPACE 做為備用檔案名稱。

Workspace 指令

WORKSPACE.bazel 可以是空白的,或者包含參照其他 Bazel 工作區的指令 (稱為「外部存放區」)。這些指令一律會為存放區命名。例如:

local_repository(
  name = "my_ssl",
  path = "/home/user/src/openssl-bazel",
)

將名稱 my_ssl 與位於建構機器上 /home/user/src/openssl-bazel 的工作區建立關聯。這個目錄也必須包含 WORKSPACE.bazel (或 WORKSPACE) 檔案。

存放區只是具有名稱的外部 Bazel 工作區。這個名稱是專案本機的名稱,之後可用於參照外部工作區的項目 (請見下文)。

Bazel 也支援其他指令,可從網路下載存放區,或甚至以程式輔助方式產生存放區的內容。

Bazel 標籤

Bazel 標籤是 BUILD.bazel 檔案中定義的來源檔案和項目的字串參照。一般格式為:

@<repository_name>//<package_name>:<target_name>

在此情況下:

  • @<repository_name>// 會指定已命名 Bazel 工作區的目錄。

    為了方便起見,這可以簡單縮寫為目前工作區 (包含目前 BUILD.bazel 檔案的工作區) 的 // 來縮寫。另請注意,@// 用於指定專案的根工作區,即使在外部存放區中使用亦然。

  • <package_name> 是套件的目錄路徑 (相對於工作區目錄)。例如,在 //src:main.cc//src/lib:foo 標籤中,套件名稱分別為 srcsrc/lib

    這可以空白,例如 //:BUILD.bazel 會指向目前工作區頂層目錄中的建構檔案。

  • 就來源檔案而言,<target_name> 是與其父項套件目錄相對的檔案路徑,可能包含子目錄部分。例如 //src:main.cc//src:extra/extra.cc 的目標名稱為 main.ccextra/extra.cc

  • 如果是其他項目,<target_name> 會對應 BUILD.bazel 檔案中定義的項目 (建構構件、建構設定、設定條件等)。

    依照慣例,其 name 屬性不應包含目錄分隔符,不過在極少數情況下,為了避免與來源產生混淆。

    如果開發人員來自其他建構系統,而這些系統區分了建構圖中的項目類型 (例如 GN 使用「目標」、「設定」、「工具鍊」和「集區」來指定不同事物),這可能會讓使用者感到困惑。

系統也支援標籤的縮短運算式:

  • 如果標籤以存放區名稱開頭且不含冒號,則代表套件路徑,並指向具有相同名稱的項目。舉例來說,//src/foo 相當於 //src/foo:foo

  • 如果標籤以冒號開頭,則為相對於目前套件的名稱。舉例來說,在 src/foo/BUILD.bazel 中顯示的「:bar」和「:extra/bar.cc」等於 //src/foo:bar//src/foo:extra/bar.cc

  • 如果標籤沒有存放區名稱且沒有半形冒號,則該標籤一律會是與目前套件相關的名稱,即使其中包含目錄分隔符也一樣。例如,src/foo/BUILD.bazel 中的「bar/bar.cc」一律是指 //src/foo:bar/bar.cc

    請注意,這與 //src/foo/bar:bar.cc 不同

相對標籤和套件擁有權

由於每個來源檔案只能屬於單一套件,因此相對標籤可能無效。例如,在如下所示的專案中:

/home/user/project/
    WORKSPACE.bazel
    src/
        BUILD.bazel
        main.cc
        extra/
            extra.cc
        lib/
            BUILD.bazel
            foo.cc
            foo.h

foo.cc 檔案屬於 src/lib 套件,因此其標籤必須為 //src/lib:foo.cc

src/BUILD.bazel 中使用 src:lib/foo.cc 之類的標籤會導致錯誤:

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "lib/foo.cc",       # Error: Label '//src:lib/foo.cc' is invalid because 'src/lib' is a subpackage
    "lib/foo.h"         # Error: Label '//src:lib/foo.h' is invalid because 'src/lib' is a subpackage
    "main.cc",
  ],
)

其他套件的來源檔案存取權

根據預設,指定套件的來源檔案無法從其他套件存取,而且相對套件標籤無效,如下所示:

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "lib:foo.cc",    # Error: invalid label 'lib:foo.cc': absolute label must begin with '@' or '//'
    "lib:foo.h"      # Error: invalid label 'lib:foo.h': absolute label must begin with '@' or '//'
    "main.cc",
  ],
)

即使使用正確的絕對標籤,仍會發生以下錯誤:

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "//src/lib:foo.cc",    # Error: no such target '//src/lib:foo.cc': target 'foo.h' not declared in package 'src/lib'
    "//src/lib:foo.h"      # Error: no such target '//src/lib:foo.h': target 'foo.h' not declared in package 'src/lib'
    "main.cc",
  ],
)

export_files() 可授予跨套件邊界的檔案直接存取權:

# From src/lib/BUILD.bazel
export_files([
  "foo.cc" ,
  "foo.h" ,
])

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "//src/lib:foo.cc",    # OK
    "//src/lib:foo.h"      # OK
    "main.cc",
  ],
)

指定來自其他套件的存取權

BUILD.bazel 檔案中定義的已加標籤項目不需要匯出,但其 visibility 屬性必須允許在其套件外使用這些項目:

# From src/lib/BUILD.bazel
cc_library(
  name = " lib" ,
  srcs = [ " foo.cc"  ],
  hdrs = [ " foo.h"  ],
  visibility = [ " //visibility:public" ],  # Anyone can reference this directly!
)

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "main.cc",
  ],
  deps = [ "lib" ],   # OK!
)

根據預設,只有同一套件中的其他商品可以查看項目。您可以使用 package() 指令變更套件中定義的所有項目的預設瀏覽權限,藉此變更這項設定:

# From src/lib/BUILD.bazel

# Ensure that all items defined in this file are visible to anyone
package(default_visibility = ["//visibility:public"])

cc_library(
  name = " lib" ,
  srcs = [ "foo.cc" ],
  hdrs = [ "foo.h" ],
)

# From src/BUILD.bazel
cc_binary(
  name = "program",
  srcs = [
    "extra/extra.cc",
    "main.cc",
  ],
  deps = [ "lib" ],   # OK!
)

虛擬套件的警告

請避免在專案中使用以下名稱建立頂層目錄:

  • conditions
  • command_line_option
  • external
  • visibility

因為 Bazel 在 BUILD.bazel 檔案的標籤中使用許多硬式編碼的「虛擬套件」。例如:

  //visibility:public
  //conditions:default
  //command_line_option:copt

external 的情況略有不同:它不會顯示在 BUILD.bazel 檔案中,但會在內部用於管理外部存放區。將 Bazel 用做專案目錄時,這就會混淆

標準存放區名稱

自 Bazel 6.0 開始,標籤中的存放區名稱也可以以 @@ 開頭。

啟用選用的 BzlMod 功能時,這些標籤會做為外部存放區的替代名稱不重複標籤名稱,在專案中使用複雜的遞移依附元件樹狀結構時,這個標籤會十分重要。

例如,@@com_acme_anvil.1.0.3 可以是在專案本身的 BUILD.bazel 檔案中由 @anvil 識別的工作區目錄標準名稱,也可以是 @acme_anvil 出現在外部存放區 (例如在 @foo//:BUILD.bazel 中) 時。這三個標籤都參照相同目錄的內容。

標準存放區名稱不會顯示在 BUILD.bazel 檔案中,但會在分析階段 (執行會查看標籤值的 Starlark 函式時) 或查看 Bazel 查詢的結果時顯示。

Bazel 副檔名 (.bzl) 檔案

擴充功能檔案包含可匯入其他多個檔案的額外定義:

  • 而且其名稱結尾一律為 .bzl 副檔名。

  • 這些記錄檔必須屬於 Bazel 套件,因此可以透過標籤識別。例如 //bazel_utils:defs.bzl

  • 這些程式庫是以 Starlark 語言編寫,因此應遵循特定規範

  • 唯一可以定義 Starlark 函式的位置!換句話說,使用者無法在 BUILD.bazel 檔案中定義函式!

  • 即使匯入多次,系統一律會「評估一次」,且其定義的變數和函式會記錄為常數。

  • 您可以使用 load() 陳述式,從其他檔案匯入這些函式。

例如:

  • 來自 $PROJECT/my_definitions.bzl

    # The official release number
    release_version = "1.0.0"
    
  • 來自 $PROJECT/BUILD.bazel

    # Import the value of `release_version` from my_definitions.bzl
    load("//:my_definitions.bzl", "release_version")
    
    # Compile C++ executable, hard-coding its version number with a macro.
    cc_binary(
      name = "my_program",
      defines = [ "RELEASE_VERSION=" + release_version ],
      sources = [ … ],
    )
    

load() 陳述式具有特殊的語意:

  • 第一個引數必須是 .bzl 檔案的標籤字串 (例如 "//src:definitions.bzl")。

  • 其他引數名稱匯入的常數或函式:

    • 如果引數是字串,值必須是 .bzl 檔案定義的匯入符號名稱。例如:

      load("//src:defs.bzl", "my_var", "my_func")
      
    • 如果引數是變數指派,會定義匯入符號的本機別名。E.g.:

      load("//src:defs.bzl", "my_var", func = "my_func")
      
    • 沒有萬用字元:所有匯入的常數與函式都必須明確命名。

    • load() 出現在 .bzl 檔案中時,系統一律不會記錄匯入的符號。

    • 同樣地,系統也不會記錄名稱開頭為與底線 (例如 _foo) 的符號,且無法匯入。亦即這些都是私人項目,僅供定義這些屬性的 .bzl 檔案使用。

.bzl 檔案有時想從另一個檔案匯入一個符號,並以相同名稱重新匯出。按照以下方式要求別名:

# From //src:utils.bzl

# Import "my_vars" from defs.bzl as '_my_var'.
load("//src:defs.bzl", _my_var = "my_var")

# Define my_var in the current scope as a copy of _my_var
# This symbol and its value will be recorded, and available for import
# to any other file that loads //src:utils.bzl.
my_var = _my_var

  1. 由於舊版原因,這個檔案也可以稱為 WORKSPACE 

  2. 此外,基於舊版原因,這個檔案也可以稱為 BUILD