Bazel 教學課程:建構 C++ 專案

簡介

初次使用 Bazel 嗎?你來對地方了!請參閱這篇「第一個建構作業」教學課程,簡要瞭解如何使用 Bazel。本教學課程會定義 Bazel 環境中使用的重要術語,並逐步說明 Bazel 工作流程的基本概念。首先會介紹必要工具,然後會建構及執行三個專案。複雜度會逐漸增加,您可以瞭解專案複雜度提高的原因和方式。

雖然 Bazel 是支援多語言建構的建構系統,但本教學課程會以 C++ 專案為例,提供適用於大多數語言的一般指南和流程。

預計完成時間:30 分鐘。

必要條件

如果尚未安裝 Bazel,請先安裝 Bazel。本教學課程使用 Git 進行原始碼控管,因此建議您安裝 Git,以獲得最佳效果。

接下來,請在所選的指令列工具中執行下列指令,從 Bazel 的 GitHub 存放區擷取範例專案:

git clone https://github.com/bazelbuild/examples

本教學課程的範例專案位於 examples/cpp-tutorial 目錄中。

結構如下:

examples
└── cpp-tutorial
    ├──stage1
    │  ├── main
    │  │   ├── BUILD
    │  │   └── hello-world.cc
    │  └── MODULE.bazel
    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── MODULE.bazel
    └──stage3
       ├── main
       │   ├── BUILD
       │   ├── hello-world.cc
       │   ├── hello-greet.cc
       │   └── hello-greet.h
       ├── lib
       │   ├── BUILD
       │   ├── hello-time.cc
       │   └── hello-time.h
       └── MODULE.bazel

共有三組檔案,每組代表本教學課程中的一個階段。在第一階段,您將建構位於單一套件中的單一目標。在第二階段,您將從單一套件建構二進位檔和程式庫。在最後的第三階段,您將建立包含多個套件和目標的專案。

摘要:簡介

安裝 Bazel (和 Git) 並複製本教學課程的存放區後,您已經符合基本條件,可以開始第一次使用 Bazel 進行建構。請繼續閱讀下一節,瞭解一些術語並設定工作區

開始使用

您必須先設定專案的工作區,才能建構專案。工作區是保存專案來源檔案和 Bazel 建構輸出內容的目錄。這個目錄也包含下列重要檔案:

  • 可將目錄及其內容識別為 Bazel 工作區的 MODULE.bazel 檔案,其位於專案目錄結構的根目錄。您也可以在這裡指定外部依附元件。
  • 一或多個 BUILD 檔案,用於告知 Bazel 如何建構專案的不同部分。工作區中含有 BUILD 檔案的目錄就是套件 (本教學課程稍後會詳細說明)。

在日後的專案中,如要將目錄指定為 Bazel 工作區,請在該目錄中建立名為 MODULE.bazel 的空白檔案。在本教學課程中,每個階段都已提供 MODULE.bazel 檔案。

瞭解 BUILD 檔案

BUILD 檔案包含多種 Bazel 指令,每個 BUILD 檔案至少需要一項規則做為一組指令,告訴 Bazel 如何建構所需的輸出內容,例如可執行的二進位檔或程式庫。BUILD 檔案中的每個建構規則例項都稱為「目標」,並指向一組特定的來源檔案和依附元件。目標也可以指向其他目標。

來看看 cpp-tutorial/stage1/main 目錄中的 BUILD 檔案:

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)

在本範例中,hello-world 目標會例項化 Bazel 的內建 cc_binary 規則。這項規則會告知 Bazel 從 hello-world.cc> 來源檔案建構獨立的可執行二進位檔,不需依附元件。

摘要:入門

您現在已熟悉一些重要術語,以及這些術語在這個專案和一般 Bazel 情境下的意義。在下一節中,您將建構及測試專案的第 1 階段。

第 1 階段:單一目標、單一套件

現在可以開始建構專案的第一部分。專案第 1 階段的結構如下圖所示:

examples
└── cpp-tutorial
    └──stage1
       ├── main
       │   ├── BUILD
       │   └── hello-world.cc
       └── MODULE.bazel

執行下列指令,移至 cpp-tutorial/stage1 目錄:

cd cpp-tutorial/stage1

接著執行下列指令:

bazel build //main:hello-world

在目標標籤中,//main: 部分是 BUILD 檔案相對於工作區根目錄的位置,而 hello-worldBUILD 檔案中的目標名稱。

Bazel 會產生類似下列內容:

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 2.267s, Critical Path: 0.25s

您剛建構了第一個 Bazel 目標。Bazel 會將建構輸出內容放在工作區根目錄的 bazel-bin 目錄中。

現在測試剛建構的二進位檔,也就是:

bazel-bin/main/hello-world

這樣就會顯示「Hello world」訊息。

以下是第 1 階段的依附關係圖表:

hello-world 的依附關係圖表,顯示單一目標和單一來源檔案。

摘要:第 1 階段

您已完成第一個建構作業,現在對建構作業的結構有基本概念。在下一個階段,您將新增另一個目標,增加複雜度。

第 2 階段:多個建構目標

雖然小型專案只要一個目標就夠了,但您可能想將大型專案分割成多個目標和套件。這樣就能快速進行增量建構 (也就是 Bazel 只會重建變更的內容),並同時建構專案的多個部分,加快建構速度。本教學課程的這一階段會新增目標,下一階段則會新增套件。

這是您在第 2 階段使用的目錄:

    ├──stage2
    │  ├── main
    │  │   ├── BUILD
    │  │   ├── hello-world.cc
    │  │   ├── hello-greet.cc
    │  │   └── hello-greet.h
    │  └── MODULE.bazel

來看看 cpp-tutorial/stage2/main 目錄中的 BUILD 檔案:

cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        ":hello-greet",
    ],
)

有了這個 BUILD 檔案,Bazel 會先建構 hello-greet 程式庫 (使用 Bazel 內建的 cc_library 規則),然後建構 hello-world 二進位檔。hello-world 目標中的 deps 屬性會告知 Bazel,建構 hello-world 二進位檔時需要 hello-greet 程式庫。

必須先變更目錄才能建構這個新版專案,方法是執行下列指令,切換至 cpp-tutorial/stage2 目錄:

cd ../stage2

現在,您可以使用下列熟悉的指令建構新的二進位檔:

bazel build //main:hello-world

Bazel 會再次產生類似下列內容:

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 2.399s, Critical Path: 0.30s

現在可以測試剛建構的二進位檔,該檔案會傳回另一個「Hello world」:

bazel-bin/main/hello-world

如果您現在修改 hello-greet.cc 並重建專案,Bazel 只會重新編譯該檔案。

查看依附元件圖表,您會發現 hello-world 依附於名為 hello-greet 的額外輸入內容:

「hello-world」的依附關係圖表,顯示修改檔案後的依附元件變更。

摘要:第 2 階段

您現在已建構包含兩個目標的專案。hello-world 目標會建構一個來源檔案,並依附於另一個目標 (//main:hello-greet),後者會建構兩個額外的來源檔案。在下一節中,我們將進一步新增另一個套件。

第 3 階段:多個套件

下一個階段會增加複雜度,並使用多個套件建構專案。cpp-tutorial/stage3 目錄的結構和內容如下:

└──stage3
   ├── main
   │   ├── BUILD
   │   ├── hello-world.cc
   │   ├── hello-greet.cc
   │   └── hello-greet.h
   ├── lib
   │   ├── BUILD
   │   ├── hello-time.cc
   │   └── hello-time.h
   └── MODULE.bazel

您可以看到現在有兩個子目錄,且每個子目錄都包含 BUILD 檔案。因此,對 Bazel 而言,工作區現在包含 libmain 兩個套件。

來看看 lib/BUILD 檔案:

cc_library(
    name = "hello-time",
    srcs = ["hello-time.cc"],
    hdrs = ["hello-time.h"],
    visibility = ["//main:__pkg__"],
)

以及 main/BUILD 檔案:

cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        ":hello-greet",
        "//lib:hello-time",
    ],
)

主要套件中的 hello-world 目標依附於 lib 套件中的 hello-time 目標 (因此目標標籤為 //lib:hello-time) - Bazel 會透過 deps 屬性瞭解這一點。您可以在依附關係圖表中看到這點:

「hello-world」的依附關係圖表,顯示主要套件中的目標如何依附「lib」套件中的目標。

如要成功建構,請使用可見度屬性,讓 lib/BUILD 中的 //lib:hello-time 目標明確顯示在 main/BUILD 的目標中。這是因為根據預設,只有相同 BUILD 檔案中的其他目標才能看到目標。Bazel 會使用目標可見度來避免問題,例如含有實作詳細資料的程式庫洩漏到公開 API 中。

現在建構這個專案的最終版本。執行下列指令,切換至 cpp-tutorial/stage3 目錄:

cd  ../stage3

再次執行下列指令:

bazel build //main:hello-world

Bazel 會產生類似下列內容:

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.167s, Critical Path: 0.00s

現在測試本教學課程的最後一個二進位檔,確認是否收到最終的 Hello world 訊息:

bazel-bin/main/hello-world

摘要:第 3 階段

現在已將專案建構為包含三個目標的兩個套件,並瞭解這些目標之間的依附關係。您已經可以開始使用 Bazel 建構其他專案。下一節將說明如何繼續 Bazel 學習之旅。

後續步驟

您已完成第一個 Bazel 基本建構作業,但這只是開始。如要繼續學習 Bazel,請參閱下列其他資源:

祝您建構愉快!