Bazel C++ 首次构建实战教程:从单一 Target 到多 Package 的完整指南
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
本教程是 Bazel 官方 First Build 系列的核心入门路径,以 C++ 项目为载体,带你零基础跑通 Bazel 的核心工作流:理解工作区(workspace)、BUILD文件、包(package)与目标(target)的关系,并分三个阶段(单目标 → 多目标 → 多包)亲手完成三次构建。读完本教程,你将掌握bazel build的基本用法、cc_binary/cc_library规则的配置方式、deps依赖声明与visibility可见性控制的实战技巧,并能在本仓库的 examples/cpp 示例中直接复现验证。全程约需 30 分钟。
准备工作:安装 Bazel 并获取示例工程
如果你还没有安装 Bazel,请先参照 安装指南 完成安装。由于本教程使用 Git 管理示例代码,建议同时安装 Git。然后克隆官方示例仓库,获取教程所需的样例工程:
git clone https://github.com/bazelbuild/examples示例工程位于examples/cpp-tutorial目录,其目录结构如下:
examples └── cpp-tutorial ├──stage1 │ ├── main │ │ ├── BUILD │ │ └── hello-world.cc │ └── MODULE.bazel ├──stage2 │ ├── main │ │ ├── BUILD │ │ ├── hello-world.cc │ │ ├── hello-greet.cc │ │ └── hello-greet.h │ └── MODULE.bazel └──stage3 ├── main │ ├── BUILD │ ├── hello-world.cc │ ├── hello-greet.cc │ └── hello-greet.h ├── lib │ ├── BUILD │ ├── hello-time.cc │ └── hello-time.h └── MODULE.bazel三组文件分别对应教程的三个阶段:第一阶段在单一包(package)中构建单一目标(target);第二阶段在单一包中同时构建一个二进制目标和一个库目标;第三阶段构建包含多个包、多个目标的项目。三个阶段中MODULE.bazel均已就绪,无需手动创建。
核心概念:Workspace、MODULE.bazel 与 BUILD 文件
在动手构建之前,先建立三个贯穿全程的关键概念。
工作区(workspace)是存放项目源代码和 Bazel 构建产物的目录。它包含两类关键文件:
MODULE.bazel:位于项目根目录,用于标识该目录是一个 Bazel 工作区,同时也是声明外部依赖的地方(Bazel 7 起默认启用 Bzlmod 模块系统,由该文件驱动)。在后续项目中,只需在目录中创建一个空的MODULE.bazel文件即可将其标记为工作区。BUILD文件:告诉 Bazel 如何构建项目的不同部分。工作区内包含BUILD文件的目录即为一个包(package)。
以本仓库为例,根目录的 MODULE.bazel 中通过bazel_dep(name = "rules_cc", version = "0.2.19")声明了 C++ 构建规则依赖,说明当前 Bazel 的 C++ 规则由rules_cc模块提供;examples/cpp/BUILD 则通过load("@rules_cc//cc:cc_binary.bzl", "cc_binary")等语句导入规则后使用。
理解 BUILD 文件:BUILD文件包含多种指令,其中最关键的是规则(rule)——一组告诉 Bazel "如何构建你想要的输出"(如可执行二进制、库)的指令。规则在BUILD文件中的每一次实例化被称为目标(target),它指向一组特定的源文件与依赖(dependency),也可以指向其他目标。
查看cpp-tutorial/stage1/main/BUILD:
cc_binary( name = "hello-world", srcs = ["hello-world.cc"], )hello-world目标实例化了 Bazel 内置的cc_binary规则,告诉 Bazel 从hello-world.cc这一个无依赖的源文件构建一个自包含的可执行二进制。从源码结构看,这一模式与本仓库 examples/cpp/BUILD 中cc_binary(name = "hello-world", srcs = ["hello-world.cc"], deps = [":hello-lib"])的写法一脉相承,只是后者额外声明了对hello-lib库的依赖。
阶段一:单一目标、单一包
进入cpp-tutorial/stage1目录并执行首次构建:
cd cpp-tutorial/stage1 bazel build //main:hello-world这里的目标标签(target label)//main:hello-world中,//main:表示BUILD文件相对于工作区根目录的位置,hello-world是BUILD文件中的目标名。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-bin目录。运行刚构建好的二进制:
bazel-bin/main/hello-world终端将打印Hello world。阶段一的依赖图只有一个目标、一个源文件:
cpp-tutorial-stage1.png 展示了 hello-world 的依赖图:单一目标对应单一源文件
至此,你已完成第一次 Bazel 构建,掌握了基本构建流程。参照本仓库的 examples/cpp/hello-world.cc,可以看到真实项目中二进制入口的写法:main函数接收命令行参数,调用库对象的greet方法打印问候语,并通过退出码(始终为 0)向 Bazel 汇报"测试通过"。
阶段二:多个构建目标
对于稍大的项目,单一目标往往不够用。将大项目拆分为多个目标与包,可以带来两个关键收益:增量构建——Bazel 只重新构建发生变化的部分;并行加速——一次构建多个部分。本阶段新增一个目标,下一阶段再新增一个包。
查看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", ], )Bazel 会先用内置的cc_library规则构建hello-greet库,再构建hello-world二进制。hello-world目标中的deps属性告诉 Bazel:构建hello-world需要hello-greet库。注意deps中:hello-greet的写法——前导冒号表示引用同一 BUILD 文件内的目标。
切换到 stage2 目录并构建:
cd ../stage2 bazel build //main:hello-world输出同样指向bazel-bin/main/hello-world,运行后依旧打印Hello world。此时若修改hello-greet.cc再重新构建,Bazel 只会重新编译该文件,这正是增量构建的直观体现。
从依赖图可以看到hello-world比阶段一多了一个名为hello-greet的输入:
cpp-tutorial-stage2.png 展示了 hello-world 的依赖图:二进制目标依赖 hello-greet 库目标
本阶段你已用两个目标完成构建:hello-world编译一个源文件并依赖//main:hello-greet,后者编译两个额外源文件。这一"库 + 二进制"的组织方式与本仓库 examples/cpp/BUILD 中hello-lib(cc_library,含srcs与hdrs)加hello-world(cc_binary,deps引用:hello-lib)的结构完全一致,hello-lib.h 与 hello-lib.cc 就是可对照阅读的真实实现。
阶段三:多个包
本阶段引入多包构建,也是教程中复杂度最高的一层。stage3目录下有两个子目录,各自包含BUILD文件,因此 Bazel 视角下工作区存在两个包:lib和main。
查看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", ], )main包中的hello-world目标通过deps依赖lib包中的hello-time目标,标签写作//lib:hello-time——不带冒号前缀的//lib:用于引用其他包中的目标。这一依赖关系同样体现在依赖图中:
cpp-tutorial-stage3.png 展示了 hello-world 的依赖图:main 包中的目标依赖 lib 包中的目标
这里出现了一个新概念:可见性(visibility)。默认情况下,目标只对同一BUILD文件内的其他目标可见。因此为了让lib/BUILD中的//lib:hello-time能被main/BUILD中的目标使用,必须用visibility = ["//main:__pkg__"]属性显式放行——//main:__pkg__表示"对main包内的所有目标可见"。Bazel 通过目标可见性机制,防止库的内部实现细节泄露进公共 API,避免跨包误用。关于可见性的更多规则与取值(如//visibility:public、//visibility:private),可参阅 可见性概念文档。
切换到 stage3 目录完成最终构建:
cd ../stage3 bazel build //main:hello-world运行构建产物,得到最后一次Hello world:
bazel-bin/main/hello-world至此你完成了"两个包、三个目标"的多包构建,理解了目标之间的依赖关系。关于标签与依赖的底层约定,可进一步阅读 标签概念文档 与 依赖概念文档。
仓库源码印证:从教程到真实工程
教程的三阶段演进并非纸上谈兵,本仓库 examples/cpp 就是其真实落地的进阶版:
- 规则导入方式:教程中直接使用内置规则名,而真实工程通过
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")显式导入cc_binary、cc_library、cc_test,因为自 Bazel 7 起 C++ 规则已外置到rules_cc模块(见根目录 MODULE.bazel 中的bazel_dep(name = "rules_cc", version = "0.2.19"))。 - 测试目标:
examples/cpp/BUILD中还有cc_test目标,如hello-success_test(直接用hello-world.cc当测试源,靠退出码判定通过)与故意失败的hello-fail_test(打了tags = ["manual"]标记),演示了 Bazel 测试与二进制的同构关系——这与教程"单一目标"思想一脉相承。 - 数据文件与运行文件:
runfile目标展示了data属性与@rules_cc//cc/runfiles的用法,runfile.cc 中通过Runfiles::Rlocation在运行时定位数据文件,属于教程之外的自然延伸。
下一步学习路径
至此,你已经完成 Bazel 的第一次完整构建。要继续深入,推荐以下路径:
- 继续聚焦 C++,阅读 C++ 常见构建用例;
- 尝试其他语言的入门教程:Java、Android 应用、iOS 应用;
- 了解本地与远程仓库的使用:外部依赖;
- 深入学习 Bazel 的其他规则体系:规则参考。
Happy building!
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考