1. 程式人生 > >Bazel入門:編譯C++專案

Bazel入門:編譯C++專案

最近用到tensorflow的時候遇到了個新的編譯工具Bazel,踩了無數坑之後終於決定還是系統地學習一下這貨。

Bazel是一個類似於Make的編譯工具,是Google為其內部軟體開發的特點量身定製的工具,如今Google使用它來構建內部大多數的軟體。Google認為直接用Makefile構建軟體速度太慢,結果不可靠,所以構建了一個新的工具叫做Bazel,Bazel的規則層級更高。

下面就以C++和Bazel結合的例子理解一下Bazel的工作原理。

Install

建立工作區(workspace)

Bazel的編譯是基於工作區(workspace)的概念。工作區是一個存放了所有原始碼和Bazel編譯輸出檔案的目錄,也就是整個專案的根目錄。同時它也包含一些Bazel認識的檔案:

  1. WORKSPACE檔案,用於指定當前資料夾就是一個Bazel的工作區。所以WORKSPACE檔案總是存在於專案的根目錄下。
  2. 一個或多個BUILD檔案,用於告訴Bazel怎麼構建專案的不同部分。(如果工作區中的一個目錄包含BUILD檔案,那麼它就是一個package。)

那麼要指定一個目錄為Bazel的工作區,就只要在該目錄下建立一個空的WORKSPACE檔案即可。

當Bazel編譯專案時,所有的輸入和依賴項都必須在同一個工作區。屬於不同工作區的檔案,除非linked否則彼此獨立。

理解BUILD檔案

一個BUILD檔案包含了幾種不同型別的指令。其中最重要的是編譯指令,它告訴Bazel如何編譯想要的輸出,比如可執行二進位制檔案或庫。BUILD檔案中的每一條編譯指令被稱為一個target

,它指向一系列的原始檔和依賴,一個target也可以指向別的target。

舉個例子,下面這個hello-world的target利用了Bazel內建的cc_binary編譯指令,來從hello-world.cc原始檔(沒有其他依賴項)構建一個可執行二進位制檔案。指令裡面有些屬性是強制的,比如name,有些屬性則是可選的,srcs表示的是原始檔。

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

使用Bazel編譯專案

Bazel提供了一些編譯的例子,在https://github.com/bazelbuild/examples/

,可以clone到本地試一下。其中examples/cpp-tutorial目錄下包含了這麼些檔案:

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

可以看到分成了3組檔案,分別對應本文中的3個例子。在第一個例子中,我們首先學習如何構建單個package中的單個target。在第二個例子中,我們將把整個專案拆分成單個package的多個target。第三個例子則將專案拆分成多個package,用多個target編譯。

1. 編譯你的第一個Bazel專案

首先進入到cpp-tutorial/stage1目錄下,然後執行以下指令:

bazel build //main:hello-world

注意target中的//main:是BUILD檔案相對於WORKSPACE檔案的位置,hello-world則是我們在BUILD檔案中命名好的target的名字。

然後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 target就編譯好了!Bazel將編譯的輸出放在專案根目錄下的bazel-bin目錄下,可以看一下這個目錄,理解一下Bazel的輸出結構。

現在你可以測試你剛剛生成的二進位制檔案了:

bazel-bin/main/hello-world

2. 檢視依賴圖

一個成功的build將所有的依賴都顯式定義在了BUILD檔案中。Bazel使用這些定義來建立專案的依賴圖,這能夠加速編譯的過程。

讓我們來視覺化一下我們專案的依賴吧。首先,生成依賴圖的一段文字描述(即在工作區根目錄下執行下述指令):

bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
  --output graph

這個指令告訴Bazel查詢target //main:hello-world的所有依賴項(不包括host和隱式依賴),然後輸出圖的文字描述。再把文字描述貼到GraphViz裡,你就可以看到如下的依賴圖了。可以看出這個專案是用單個原始檔編譯出的單個target,並沒有別的依賴。

Dependency graph for 'hello-world'

好的,到目前為止,我們已經建立了工作區,編譯了一個專案,並且查看了它的依賴。接下來讓我們加點難度。

3. 多個target的編譯

單個target的方式對於小專案來說是高效的,但是對於大專案來說,你可能會想把它拆分成多個target和多個package來實現快速增量的編譯(這樣就只需要重新編譯改變過的部分)。

首先我們來嘗試著把專案拆分成兩個target。看一下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這個target的deps屬性告訴Bazel,要構建hello-world這個二進位制檔案需要hello-greet這個庫。

好,讓我們編譯一下新的版本。進入到cpp-tutorial/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

現在又可以測試剛剛生成的二進位制檔案了:

bazel-bin/main/hello-world

注意,如果你現在修改一下hello-greet.cc然後重新編譯整個專案的話,Bazel其實只會編譯修改過的那個檔案。

然後我們再來看一下依賴圖,發現hello-world在編譯時候的結構和之前有所不同,現在是有兩個targets。hello-world這個target從一個原始檔編譯而來,同時依賴於另一個target//main:hello-greet,這個target又是從兩個原始檔編譯而來。

Dependency graph for 'hello-world'

4. 多個package的編譯

我們現在再將專案拆分成多個package。看一下cpp-tutorial/stage3目錄下的內容:

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

注意到我們現在有兩個子目錄了,每個子目錄中都包含了BUILD檔案。因此,對於Bazel來說,整個工作區現在就包含了兩個package: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這個mainpackage中的target依賴於lib package中的hello-time target(即target label為://lib:hello-time)- Bazel是通過deps這個屬性知道自己的依賴項的。那麼現在依賴圖就變成了下圖的樣子:

Dependency graph for 'hello-world'

注意到lib/BUILD檔案中我們將hello-time這個target顯式可見了(通過visibility屬性)。這是因為預設情況下,targets只對同一個BUILD檔案裡的其他targets可見(Bazel使用target visibility來防止像公有API中庫的實現細節的洩露等情況)。

好,讓我們編譯一下新的版本。進入到cpp-tutorial/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

現在又可以測試剛剛生成的二進位制檔案了:

bazel-bin/main/hello-world

好,現在我們學會了編譯一個包含2個package和3個target的專案,並且理解了它們之前的依賴關係。

Reference