官網(wǎng):https://www.bazel.build
Github: https://github.com/bazelbuild/bazel
最近用到tensorflow的時候遇到了個新的編譯工具Bazel,踩了無數(shù)坑之后終于決定還是系統(tǒng)地學(xué)習(xí)一下這貨。
Bazel是一個類似于Make的編譯工具,是Google為其內(nèi)部軟件開發(fā)的特點量身定制的工具,如今Google使用它來構(gòu)建內(nèi)部大多數(shù)的軟件。Google認為直接用Makefile構(gòu)建軟件速度太慢,結(jié)果不可靠,所以構(gòu)建了一個新的工具叫做Bazel,Bazel的規(guī)則層級更高。
下面就以C++和Bazel結(jié)合的例子理解一下Bazel的工作原理。
Install
安裝過程請參考:http://bazel.io/docs/install.html
建立工作區(qū)(workspace)
Bazel的編譯是基于工作區(qū)(workspace)的概念。工作區(qū)是一個存放了所有源代碼和Bazel編譯輸出文件的目錄,也就是整個項目的根目錄。同時它也包含一些Bazel認識的文件:
- WORKSPACE文件,用于指定當(dāng)前文件夾就是一個Bazel的工作區(qū)。所以WORKSPACE文件總是存在于項目的根目錄下。
- 一個或多個BUILD文件,用于告訴Bazel怎么構(gòu)建項目的不同部分。(如果工作區(qū)中的一個目錄包含BUILD文件,那么它就是一個package。)
那么要指定一個目錄為Bazel的工作區(qū),就只要在該目錄下創(chuàng)建一個空的WORKSPACE文件即可。
當(dāng)Bazel編譯項目時,所有的輸入和依賴項都必須在同一個工作區(qū)。屬于不同工作區(qū)的文件,除非linked否則彼此獨立。
理解BUILD文件
一個BUILD文件包含了幾種不同類型的指令。其中最重要的是編譯指令,它告訴Bazel如何編譯想要的輸出,比如可執(zhí)行二進制文件或庫。BUILD文件中的每一條編譯指令被稱為一個target,它指向一系列的源文件和依賴,一個target也可以指向別的target。
舉個例子,下面這個hello-world的target利用了Bazel內(nèi)置的cc_binary編譯指令,來從hello-world.cc源文件(沒有其他依賴項)構(gòu)建一個可執(zhí)行二進制文件。指令里面有些屬性是強制的,比如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組文件,分別對應(yīng)本文中的3個例子。在第一個例子中,我們首先學(xué)習(xí)如何構(gòu)建單個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的輸出結(jié)構(gòu)。
現(xiàn)在你可以測試你剛剛生成的二進制文件了:
bazel-bin/main/hello-world
2. 查看依賴圖
一個成功的build將所有的依賴都顯式定義在了BUILD文件中。Bazel使用這些定義來創(chuàng)建項目的依賴圖,這能夠加速編譯的過程。
讓我們來可視化一下我們項目的依賴吧。首先,生成依賴圖的一段文字描述(即在工作區(qū)根目錄下運行下述指令):
bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph
這個指令告訴Bazel查找target //main:hello-world的所有依賴項(不包括host和隱式依賴),然后輸出圖的文字描述。再把文字描述貼到GraphViz里,你就可以看到如下的依賴圖了。可以看出這個項目是用單個源文件編譯出的單個target,并沒有別的依賴。

好的,到目前為止,我們已經(jīng)建立了工作區(qū),編譯了一個項目,并且查看了它的依賴。接下來讓我們加點難度。
3. 多個target的編譯
單個target的方式對于小項目來說是高效的,但是對于大項目來說,你可能會想把它拆分成多個target和多個package來實現(xiàn)快速增量的編譯(這樣就只需要重新編譯改變過的部分)。
首先我們來嘗試著把項目拆分成兩個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內(nèi)置的cc_library編譯指令),然后編譯hello-world這個二進制文件。hello-world這個target的deps屬性告訴Bazel,要構(gòu)建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
現(xiàn)在又可以測試剛剛生成的二進制文件了:
bazel-bin/main/hello-world
注意,如果你現(xiàn)在修改一下hello-greet.cc然后重新編譯整個項目的話,Bazel其實只會編譯修改過的那個文件。
然后我們再來看一下依賴圖,發(fā)現(xiàn)hello-world在編譯時候的結(jié)構(gòu)和之前有所不同,現(xiàn)在是有兩個targets。hello-world這個target從一個源文件編譯而來,同時依賴于另一個target//main:hello-greet,這個target又是從兩個源文件編譯而來。

4. 多個package的編譯
我們現(xiàn)在再將項目拆分成多個package??匆幌?code>cpp-tutorial/stage3目錄下的內(nèi)容:
└──stage3
├── main
│ ├── BUILD
│ ├── hello-world.cc
│ ├── hello-greet.cc
│ └── hello-greet.h
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
└── WORKSPACE
注意到我們現(xiàn)在有兩個子目錄了,每個子目錄中都包含了BUILD文件。因此,對于Bazel來說,整個工作區(qū)現(xiàn)在就包含了兩個package:lib和main。
lib/BUILD文件長這樣:
cc_library(
name = "hello-time",
srcs = ["hello-time.cc"],
hdrs = ["hello-time.h"],
visibility = ["http://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",
"http://lib:hello-time",
],
)
可以看出hello-world這個mainpackage中的target依賴于lib package中的hello-time target(即target label為://lib:hello-time)- Bazel是通過deps這個屬性知道自己的依賴項的。那么現(xiàn)在依賴圖就變成了下圖的樣子:

注意到lib/BUILD文件中我們將hello-time這個target顯式可見了(通過visibility屬性)。這是因為默認情況下,targets只對同一個BUILD文件里的其他targets可見(Bazel使用target visibility來防止像公有API中庫的實現(xiàn)細節(jié)的泄露等情況)。
好,讓我們編譯一下新的版本。進入到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
現(xiàn)在又可以測試剛剛生成的二進制文件了:
bazel-bin/main/hello-world
好,現(xiàn)在我們學(xué)會了編譯一個包含2個package和3個target的項目,并且理解了它們之前的依賴關(guān)系。