Bazel介绍
Bazel是一个类似于Make的编译工具,是Google为其内部软件开发的特点量身定制的工具,现在Google内部大部分软件都用Bazel进行构建。
安装
安装过程参考:http://bazel.io/docs/install.html
主要特点
- 多语言支持:Bazel支持Java,Objective-C和C++,可以扩展来支持任意的编程语言
- 高级别的构建语言:工程是通过BUILD语言来描述的。BUILD语言以简洁的文本格式,描述了由多个小的互相关联的库、二进制程序和测试程序来组成的一个项目。而与之相比,Make这类的工具需要描述各个单独的文件和编译的命令
- 多平台支持:同一套工具和同样的BUILD文件可以用来构建不同架构和不同平台的软件
- 可重现性:在BUILD文件中,每个库,测试程序,二进制文件必须明确完整地指定直接依赖。当修改源代码文件后,Bazel使用这个依赖信息就可以知道哪些必须重新构建,哪些任务可以并行执行。这意味者所有的构建都是增量形式的并能够每次都生成相同的结果
- 可扩展性:Bazel可以处理巨大的构建,并且可以扩展支持新语言和新平台
- 编译迅速:Bazel可以成倍提高构建速度,对依赖关系进行优化,支持并发执行;只重新编译修改的文件
- 可伸缩性:可处理任意大小的代码库,并且库可以任意增加或缩减
适用项目:
- 有庞大代码库的项目
- 用多种语言写的项目
- 在多平台上部署的项目
- 有大量测试的工程
项目结构
Bazel工程的顶层为根目录,即工作区间Workspace;Workspace下包含多个Package,而每个Package又包含多个编译目标Target。
- Workspace
Bazel的编译是基于Workspace的概念,工作区存放了所有源代码和Bazel编译输出的文件。
Bazel工程的根目录下,必须包含一个文件名为Workspace的文件,它用来指明构建的根目录,Workspace采用类似于Python的语法。 - Package
Workspace下可以有多个Package,每个Package都可以作为一个子模块,并且每个Package下必须包含一个BUILD文件,用来指定Package编译构建的规则。 - Target
Target是Bazel的最小组成
Bazel编译Demo
Bazel提供了一些编译的例子,在Github上可以找到: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
example一共分成了三组文件,分别对应以下列举的三个例子。第一个例子:学习如何构建单个Package中的单个Target;第二个例子:将整个项目拆分成单个Package的多个Target。第三个例子:将项目拆分成多个Package,用多个Target编译。
理解BUILD文件
一个BUILD文件包含多种指令。其中最重要的是编译指令,它告诉Bazel如何编译想要的输出,比如可执行二进制文件或库。BUILD文件中的每一条编译指令被称为一个Target,它指向一系列的源文件和依赖,一个Target也可以指向别的Target。
举个简单例子:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
一个名为“hello-world”的Target使用了Bazel内置的cc_binary编译指令,从hello-world.cc源文件(没有其他依赖)构建了一个可执行二进制文件。
第一个例子
首先通过下面的命令来编译该例子:
bazel build //main:hello-world
Target中的//main:
是BUILD文件相对于WORKSPACE文件的位置,hello-world
是我们在BUILD文件中命名号的Target名字。
输出以下信息表示编译成功:
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
Bazal将编译的输出放在项目根目录下bazel-bin
目录.
查看依赖图
成功地构建一个项目,需要将项目所有的依赖都定义在BUILD文件内,Bazel可以通过命令生成可视化关系图,来检查这些依赖项.
以下是检查该项目的依赖命令:
bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
--output graph
这个命令意在查找Target//main:hello-world
的所有依赖项(不包括host和隐式依赖),输出结果为输出图的文字描述,通过粘贴文字描述到GraphViz,就可以查看到如下的依赖图:
第二个例子
单个Target编译对于小项目来说非常高效,但是对于大项目而言,将项目拆分成多个Target和多个Package更加高效,因为Bazel能够实现快速增量的编译.
第二个例子将项目拆分成两个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
这个库。
接下来再来看看项目生成的依赖图:
hello-world
在编译时候的结构和例子1有所不同,现在是有两个Targets。hello-world
这个Target从一个源文件编译而来,同时依赖于另一个Target//main:hello-greet
,这个Target又是从两个源文件编译而来。
第三个例子
第三个例子将项目拆分成多个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:lib
和main
,两个Package的BUILD文件如下:
lib/BUILD:
cc_library(
name = "hello-time",
srcs = ["hello-time.cc"],
hdrs = ["hello-time.h"],
visibility = ["//main:__pkg__"],
)
注意到这个Target显式可见了(通过visibility
属性)。这是因为默认情况下,Targets只对同一个BUILD文件里的其他Targets可见(Bazel使用Target visibility来防止像公有API中库的实现细节的泄露等情况)。
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
这个main
Package中的Target依赖于lib
Package中的hello-time
Target(即Target label为://lib:hello-time
).那么现在依赖图就变成了下图的样子: