支持原创,转载请附上原文链接
keywords
- bazel c++ 入门
- bazel 库依赖的多种方式
- bazel 全局配置
0 引言
大型工程编译采用的是cmake,存在如下问题:
- 编译时间长
- cmake过于依赖开发人员能正确编写规则,结果不可靠
综上,cmake会影响研发效率和公司的敏捷性。基于此背景,决定探索bazel用于复杂工程的构建编译,对比两者性能。
1 bazel 之 c++
1.1 bazel 特性
Bazel 是一个类似于 Make 的工具,是 Google 为其内部软件开发的特点量身定制的工具,如今 Google 使用它来构建内部大多数的软件。它的功能有诸多亮点:
- 多语言支持:目前 Bazel 默认支持 Java、Objective-C 和 C++,但可以被扩展到其他任何变成语言。
- 高级构建描述语言:项目是使用一种叫 BUILD 的语言来描述的,它是一种简洁的文本语言,它把一个项目视为一个集合,这个集合由一些互相关联的库、二进制文件和测试用例组成。相反,像 Make 这样的工具,需要去描述每个文件如何调用编译器。
- 多平台支持:同一套工具和相同的 BUILD 文件可以用来为不同的体系结构构建软件,甚至是不同的平台。在 Google,Bazel 被同时用在数据中心系统中的服务器应用和手机端的移动应用上。
- 可重复性:在 BUILD 文件中,每个库、测试用例和二进制文件都需要明确指定它们的依赖关系。当一个源码文件被修改时,Bazel 凭这些依赖来判断哪些部分需要重新构建,以及哪些任务可以并行进行。这意味着所有构建都是增量的,并且相同构建总是产生一样的结果。
- 可伸缩性:Bazel 可以处理大型项目;在 Google,一个服务器软件有十万行代码是很常见的,在什么都不改的前提下重新构建这样一个项目,大概只需要 200 毫秒。
1.2 bazel 快速入门
1.2.1 安装
参见官方安装文档安装即可
安装完成后,执行
bazel version
可见如下输出
Build label: 0.13.0
Build target: bazel-out/k8-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
Build time: Mon Oct 18 21:33:40 +50297 (1525078013620)
Build timestamp: 1525078013620
Build timestamp as int: 1525078013620
1.2.2 第一个bazel工程
首先,我们来通过第一个工程进入bazel的世界。文件的目录结构如下:
.
├── src
│ ├── BUILD
│ └── main.cc
└── WORKSPACE
其中,WORKSPACE 是一个空文件,但是该文件必须存在
- src/BUILD
cc_binary(
name = "hello-bazel",
srcs = ["main.cc"]
)
- src/main.cc
#include <iostream>
int main() {
std::cout << "hello bazel!!!" << std::endl;
return 0;
}
此时可执行如下指令进行编译:
bazel build //src:hello-bazel # 1:指定编译src package 下面的target hello-bazel
bazel build src:hello-bazel # 2: 与1等效
bazel build ... # 3:编译所有target
此外bazel支持编译同时执行(run时,只能指定一个target):
bazel run //src:hello-bazel # 1:指定编译并执行src package 下面的target hello-bazel
bazel run src:hello-bazel # 2: 与1等效
其他bazel相关的指令,参见官网手册
1.2.3 bazel 工程结构介绍
从第一个bazel工程可以看出,bazel工程包含两个文件:
- WORKSPACE
- BUILD
下面对这2个文件进行介绍。
WORKSPACE
用于指定当前文件夹是一个bazel的工作区。WORKSPACE所在的目录就是项目的根目录。几个注意事项如下:
- WORKSPACE 文件可以为空,但是必须存在
- WORKSPACE不支持嵌套,bazel会自动忽略子目录的WORKSPACE文件
BUILD
BUILD文件的核心就是告诉bazel按照用户的需求进行编译。BUILD的一条编译指令成为一个target,每个target的name是不可省略的,上面的hello-bazel就是一个target
1.2.4. WORKSPACE 规则
上面提高过WORKSPACE文件的目的是定义bazel工程根目录,除此之外,WORKSPACE还用于拉取外部依赖项,下面介绍WORKSPACE相关的一些规则。
依赖项获取相关规则:
- local_repository
- new_local_repository
- git_repository
- new_git_repository
- http_archive
- http_file
参考文档:
工作区规则
Starlark 工作区规则
上述指令主要用于获取外部依赖文件。不同指令具有不同的功能,具体参见上面的参考文件。
加载扩展.bzl规则:
- load
对于上述依赖项获取相关的规则,工作区规则对应的指令,可以不执行load,比如:local_repository与new_local_repository,但是对于Starlark需要load后才能执行:
- git_repository:load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
- new_git_repository:load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository")
- http_archive:load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
- http_file:load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
1.2.5 BUILD 规则
BUILD 主要是按照预期的规则执行编译,生成二进制文件。对于C/C++,常用的库规则:
- cc_binary
- cc_import
- cc_library
- cc_proto_library
- fdo_prefetch_hints
- fdo_profile
- propeller_optimize
- cc_test
- cc_工具链
- cc_toolchain_suite
参考文档:
Bazel 构建函数百科全书
1.3 bazel 进阶 之 依赖
我们工程肯定不会是像上文demo那样的简单工程,完全没有依赖。接下来我们就探索bazel是如何解决工程依赖的。
1.3.1 同bazel工程
依赖同一个bazel工程的其他target产出,这个是比较简单的。我们同样基于上面demo进行修改,文件目录结构如下:
├── lib
│ ├── BUILD
│ └── print.h
├── src
│ ├── BUILD
│ └── main.cc
└── WORKSPACE
其中文件内容如下(默认规则:如果未提及,表示内容为空或者上文出现后未更改)
- lib/BUILD
cc_library(
name = "print",
hdrs = ["print.h"],
visibility = ["//visibility:public"],
)
- lib/print.h
#pragma once
#include <iostream>
static void print(const std::string & msg){
std::cout << "[I] " << msg << std::endl;
}
- src/BUILD
cc_binary(
name = "hello-bazel",
srcs = ["main.cc"],
deps = [
"//lib:print",
],
)
- src/main.cc
#include <iostream>
#include "lib/print.h"
int main() {
std::cout << "hello bazel!!!" << std::endl;
print("hello bazel!!!");
return 0;
}
1.3.2 本地bazel工程
本地不同bazel工程之间的依赖,目录结构以及文件内容如下
.
├── sub1
│ ├── src
│ │ ├── BUILD
│ │ └── main.cc
│ └── WORKSPACE
└── sub2
├── lib
│ ├── BUILD
│ └── print.h
└── WORKSPACE
- sub1/WORKSPACE
local_repository(
name = "sub2",
path = "../sub2",
)
- sub1/src/BUILD
cc_binary(
name = "hello-bazel",
srcs = ["main.cc"],
deps = [
"@sub2//lib:print",
],
)
- sub1/src/main.cc
#include <iostream>
#include "lib/print.h"
int main() {
std::cout << "hello bazel!!!" << std::endl;
print("hello bazel!!!");
return 0;
}
- sub2/lib/BUILD
cc_library(
name = "print",
hdrs = ["print.h"],
visibility = ["//visibility:public"],
)
1.3.3 本地非bazel工程
.
├── sub1
│ ├── src
│ │ ├── BUILD
│ │ └── main.cc
│ ├── sub2.BUILD
│ └── WORKSPACE
└── sub2
└── lib
└── print.h
- sub1/WORKSPACE
new_local_repository(
name = "sub2",
path = "../sub2",
build_file = "sub2.BUILD",
)
- sub1/src/BUILD
cc_binary(
name = "hello-bazel",
srcs = ["main.cc"],
deps = [
"@sub2//:print",
],
)
- sub1/sub2.BUILD
cc_library(
name = "print",
hdrs = glob(["lib/**"]),
visibility = ["//visibility:public"],
)
1.3.4 本地依赖库
该项测试前,需要先利用bazel生成依赖库,目录结构如下。其中sub2是生成依赖库的bazel工程;sub1是调用依赖库的工程
.
├── sub1
│ ├── src
│ │ ├── BUILD
│ │ ├── deps
│ │ │ ├── inc
│ │ │ │ └── print.h
│ │ │ └── so
│ │ │ └── libprint.so
│ │ └── main.cc
│ └── WORKSPACE
└── sub2
├── lib
│ ├── BUILD
│ ├── print.cc
│ └── print.h
└── WORKSPACE
先介绍sub2,动态库的生成工程,内容如下:
- sub2/lib/BUILD
cc_library(
name = "print",
srcs = glob(["*.cc"]),
hdrs = glob(["*.h"]),
linkstatic = False,
)
- sub2/lib/print.h
#pragma once
#include <iostream>
#include <string>
void print(const std::string & msg);
- sub2/lib/print.cc
#include "print.h"
void print(const std::string& msg){
std::cout << "[I] " << msg << std::endl;
return;
}
编译后,将动态库和头文件拷贝到sub1目录,目录结构如上。sub1文件内容如下:
- sub1/src/BUILD
cc_import(
name = "print",
hdrs = ["deps/inc/print.h"],
shared_library = "deps/so/libprint.so",
)
cc_binary(
name = "hello-bazel",
srcs = ["main.cc"],
deps = [
":print",
],
copts = ["-I deps/inc"],
)
- sub1/src/main.cc
#include <iostream>
#include "deps/inc/print.h"
int main() {
std::cout << "hello bazel!!!" << std::endl;
print("hello bazel!!!");
return 0;
}
这里存在的一个问题是:引入的头文件和so需要放在引用的package下面(类似的问题 issues #9965)。
这样肯定是不会满足我们实际需求的,参考Practical Bazel: Depending on a System-Provided C/C++ Library,得到新的方案如下:
.
├── sub1
│ ├── deps
│ │ ├── include
│ │ │ └── print.h
│ │ └── lib
│ │ └── libprint.so
│ ├── src
│ │ ├── BUILD
│ │ └── main.cc
│ ├── third_party
│ │ └── myprint
│ │ └── myprint.BUILD
│ └── WORKSPACE
└── sub2
├── lib
│ ├── BUILD
│ ├── print.cc
│ └── print.h
└── WORKSPACE
sub2目录没有变化,不再介绍。接下面介绍sub1的文件内容
- WORKSPACE
new_local_repository(
name = "libprint",
path = "deps",
build_file = "third_party/myprint/myprint.BUILD",
)
- third_party/myprint/myprint.BUILD
cc_import(
name = "libprint",
hdrs = glob(["include/*.h"]),
shared_library = "lib/libprint.so",
visibility = ["//visibility:public"],
)
- src/BUILD
cc_binary(
name = "hello-bazel",
srcs = ["main.cc"],
deps = [
"@libprint",
],
)
- src/main.cc
#include <iostream>
#include "include/print.h"
int main() {
std::cout << "hello bazel!!!" << std::endl;
print("hello bazel!!!");
return 0;
}
1.3.5 远程bazel工程
.
├── BUILD
├── main.cc
└── WORKSPACE
- WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "gtest",
sha256 = "9dc9157a9a1551ec7a7e43daea9a694a0bb5fb8bec81235d8a1e6ef64c716dcb",
strip_prefix = "googletest-release-1.10.0",
urls = [
"https://apollo-system.cdn.bcebos.com/archive/6.0/release-1.10.0.tar.gz",
"https://github.com/google/googletest/archive/release-1.10.0.tar.gz",
],
)
- BUILD
cc_test(
name = "gtest",
srcs = ["main.cc"],
copts = ["-Iexternal/gtest/include"],
deps = [
"@gtest//:gtest_main",
],
)
1.3.6 远程非bazel工程
.
├── BUILD
├── gtest.BUILD
├── main.cc
└── WORKSPACE
- BUILD
cc_test(
name = "gtest",
srcs = ["main.cc"],
copts = ["-Iexternal/gtest/include"],
deps = [
"@gtest//:main",
],
)
- gtest.BUILD
cc_library(
name = "main",
srcs = glob(
["src/*.cc"],
exclude = ["src/gtest-all.cc"]
),
hdrs = glob([
"include/**/*.h",
"src/*.h"
]),
copts = ["-Iexternal/gtest/include"],
linkopts = ["-pthread"],
visibility = ["//visibility:public"],
)
- main.cc
#include "gtest/gtest.h"
int abs(int x) {
return x >= 0? x : -x;
}
TEST(BazelTest, AbsTest) {
EXPECT_EQ(abs(-1), 1);
}
- WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "gtest",
url = "https://github.com/google/googletest/archive/release-1.7.0.zip",
sha256 = "b58cb7547a28b2c718d1e38aee18a3659c9e3ff52440297e965f5edffe34b6d0",
build_file = "@//:gtest.BUILD",
strip_prefix = "googletest-release-1.7.0",
)
官方建议:尽量使用http_archive 替代 git_repository 以及 new_git_repository,原因如下:
- Git repository rules depend on system git(1) whereas the HTTP downloader is built into Bazel and has no system dependencies.
- http_archive supports a list of urls as mirrors, and git_repository supports only a single remote
- http_archive works with the repository cache, but not git_repository. See #5116 for more information.
1.4 全局配置
针对每个target,我们可以采用copts参数来配置编译参数,但是如果有些参数需要全局配置时,通过copts就比较麻烦。
针对这种场景,bazel提供了一个方案:
在根目录创建.bazelrc文件,比如配置c++11作为c++标准
build --copt=-std=c++11
3 bazel经验总结
对于使用者来说,bazel相比于cmake更加简单,可配置性和灵活性更加受限,也是基于这些特性,在编译构建复杂工程的时候,bazel对于编码规范要求更加严格。
下面从原则(必须遵守)和经验(有了会更优)介绍(持续更新)
3.1 原则
- 编码一定要注意回环调用(同层级之间相互调用或者下层调用上层),否则编译出错
3.2 经验
- 为了发挥bazel的优势,并发编译,target划分的尽量小
99 参考
Practical Bazel: Depending on a System-Provided C/C++ Library