第三章 编译工具链入门:Bazel基础概念、BUILD文件结构、依赖管理(deps)、编译标签(tags)

好,我们进入第三章。说实话,很多刚接触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编译知识体系 Bazel编译系统 WORKSPACE文件 定义工作空间根目录 BUILD文件 定义编译目标与规则 依赖管理(deps) 内部/外部依赖声明 编译标签(tags) 分组筛选与批量操作 cc_library / cc_binary cc_test / py_binary //内部路径 / @外部仓库 manual / gpu / unit_test 核心逻辑:WORKSPACE定义边界 → BUILD定义目标 → deps管理关系 → tags控制筛选

这张图把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其实没那么可怕,多用几次就熟了。记住,依赖写全、标签打对、缓存别乱清,这三点做到,编译基本不会出大问题。

可可精品资料库