3. 编译工具链入门
好,我们进入第三章。说实话,很多刚接触Apollo的朋友,第一反应是「这项目怎么编译的?」,然后看到Bazel就懵了。别急,我当年也一样。这一章,我们就把它彻底讲透。
3.1 Bazel是什么?为什么Apollo选它?
Bazel,说白了就是一个「超级智能的构建工具」。你想想看,Apollo有几百个模块,每个模块又有自己的依赖关系。如果用Makefile,光是维护依赖就得疯掉。
Bazel的核心优势就三点:
- 增量编译:只重新编译你改过的文件。我第一次用的时候,改了一行代码,编译只花了3秒,当时就惊了。
- 沙箱隔离:每个编译任务都在独立环境里跑,不会互相污染。我在项目中遇到过因为环境变量冲突导致编译失败的问题,用Bazel后彻底解决了。
- 多语言支持:C++、Python、Java混着来,Bazel都能搞定。
重要概念:Bazel的工作空间(Workspace)就是你的项目根目录,里面必须有一个WORKSPACE文件。这个文件告诉Bazel:「嘿,这里是我的地盘」。
3.2 BUILD文件结构——模块的「身份证」
每个需要编译的目录下,都会有一个BUILD文件。它定义了该目录下有哪些编译目标(target),以及它们怎么编译。
来看一个典型的BUILD文件:
load("//tools:cpplint.bzl", "cpplint")
package(default_visibility = ["//visibility:public"])
cc_library(
name = "planning_common",
srcs = ["planning_common.cc"],
hdrs = ["planning_common.h"],
deps = [
"//modules/common:common",
"//modules/common/proto:common_proto",
"@com_google_protobuf//:protobuf",
],
tags = ["planning"],
)
cc_test(
name = "planning_common_test",
srcs = ["planning_common_test.cc"],
deps = [
":planning_common",
"@gtest//:gtest_main",
],
tags = ["planning", "unit_test"],
)
cpplint()
嗯,这里要注意几个关键字段:
- name:编译目标的名字,其他模块通过这个名字来引用它。
- srcs/hdrs:源文件和头文件列表。我建议把头文件也列出来,这样Bazel能更好地做依赖分析。
- deps:依赖列表,后面重点讲。
- tags:编译标签,用来分组和筛选。
个人习惯:我一般把BUILD文件里的name和目录名保持一致,这样找起来方便。比如modules/planning/common/目录下的库就叫planning_common。
3.3 依赖管理(deps)——模块间的「关系网」
依赖管理是Bazel最核心的功能。你想想看,一个模块要编译,它需要知道「我依赖谁,谁依赖我」。
依赖的写法有三种:
| 写法 | 含义 | 示例 |
|---|---|---|
//路径:目标 |
引用同一工作空间内的目标 | //modules/common:common |
:目标 |
引用同一BUILD文件内的目标 | :planning_common |
@仓库名//路径:目标 |
引用外部依赖(如第三方库) | @com_google_protobuf//:protobuf |
我曾经犯过一个错误:把依赖写成了循环依赖。比如A依赖B,B又依赖A。Bazel会直接报错,编译不过。所以,设计模块时一定要想清楚依赖方向。
避坑指南:我曾经在写deps时漏掉了一个头文件依赖,结果编译通过,但运行时疯狂报「undefined symbol」。排查了整整两天才发现是依赖没写全。所以,deps一定要写完整,不要偷懒。
3.4 编译标签(tags)——给目标打上「标签」
tags的作用很简单:给编译目标打上标签,方便批量操作。比如:
cc_library(
name = "perception_lidar",
...
tags = ["perception", "lidar", "gpu"],
)
然后你可以这样编译:
bazel build //modules/perception/... --build_tag_filters="gpu"
这条命令只编译所有带gpu标签的目标。我在项目中经常用这个功能来区分「CPU版本」和「GPU版本」的编译。
常用的标签约定:
manual:手动编译,不会被通配符匹配到unit_test:单元测试integration_test:集成测试gpu:需要GPU支持exclude_from_build:排除在常规编译之外
我建议:每个模块至少打两个标签——模块名和功能类型。比如planning和core,这样后期筛选起来特别方便。
3.5 知识体系总览
下面这张图,是我自己总结的Bazel编译知识体系。你看一眼,心里就有谱了。
这张图把Bazel的四个核心要素串起来了。你记住:WORKSPACE是地盘,BUILD是身份证,deps是关系网,tags是筛选器。搞懂这四样,Apollo编译你就入门了。
3.6 实战小技巧
最后,分享几个我在实际项目中常用的命令:
# 编译单个目标
bazel build //modules/planning:planning_common
# 编译整个目录
bazel build //modules/planning/...
# 只编译带特定标签的目标
bazel build //modules/... --build_tag_filters="gpu"
# 查看依赖树
bazel query 'deps(//modules/planning:planning_common)' --output graph
# 清理编译缓存(当遇到奇怪问题时试试)
bazel clean --expunge
注意:bazel clean --expunge会清空所有缓存,下次编译会从头开始。我一般只在「编译行为异常」时才用,平时用bazel clean就够了。
嗯,这一章的内容就到这里。Bazel其实没那么可怕,多用几次就熟了。记住,依赖写全、标签打对、缓存别乱清,这三点做到,编译基本不会出大问题。