Bazel 專案版面配置和機構

Bazel 專案

Bazel 專案是來源和建構檔案的集合,用於說明下列項目:

  • 如何建構二進位檔或資料檔案等構件,以及這些構件的依附元件。
  • 如何執行特定指令,例如指令碼或可執行檔。
  • 如何測試上述建構成果。

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,僅適用於 Linux 和大小寫敏感的檔案系統。Blaze 只使用檔案名稱 BUILD 來儲存建構指示。

不過,Bazel 也需要在 Windows 上執行,而 Windows 的檔案系統不區分大小寫,許多 Google 或非 Google 專案都已使用名為「build」的目錄,因此會與此類系統上的「BUILD」檔案衝突。

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

工作區指令

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 標示,在外部存放區 (例如 @foo//:BUILD.bazel 內) 中則以 @acme_anvil 標示。所有三個標籤都會參照同一個目錄的內容。

標準存放區名稱不會顯示在 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。