原文参见这里
targets
和 BUILD
文件。读完这个指南后,你还可以再看下 C++编译用例了解下更高级的特性。
在这份指南中你将会学到怎么:
target
target
和package
package
之间的可见性label
)来引用target
开始之前你先得把bazel给装上了,然后从bazel的Github仓库中检出下示例工程:
git clone https://github.com/bazelbuild/examples/
示例工程在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个集合的文件,每一个代表了一个阶段。在第一个阶段,你将学会在单一包(package
)中的单一target的编译,在第二阶段,你将把工程分割成多个target,但是在同一个包里。最后一个阶段,将会把工程分割成多个包(main和lib)和多个target,然后编译。
在开始编译之前,你需要先设置好workspace,workspace是包含了工程源码和bazel编译输出文件的一个目录,同时它还包含了一些特殊的文件:
WORKSPACE
文件:用于标示这个目录以及里面的内容是bazel workspace,它位于workspace的根目录。BUILD
文件:用于告诉bazel怎么编译工程的各个部分(在workspace中包含BUILD文件的目录被称为一个包[package
],后面会介绍)。 为了把一个目录变成bazel的workspace你只需要在这个目录下创建一个WORKSPACE
空文件。
当bazel编译一个工程时,所有的输入和依赖必须都在同一个workspace里。在不同的workspace中文件是彼此隔离的,这个超出了这个指南的范畴。
BUILD文件中包含了多个不同类型的bazel指令。其中最重要的是编译规则(build rule
),它告诉bazel怎么编译目标输出,是一个执行文件还是一个库。BUILD
文件中每一个编译规则被称为target
,指向了一堆源文件和相关的依赖,一个target也可以指向其他target。
咱瞅瞅cpp-tutorial/stage1/main
目录下的BUILD
文件:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
在上面的例子中,hello-world
target使用了bazel内置的cc_binary rule,它告诉bazel怎么从没有依赖的hhello-world.cc
源文件编译出可执行文件。
target中的属性包含编译的依赖和选项,例如:name
是必须的,就是名字,而其他有好多可选项,srcs
指定了编译的源文件。
现在我们就使用bazel来编译下示例工程,cd到cpp-tutorial/stage1
,执行下面的命令:
bazel build //main:hello-world
可以看到,这里的//main
是BUILD
文件相对于workspace根目录的相对路径,hello-world
是BUILD文件中定义的target的名字(target的标签后面会介绍)。然后bazel会有下面的输出:
INFO: Analyzed target //main:hello-world (13 packages loaded, 66 targets configured).
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 0.621s, Critical Path: 0.28s
INFO: 2 processes: 2 linux-sandbox.
INFO: Build completed successfully, 6 total actions
恭喜,你已经编译出了你的第一个bazel target,bazel把编译的输出文件放到了workspace根目录的bazel-bin
目录下,去瞅瞅里面的目录结构吧。
现在就可以用下面的命令测试下编译好的文件了:
./bazel-bin/main/hello-world
为了能够成功编译BUILD
文件中必须包含所有的编译依赖,bazel使用这些来生成编译依赖关系图,而后用于增量编译。
让我们来看看示例工程的依赖吧,首先使用下面的命令来生成一个依赖图的文本(请在workspace根目录下运行):
bazel query 'deps(//main:hello-world)' --noimplicit_deps --output=graph
上面的命令用于查找//main:hello-world
target的所有依赖,然后将输出格式化为图,在我电脑上输出是下面的样子:
digraph mygraph {
node [shape=box];
"//main:hello-world"
"//main:hello-world" -> "//main:hello-world.cc"
"//main:hello-world.cc"
}
然后就可以把生成的文本复制到网页端的小工具GraphViz上查看这个依赖图。Ubuntu上你也可以在本地安装个GraphViz
和xdot Dot Viewer
在本地查看:
sudo apt update && sudo apt install graphviz xdot
装好后就可以把上面的的依赖图文本直接通过管道输出显示到xdot
上:
xdot <(bazel query 'deps(//main:hello-world)' --noimplicit_deps --output=graph)
然后你就可以看到下面这张依赖图:从单一源文件编译的单一target
单一target对于小项目是比较高效的,对于大项目来说,能够拆分成多个target和包来使用增量编译的特性就太棒了,这样可以通过把项目的多个部分同时编译来加快速度。
让我们把上面第一个阶段的示例工程分成多个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
库(使用内置的cc_library rule)然后编译hello-world
可执行文件,hello-world
中的deps
告诉bazel:hello-greet
是编译hello-world
可执行文件时的必须库。
让我们用下面的命令编译下这个新的工程(先cd到cpp-tutorial/stage2
目录):
bazel build //main:hello-world
bazel会有类似下面的输出:
INFO: Analyzed target //main:hello-world (13 packages loaded, 69 targets configured).
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 0.860s, Critical Path: 0.41s
INFO: 3 processes: 3 linux-sandbox.
INFO: Build completed successfully, 7 total actions
我们来试试编译出的文件:
./bazel-bin/main/hello-world
你可以修改下hello-greet.cc
,bazel会自动重新编译那个文件。
来瞅瞅依赖图,可以看到hello-world
输入是相同的,但是依赖图变得不一样了:
现在你已经用两个target来编译这个工程 了:hello-world
target从一个源文件和另一个target(//main:hello-greet
)编译,而它是从两个源文件编译的。
现在让我们把工程分成多个包,瞅瞅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
文件,因此在这个workspace中,对于bazel它包含两个包:main
和lib
(对于两个目录的名称)。
看下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",
],
)
如你所见,main
包中的hello-world
target依赖于lib
包中的hello-time
(deps
中的//lib:hello-time
,'lib’是包名[目录名],hello-time是lib/BUILD
文件中定义的库名称),来看下依赖图:
注意为了能够成功编译,这里把lib/BUILD
中//lib:hello-time
target的可见性暴露给了main/BUILD
(通过设置visibility
属性)。这样做是由于bazel的target只对于同一个BUILD文件里的其他target可见(bazel通过设置可见性来防止库的内部实现被公开到外部api中)。
让我们来编译下最终版的工程,cd到cpp-tutorial/stage3
执行下面的命令:
bazel build //main:hello-world
bazel会输出类似下面的log:
INFO: Analyzed target //main:hello-world (14 packages loaded, 77 targets configured).
INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 1.399s, Critical Path: 0.43s
INFO: 4 processes: 4 linux-sandbox.
INFO: Build completed successfully, 8 total actions
我们来试试编译出的文件:
./bazel-bin/main/hello-world
这会儿工程已经从两个包三个target中编译了,你也了解了他们之间的关系了。
在BUILD
文件和命令行中bazel使用标签来引用target,例如://main:hello-world
或者//lib:hello-time
,标签的定义是:
//path/to/package:target-name
如果target是一个编译target(rule target
),那么path/to/package
是包含BUILD
文件的路径,target-name
是这个BUILD
文件中target的名称(其中的name
属性);如果target是文件target(file target
)那么path/to/package
是包的目录,target-name
是目标文件的全路径。
如果target是在workspace根目录下定义的,那么它的包就是空的,可以使用//:target-name
来引用它;如果在同一个BUILD
文件中引用target你甚至可以省掉//
直接用:target-name
来引用它。