時間:2018-12-18
作者:魏文應(yīng)
一、前 言
通過這個本文凄硼,你將知道如何構(gòu)建一個C++項目:
- 編譯一個目標(target)。
- 可視化項目中目標的相互依賴關(guān)系。
- 控制包的可見性良哲,也就是限定包的作用域。
其實助隧,就是使你對 bazel 如何組織筑凫、編譯一個 C++ 項目有一個大致了解。這里,你可以參考 bazel 官方教程《Introduction to Bazel: Building a C++ Project》:
二巍实、bazel 構(gòu)建一個C++項目
下載示例代碼
首先滓技,找個位置,創(chuàng)建一個文件夾用于存放下載的代碼棚潦,依次執(zhí)行下面命令:
# 創(chuàng)建一個目錄
mkdir ~/wwy-dir
# 進入這個目錄
cd ~/wwy-dir
然后令漂,從 github 上下載官方提供的示例代碼,執(zhí)行下面命令:
git clone https://github.com/bazelbuild/examples/
下載完成以后丸边,就會在當前目錄下叠必,得到一個名為 examples
的文件夾:
這個項目有 C++
、java
妹窖、android
纬朝、java-maven
等示例,我們進入 C++
項目的示例:
cd examples/cpp-tutorial/
你可以通過 tree
命令(你還可以指定子目錄層級骄呼,tree -L 2
指定子目錄為2)玄组,查看一下有哪些文件。下面是目錄 examples/cpp-tutorial/
中的內(nèi)容:
├── README.md
├── stage1
│ ├── main
│ │ ├── BUILD
│ │ └── hello-world.cc
│ ├── README.md
│ └── WORKSPACE
├── stage2
│ ├── main
│ │ ├── BUILD
│ │ ├── hello-greet.cc
│ │ ├── hello-greet.h
│ │ └── hello-world.cc
│ ├── README.md
│ └── WORKSPACE
└── stage3
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
├── main
│ ├── BUILD
│ ├── hello-greet.cc
│ ├── hello-greet.h
│ └── hello-world.cc
├── README.md
└── WORKSPACE
上面 stage1
谒麦、stage2
俄讹、stage3
是三個示例,是三個獨立的工程項目(三個工程之間沒有直接關(guān)系)绕德。示例 stage1 演示了一個簡單的 C++ 程序患膛,示例 stage2 演示了一個 .cc 文件需要引用另一個 .cc 文件的情況,示例 stage3 演示了一個 .cc 文件和 main 程序不在同一個目錄下的情況(一個在 main 目錄耻蛇,一個在 lib 目錄)踪蹬。其中,以下幾點比較重要:
- WORKSPACE : 工作空間臣咖,在項目工程的根(root)目錄下跃捣,有個
WORKSPACE
文件,表示這是一個工程項目夺蛇。比如疚漆,stage1 目錄下有一個WORKSPACE
文件,說明這個目錄下有一個完整的工程項目刁赦。- BUILD :
BUILD
文件娶聘,這個文件會告訴 bazel 如何編譯 .cc 源文件,限定多個源文件之間的關(guān)系甚脉。工作空間中的可能有多個文件夾丸升,每個目錄有代碼的源文件和BUILD
文件,這些有BUILD
文件的文件夾牺氨,被稱為 包(pakege)狡耻。
Bazel 去編譯構(gòu)建一個項目(project)時墩剖,所有的輸入文件和依賴文件,都應(yīng)該要求在同一個工作空間(WORKSPACE)中夷狰。 雖然可以使用某種鏈接方式涛碑,達到使用其它工作空間的文件的目的,但一般不這么使用孵淘。
簡單認識 BUILD 文件
BUILD 文件包含了一系列不同類型的指令蒲障,Bazel 工具執(zhí)行這些指令,來構(gòu)建和編譯我們的目標(target):
你可能會問:什么是目標target瘫证?target 就是你想生成的東西揉阎,比如使用源代碼生成二進制可執(zhí)行文件,這個可執(zhí)行文件就是target背捌。當然毙籽,target 還可以是一些 .o中間文件、lib 庫文件等等毡庆。
BUILD 文件中坑赡,最重要命令的就是 構(gòu)建規(guī)則(build rule),它告訴 Bazel 如何去創(chuàng)建和生成我們的目標么抗。比如 cpp-tutorial/stage1/main
目錄下的 BUILD
文件毅否,內(nèi)容如下:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
上面規(guī)則中, hello-world
就是 目標target蝇刀, hello-world.cc
就是 依賴文件螟加,意思就是說,使用 hello-world.cc
文件吞琐,編譯生成一個我們想要的名為 hello-world
的可執(zhí)行文件捆探。雖然生成的目標文件 hello-world
,這個命名你可以隨意起站粟,但是黍图,我們一般會命名成和源文件名稱一致的名字。比如源文件是 srcs = [xxx.cc]
奴烙,那么生成的文件就命名為 xxx
即可助被。
編譯工程
接下來,我們先來嘗試編譯一個簡單的工程缸沃。進入 cpp-tutorial/stage1
這個目錄恰起,并執(zhí)行下面命令:
bazel build //main:hello-world
//main:
是相對路徑修械,相對于工作區(qū)間 workspace 的根目錄的路徑趾牧。hello-world
是目標target,Bazel工具使用 main 目錄下 BUILD 這個文件指定的編譯規(guī)則肯污,來編譯生成目標翘单。編譯過程吨枉,會打印下面信息:
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 3.756s, Critical Path: 0.45s
INFO: 2 processes: 2 linux-sandbox.
INFO: Build completed successfully, 6 total actions
恭喜你!第一次使用 Bazel 成功編譯了你的項目哄芜。這是貌亭,項目空間的根目錄下,生成目錄 bazel-bin
认臊,這個目錄中圃庭,包含有剛才編譯生成的目標:
上圖中,bazel-
開頭的文件失晴,是編譯過程中剧腻,bazel 生成的中間文件目標文件。我們來執(zhí)行一些目標文件:
bazel-bin/main/hello-world
這時涂屁,就會運行可執(zhí)行程序 hello-world 书在,打印結(jié)果如下:
查看依賴關(guān)系圖
有時你想知道,目標是依賴哪些文件生成的呢拆又?你除了查看 BUILD 文件以外儒旬,還可以使用 bazel 工具查看。比如執(zhí)行下面命令:
bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph
deps(//main:hello-world)
說的是查看 //main:hello-world
這個目標的依賴關(guān)系帖族。--output graph
指定輸出格式為圖的格式栈源。打印結(jié)果如下:
INFO: Invocation ID: 661d04c1-f7f1-4419-a54c-afa1ba9619b4
digraph mygraph {
node [shape=box];
"http://main:hello-world"
"http://main:hello-world" -> "http://main:hello-world.cc"
"http://main:hello-world.cc"
}
其中,出去 INFO
中的提示類信息竖般,下面的內(nèi)容描述的是一個圖:
digraph mygraph {
node [shape=box];
"http://main:hello-world"
"http://main:hello-world" -> "http://main:hello-world.cc"
"http://main:hello-world.cc"
}
可以將上面這個代碼凉翻,粘貼到網(wǎng)頁版的 Graphviz 中:
然后點擊 Generate Graph 這個圖標按鈕,生成圖捻激,//main:hello-world
指向 //main:hello-world.cc
制轰, 說明 //main:hello-world
的生成依賴于 //main:hello-world.cc
。對應(yīng) ubuntu 用戶來說胞谭,還可以安裝 Graphviz 客戶端垃杖,執(zhí)行下面命令安裝:
sudo apt update && sudo apt install graphviz xdot
然后使用 graphviz 客戶端軟件直接在本地顯示,執(zhí)行下面命令:
xdot <(bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph)
顯示效果是一樣的丈屹,如下:
編譯多個目標
在上面的示例中调俘,我們只有一個 .cc 源文件,但我們項目開發(fā)中旺垒,一般會有多個 .cc 源文件彩库。下面,將演示如何編譯多個 .cc 源文件這種情況先蒋。先進入示例 stage2 工程目錄下:
cd cpp-tuorial/stage2
可以查看該項目下 main
文件夾下的 BUILD
文件骇钦,內(nèi)容如下:
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 這個目標竞漾,這個目標是一個 C++ 的 lib 庫文件(使用 bazel 內(nèi)建規(guī)則 cc_library rule 來生成這個lib)眯搭。然后窥翩,Bazel 再編譯 hello-world 這個目標,這個目標是一個二進制可執(zhí)行文件鳞仙。deps
這個屬性寇蚊,告訴 Bazel 編譯 hello-world 時,需要用到 hello-greet
這個目標棍好,也就是 hello-world
的生成需要依賴(dependencies) hello-greet
仗岸。執(zhí)行下面命令來編譯 stage2 這個工程:
bazel build //main:hello-world
編譯完成以后,和剛才的示例 stage1 一樣借笙,可以嘗試執(zhí)行一下生成的二進制文件:
bazel-bin/main/hello-world
打印結(jié)果如下:
Hello world
Wed Dec 19 03:19:53 2018
如果這時候爹梁,你去修改 hello-greet.cc
這個源文件,那么提澎,bazel 只會重新編譯 ``hello-greet.cc` 這個文件姚垃,其它文件不會被重新編譯。其中:
# build 經(jīng)常被簡稱為編譯盼忌,其實是包含了很多步驟:編譯积糯、鏈接等。
# c++ 語言編譯過程大致如下:
# - 編譯(compile): .cc文件編譯生成.o文件谦纱。
# - 鏈接(link): 將所有.o文件鏈接生成二進制可執(zhí)行文件看成。
其實就是如果你 bazel build
編譯過一次,就會生成中間緩存文件(.o 文件和其它文件)跨嘉,然后將這些中間文件鏈接生成最終的二進制可執(zhí)行文件川慌,這個可執(zhí)行文件就是我們想要生成的最終的目標。等你下次編譯的時候祠乃,只會編譯你修改過的文件梦重,其它緩存文件不變,最終再次鏈接這些緩存文件亮瓷,生成新的最終目標琴拧。這么做,就得到了這樣一個目的:
編譯生成緩存文件的時間比較長嘱支,不需要生成緩存文件蚓胸,節(jié)約了編譯時間。這樣縮減了整個 build 過程的時間除师,提高構(gòu)建效率沛膳。
同樣,執(zhí)行下面命令汛聚,我們可以查看一下依賴關(guān)系圖:
xdot <(bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph)
結(jié)果如下:
從中可以看出锹安,生成 hello-world
這個目標,需要依賴一個 hello-world.cc
源文件和一個 hello-greet
目標。這種增量式的構(gòu)建方式八毯,方便我們往項目中添加新的源文件搓侄,達到源文件分離瞄桨、目標分離的效果话速,減少耦合。
使用多個包
上面芯侥,我們講的是使用多個目標泊交。那么,包(package)又是什么玩意柱查?我們可以看示例 stage3 廓俭,來理解什么是包。示例 stage3 的文件結(jié)構(gòu)如下:
└──stage3
├── main
│ ├── BUILD
│ ├── hello-world.cc
│ ├── hello-greet.cc
│ └── hello-greet.h
├── lib
│ ├── BUILD
│ ├── hello-time.cc
│ └── hello-time.h
└── WORKSPACE
因為目錄 stage 下唉工,有兩個子目錄 main 和 lib研乒,而且子目錄內(nèi)都包含了 BUILD 文件,因此淋硝, main
和 lib
被稱為 包(package) 雹熬,它們是項目 stage3 的兩個包。前面 stage1 和 stage2 兩個項目中谣膳,源文件都在一個目錄下竿报,引入包的概念以后,源文件可以在項目空間中的不同目錄下继谚×揖可以看一下 lib/BUILD
這個文件的內(nèi)容:
cc_library(
name = "hello-time",
srcs = ["hello-time.cc"],
hdrs = ["hello-time.h"],
visibility = ["http://main:__pkg__"],
)
以及 main/BUILD
文件中的內(nèi)容:
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
這個 target 在 package main
中花履,需要依賴 target hello-time
芽世,但是 hello-time
這個 target 在 package lib
中。默認情況下诡壁,target 只在同一個 BUILD
中有效捂襟,其它 BUILD
不可見,也就是無法使用欢峰。為了讓其它 package 下的 target 也能使用葬荷,那么,只需加上 visibility
即可纽帖。比如上面的 visibility = ["http://main:__pkg__"]
就是告訴 Bazel 宠漩,hello-time 這個 target ,main 這個包可以看到懊直,這樣 hello-world
就可以使用 hello-time
這個target 了扒吁。這就好比:
'''
下面打個形象的比喻:
張三、李四室囊、王五各自家中有一些好吃的雕崩。張三拿出他家的紅棗給李四看魁索,
并說,李四你可以吃我家的紅棗盼铁,這時李四才能吃張三家的紅棗粗蔚,至于李四吃
不吃,又是另一回事饶火。同時鹏控,張三沒有說讓王五吃他家紅棗,連看都不讓王五
看肤寝,王五當然吃不到張三家的紅棗啦当辐。
'''
這樣,每個 package 目錄下有一個自己的 BUILD
鲤看,既可以做到相互獨立缘揪,也可以做到相互調(diào)用,減少耦合义桂,利用維護找筝。你同樣可以通過下面命令編譯項目 stage3 :
bazel build //main:hello-world
編譯完成以后,執(zhí)行生成的二進制可執(zhí)行文件:
bazel-bin/main/hello-world
還可以通過下面指令澡刹,查看依賴關(guān)系圖:
xdot <(bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph)
在目標中使用標簽
所謂標簽呻征,其實就是 路徑 + 目標
。在使用命令 bazel build //main:hello-world
和 BUILD 文件中 //main:hello-world
罢浇,以及 //lib:hello-time
陆赋。其中 //main:
和 //lib:
都是包的相對路徑,相對于項目空間的根目錄的路徑嚷闭。在命令 bazel build target
攒岛,如果其中 target 這個目標是一個需要編譯的目標,那么這個目標所在的目錄下胞锰,要有一個 BUILD
腳本灾锯,而且這個target 在 BUILD
腳本中要有,名稱相同嗅榕。比如顺饮,上面 bazel build //main:hello-world
中的目標 //main:hello-world
,在 main/BUILD
中有相應(yīng)的 target name = hello-world
凌那,兩者 hello-world
這個名字一致兼雄。
如果 target 和當前 BUILD 腳本在同一個 package 中(其實就是在同一個目錄下),那么路徑可以不寫帽蝶,比如 : //:hello-world
赦肋。如果是在同一個 BUILD 腳本中,可以更簡潔一些,這么寫 :hello-world
佃乘。
三囱井、小 結(jié)
至此,我們完成了 C++ 編譯的基本操作趣避。