bazel c++ 编译入门


支持原创,转载请附上原文链接


keywords

  1. bazel c++ 入门
  2. bazel 库依赖的多种方式
  3. 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所在的目录就是项目的根目录。几个注意事项如下:

  1. WORKSPACE 文件可以为空,但是必须存在
  2. 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 原则

  1. 编码一定要注意回环调用(同层级之间相互调用或者下层调用上层),否则编译出错

3.2 经验

  1. 为了发挥bazel的优势,并发编译,target划分的尽量小

99 参考

官网手册

官方安装文档

工作区规则

Starlark 工作区规则

Bazel 构建函数百科全书

官方参考文档

Practical Bazel: Depending on a System-Provided C/C++ Library

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354