建立工作区(workspace
)
Bazel
的编译是基于工作区(workspace
)的概念。工作区是一个存放了所有源代码和Bazel
编译输出文件的目录,也就是整个项目的根目录。同时它也包含一些Bazel
认识的文件:
WORKSPACE
文件,用于指定当前文件夹就是一个Bazel
的工作区。所以WORKSPACE
文件总是存在于项目的根目录下。- 一个或多个
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
表示的是源文件。1
2
3
4cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)使用
Bazel
编译项目Bazel
提供了一些编译的例子,在参考链接中,可以clone
到本地试一下。其中examples/cpp-tutorial
目录下包含了这么些文件:可以看到分成了3组文件,分别对应本文中的3个例子。在第一个例子中,我们首先学习如何构建单个1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25examples
└── 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
└── WORKSPACEpackage
中的单个target
。在第二个例子中,我们将把整个项目拆分成单个package
的多个target
。第三个例子则将项目拆分成多个package
,用多个target
编译。1.编译你的第一个
首先进入到Bazel
项目cpp-tutorial/stage1
路径下,然后运行以下指令:注意1
bazel build //main:hello-world
target
中的//main:
是BUILD
文件相对于WORKSPACE
文件的位置,hello-world
则是我们在BUILD
文件中命名好的target
的名字。
然后Bazel
就会有一些类似这样的输出:这样你的第一个1
2
3
4INFO: Found 1 target...
Target //main:hello-world up-to-date:
bazel-bin/main/hello-world
INFO: Elapsed time: 2.267s, Critical Path: 0.25sBazel target
就编译好了!Bazel
将编译的输出放在项目根目录下的bazel-bin
目录下,可以看一下这个目录,理解一下Bazel
的输出结构。
现在你可以测试你刚刚生成的二进制文件了:1
bazel-bin/main/hello-world
2.查看依赖图
一个成功的build
将所有的依赖都显式定义在了BUILD
文件中。Bazel
使用这些定义来创建项目的依赖图,这能够加速编译的过程。
让我们来可视化一下我们项目的依赖吧。首先,生成依赖图的一段文字描述(即在工作区根目录下运行下述指令):这个指令告诉1
2bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graphBazel
查找target //main:hello-world
的所有依赖项(不包括host
和隐式依赖),然后输出图的文字描述。再把文字描述贴到GraphViz
里(如果不行可以尝试一下其他工具网站),你就可以看到如下的依赖图了。可以看出这个项目是用单个源文件编译出的单个target
,并没有别的依赖。3.多个
单个target
的编译target
的方式对于小项目来说是高效的,但是对于大项目来说,你可能会想把它拆分成多个target
和多个package
来实现快速增量的编译(这样就只需要重新编译改变过的部分)。
首先我们来尝试着把项目拆分成两个target
。看一下cpp-tutorial/stage2/main
目录下的BUILD
文件,它是这样的:
1 | cc_library( |
我们看到在这个BUILD
文件中,Bazel
首先编译了hello-greet
这个库(利用Bazel
内置的cc_library
编译指令),然后编译hello-world
这个二进制文件。hello-world
这个target
的deps
属性告诉Bazel
,要构建hello-world
这个二进制文件需要hello-greet
这个库。
好,让我们编译一下新的版本。进入到cpp-tutorial/stage2
目录下然后运行以下指令:
1 | bazel build //main:hello-world |
然后Bazel
又会有一些类似这样的输出:
1 | INFO: Found 1 target... |
现在又可以测试刚刚生成的二进制文件了:
1 | bazel-bin/main/hello-world |
注意,如果你现在修改一下hello-greet.cc
然后重新编译整个项目的话,Bazel
其实只会编译修改过的那个文件。
然后我们再来看一下依赖图,发现hello-world
在编译时候的结构和之前有所不同,现在是有两个targets
。hello-world
这个target
从一个源文件编译而来,同时依赖于另一个target//main:hello-greet
,这个target
又是从两个源文件编译而来。
4.多个package
的编译
我们现在再将项目拆分成多个package。看一下cpp-tutorial/stage3
目录下的内容:
1 | └──stage3 |
注意到我们现在有两个子目录了,每个子目录中都包含了BUILD
文件。因此,对于Bazel
来说,整个工作区现在就包含了两个package:lib
和main
。lib/BUILD
文件长这样:
1 | cc_library( |
main/BUILD
文件长这样:
1 | cc_library( |
可以看出hello-world
这个mainpackage
中的target
依赖于lib
package中的hello-time
target(即target label
为://lib:hello-time
)- Bazel
是通过deps
这个属性知道自己的依赖项的。
注意到lib/BUILD
文件中我们将hello-time
这个target
显式可见了(通过visibility
属性)。这是因为默认情况下,targets
只对同一个BUILD
文件里的其他targets
可见(Bazel
使用target visibility
来防止像公有API
中库的实现细节的泄露等情况)。
好,让我们编译一下新的版本。进入到cpp-tutorial/stage3
目录下然后运行以下指令:
1 | bazel build //main:hello-world |
然后Bazel
又会有一些类似这样的输出:
1 | INFO: Found 1 target... |
现在又可以测试刚刚生成的二进制文件了:
1 | bazel-bin/main/hello-world |
好,现在我们学会了编译一个包含2个package
和3个target
的项目,并且理解了它们之前的依赖关系。