本文大约需要40分钟学习时间,阅读理解即可,通过本文您将理解bazel构建项目如何组织项目结构,如何进行简单的和分模块的构建任务

官网:https://www.bazel.build
Github:](https://so.csdn.net/so/search?q=Github&spm=1001.2101.3001.7020):) https://github.com/bazelbuild/bazel](https://github.com/bazelbuild/bazel)

Bazel是一个类似于Make的编译工具,是Google为其内部软件开发的特点量身定制的工具,如今Google使用它来构建内部大多数的软件。Google认为直接用Makefile构建软件速度太慢,结果不可靠,所以构建了一个新的工具叫做Bazel,Bazel的规则层级更高。

一、bazel是用来干啥的

bazel简单来说就是用来自动化构建大型工程的,和make, maven之类的工具属于一类东西,他的目标是当项目里的一部分程序修改后,只需要几个命令就可以将整个项目进行重新编译,且要求快。它还可以进行自动化测试,当然这些都必须由你来编写测试流程和规则。因为我们项目用的c++,而且bazel对c++的支持很完善,所以我的整个系列都只针对c++来说。

二、bazel的文件管理架构

bazel中对于文件架构的概念有两个:workspace和package。

workspace是表示整个项目的,也叫repo,必须在项目的根目录下建一个WORKSPACE文件来定义项目的根目录,bazel会忽略所有项目子目录下的WORKSPACE文件。

package是项目中的模块,也就是一个一个包,包在组织上比较随意,可以根据项目需求来定,你想哪个文件夹中的东西成为一个包,就在那个文件夹的目录里创建一个BUILD文件即可,包的管理范围包括子目录里的东西,但不包括子包所包括的内容。比如:

  1. /project/
  2. WORKSPACE
  3. lib/
  4. BUILD
  5. ...
  6. src/
  7. BUILD
  8. ...
  9. other1/
  10. BUILD
  11. other2/
  12. ...

则lib, src以及other1都是packages,但other1目录里的东西不属于src包,但other2里的东西都属于src包。包和包之间的内容不重叠,不包含。
引用一个包写全了是这样的:

@[project_name]//[path/to/package]

比如上面的三个包:

@project//lib @project//src @project//src/other1

后面会提到其他简化写法,但我觉得一开始明白这个完整形态是很重要的。

三、bazel的运行架构

首先明白bazel的运行架构我觉得会让学习bazel做到心中有数。bazel自动化构建代码时大体分为三个步骤:

加载阶段:load phase
分析阶段:analysis phase
执行阶段:execution phase
加载阶段:
bazel在构建时我认为只会执行BUILD文件,这个文件可以理解成一个脚本,指导bazel如何去构建这个package中的代码,将他们变成二进制可执行文件。但bazel的文件其实不止只有WORKSPACE和BUILD,还有很多.bzl文件,这些文件可以理解成类似于c里面的头文件一样,为了让BUILD文件看起来简洁,工整,所以把很多其他东西以及定义之类的内容,扔到了.bzl文件里,然后在BUILD文件开始处通过一个load命令将需要的东西包含进来,类似于#include,所以开发bazel的人限制了BUILD文件中可以使用的语法,而在.bzl文件中没有限制。

以上说的这些废话,就是在加载阶段完成的,加载阶段做的就是替换代码,展开代码,类似于#include干的事,还有一些比如宏(Macro)展开之类的事情也在这一阶段完成,宏在后面会说。

分析阶段:
加载阶段结束后,我觉得此时可以认为我们只有BUILD文件需要考虑了,而且是一个展开完全的BUILD文件,这其中都是一个一个的函数调用,每一个函数调用定义一个对象,每一个对象都包含一个label(这个label的概念很重要,他是bazel的一种基本数据结构,不是string那么简单),由name域的传参决定。这些对象功能各样,有的是定义了一个行为(称为rule,规则),有的是定义了一个文件或一组文件,有的定义了一组配置等等,后面细说。类似如下,不同的函数需要的参数也是不同的:

  1. cc_binary(name="hahaha",srcs=["hello.cc"],deps=[":enenen"])
  2. cc_library(name="enenen",srcs=[......],hdrs=[......])
  3. config_setting(name="x86",values={"cpu","x86"})

分析阶段就是来执行这些函数,生成(我的理解)一个静态有向图,就好像tensorflow那个静态图的概念,每一个函数都生成静态图中的一个结点,根据结点中定义的label和其他信息,构建结点之间的依赖关系。比如上面那个”hahaha”是个label,“enenen”也是一个label,然后”hahaha”依赖于”enenen”。

分析阶段只是建立这种依赖关系的有向图,定义这些对象,但不会确切的执行任何构建工作,它只是分配了工作顺序,就好像一个工厂把人员各就各位了,但大家都没开始工作,等到execution phase,大家一起开工。

四、bazel实战

Install

安装过程请参考:http://bazel.io/docs/install.html](http://bazel.io/docs/install.html)

建立工作区(workspace)

Bazel的编译是基于工作区(workspace)的概念。工作区是一个存放了所有源代码和Bazel编译输出文件的目录,也就是整个项目的根目录。同时它也包含一些Bazel认识的文件:

  1. WORKSPACE文件,用于指定当前文件夹就是一个Bazel的工作区。所以WORKSPACE文件总是存在于项目的根目录下。
    2. 一个或多个BUILD文件,用于告诉Bazel怎么构建项目的不同部分。(如果工作区中的一个目录包含BUILD文件,那么它就是一个package。)

那么要指定一个目录为Bazel的工作区,就只要在该目录下创建一个空的WORKSPACE文件即可。
当Bazel编译项目时,所有的输入和依赖项都必须在同一个工作区。属于不同工作区的文件,除非linked否则彼此独立。

使用Bazel编译项目

Bazel提供了一些编译的例子,在https://github.com/bazelbuild/examples/](https://github.com/bazelbuild/examples/),可以clone到本地试一下。其中examples/cpp-tutorial目录下包含了这么些文件:

  1. examples
  2. └── cpp-tutorial
  3. ├──stage1
  4. └── main
  5. ├── BUILD
  6. ├── hello-world.cc
  7. └── WORKSPACE
  8. ├──stage2
  9. ├── main
  10. ├── BUILD
  11. ├── hello-world.cc
  12. ├── hello-greet.cc
  13. ├── hello-greet.h
  14. └── WORKSPACE
  15. └──stage3
  16. ├── main
  17. ├── BUILD
  18. ├── hello-world.cc
  19. ├── hello-greet.cc
  20. └── hello-greet.h
  21. ├── lib
  22. ├── BUILD
  23. ├── hello-time.cc
  24. └── hello-time.h
  25. └── WORKSPACE

可以看到分成了3组文件,分别对应本文中的3个例子。在第一个例子中,我们首先学习如何构建单个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的输出结构。

现在你可以测试你刚刚生成的二进制文件了:

bazel-bin/main/hello-world

2. 查看依赖图

一个成功的build将所有的依赖都显式定义在了BUILD文件中。Bazel使用这些定义来创建项目的依赖图,这能够加速编译的过程。

让我们来可视化一下我们项目的依赖吧。首先,生成依赖图的一段文字描述(即在工作区根目录下运行下述指令):

bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' \
  --output graph

这个指令告诉Bazel查找target //main:hello-world的所有依赖项(不包括host和隐式依赖),然后输出图的文字描述。再把文字描述贴到 GraphViz 里,你就可以看到如下的依赖图了。可以看出这个项目是用单个源文件编译出的单个target,并没有别的依赖。
image.png
好的,到目前为止,我们已经建立了工作区,编译了一个项目,并且查看了它的依赖。接下来让我们加点难度。

3. 多个target的编译

单个target的方式对于小项目来说是高效的,但是对于大项目来说,你可能会想把它拆分成多个target和多个package来实现快速增量的编译(这样就只需要重新编译改变过的部分)。

首先我们来尝试着把项目拆分成两个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这个库。

好,让我们编译一下新的版本。进入到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

现在又可以测试刚刚生成的二进制文件了:

bazel-bin/main/hello-world

注意,如果你现在修改一下hello-greet.cc然后重新编译整个项目的话,Bazel其实只会编译修改过的那个文件。

然后我们再来看一下依赖图,发现hello-world在编译时候的结构和之前有所不同,现在是有两个targets。hello-world这个target从一个源文件编译而来,同时依赖于另一个target//main:hello-greet,这个target又是从两个源文件编译而来。
image.png

4. 多个package的编译

我们现在再将项目拆分成多个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:libmain

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",
    ],
)

可以看出hello-world这个mainpackage中的target依赖于lib package中的hello-time target(即target label为://lib:hello-time)- Bazel是通过deps这个属性知道自己的依赖项的。那么现在依赖图就变成了下图的样子:

image.png

注意到lib/BUILD文件中我们将hello-time这个target显式可见了(通过visibility属性)。这是因为默认情况下,targets只对同一个BUILD文件里的其他targets可见(Bazel使用target visibility来防止像公有API中库的实现细节的泄露等情况)。

好,让我们编译一下新的版本。进入到cpp-tutorial/stage3目录下然后运行以下指令:

bazel build //main:hello-world

然后Bazel又会有一些类似这样的输出:

NFO: Found 1 target...
Target 
  bazel-bin/main/hello-world
INFO: Elapsed time: 0.167s, Critical Path: 0.00s

现在又可以测试刚刚生成的二进制文件了:

bazel-bin/main/hello-world

好,现在我们学会了编译一个包含2个package和3个target的项目,并且理解了它们之前的依赖关系。

Reference

  1. Google开源构建工具Bazel](http://www.infoq.com/cn/news/2015/03/google-open-source-bazel))
    2. Introduction to Bazel: Build a C++ Project](https://docs.bazel.build/versions/master/tutorial/cpp.html))

  2. https://blog.csdn.net/elaine_bao/article/details/78668657