构建工具(十一):构建系统全景图——从 make 到 Bazel 该怎么选

1、先讲个故事

有个人想煮一碗面。

  • 第一次,他烧了水、切了菜、下了面、调了酱。每一步都自己动手。这像手敲 gcc 命令
  • 后来他把步骤写在了便签上,每次照着做。这像写了个 shell 脚本
  • 再后来他发现,如果菜已经切好了,就不需要重新切。于是他在便签上加了判断。这像Makefile
  • 再后来他请了个厨师,只需要告诉厨师「我想吃面」,厨师自己去搞定一切。这像CMake
  • 后来他改造了厨房,让所有灶台能同时开火,一分钟出餐。这像Ninja
  • 最后他开了连锁面馆,不管在哪个城市、哪家店,面都是一个味道、一分钟上桌。这像Bazel

这个故事串起了我们这十一篇文章的脉络。最后一篇,我们来做一次全景回顾——站在高处看看这些工具之间的关系,以及面对一个新项目时该怎么选。

2、全景地图

层级     工具                        角色
────────────────────────────────────────────────
原始     gcc / g++ / clang           编译器
简单     Makefile / make              构建规则 + 执行引擎
高级     CMake / Meson / Bazel        构建描述(跨平台)
超快     Ninja                        执行引擎(极速)
辅助     pkg-config / vcpkg / Conan  依赖管理

编译器负责把源码变成机器码。它是地基。

构建工具负责组织编译过程——哪些文件要先编译,哪些可以并行,哪些需要重新编译。构建工具的核心问题是管理依赖关系

按这个维度,构建工具可以分成两个角色:

  • 构建描述者:你描述你的项目结构、依赖关系(CMake、Meson、Bazel)
  • 构建执行者:执行具体的编译命令,按依赖关系调度(make、Ninja)

有些工具两者都做(make 既是描述者又是执行者),有些只做执行(Ninja 需要一个描述者来生成构建文件),有些做得特别重(Bazel 连编译器版本都替你锁死)。

3、各工具一句话介绍

GCC / Clang 编译器的本质。gcc -c file.c 是最原始的构建单元。理解编译四阶段就有了地基。

Make / Makefile 当你需要从「每次敲命令」升级到「自动化 + 增量编译」时,make 是你的第一个选择。轻量、无依赖、Linux 标配。

CMake 当你需要跨平台构建、或者项目复杂到 Makefile 难以维护时,CMake 是 C/C++ 生态的事实标准。写一份 CMakeLists.txt,在 Linux、macOS、Windows 上都能生成对应的构建文件。

Ninja 当你已经用 CMake 写好构建描述,但 build 速度让你不耐烦时,-G Ninja 几乎零成本加速。它是目前最快的通用构建执行引擎。

pkg-config 当你被 -I-L-l 的参数地狱折磨,或者需要管理第三方库的编译参数时,pkg-config 把它们变成一个简单的查询。$(pkg-config --cflags --libs libcurl) 解决一切。

Meson CMake 的竞争者,语法更现代、更简洁。用 Python-like 的 DSL 替代了 CMake 的自定义语言。如果你开新项目且不喜欢 CMake 的语法,可以看看 Meson。

Bazel Google 出品的重量级构建系统。强调「可重现构建」和「超大规模 monorepo」。如果你的项目有几千个源文件、跨多种语言、需要严格的增量构建,Bazel 是为这种场景生的。但也意味着更重的运行时(需要 Java)和更陡的学习曲线。

4、面对一个新项目,怎么选?

没有银弹。我根据项目规模给一个参考决策:

个人小项目(< 10 个源文件)

  • Makefile 就够用了。不要在工具上浪费时间,专注写代码。
  • 如果你想顺便练习 CMake,那就用 CMake + Ninja。成本很低。

团队中型项目(10-100 个源文件)

  • CMake + Ninja 是当前的最佳组合。
  • 用现代 CMake 风格写,target_* 系列命令,PUBLIC/PRIVATE/INTERFACE 精确控制依赖。
  • 第三方依赖用 find_packageFetchContent

大型项目(100+ 个源文件)

  • 仍然是 CMake + Ninja
  • 但需要更注意模块边界的清晰划分,多目录嵌套 CMakeLists.txt。
  • 考虑用 ccache 加速重复编译。

超大型项目 / monorepo(1000+ 个源文件,跨语言)

  • 可以考虑 Bazel
  • 但 Bazel 的迁移成本很高,如果你已经在用 CMake 且没有遇到瓶颈,不用急着换。

嵌入式 / 交叉编译

  • CMake + 工具链文件是标准答案。几乎所有嵌入式 SDK 都支持 CMake。

5、一张总结表

工具 适合谁 优点 缺点
Makefile 个人、小项目 轻量、标配、学习成本低 跨平台差、语法老旧
CMake 几乎所有 C/C++ 项目 跨平台、生态强大、事实标准 语法丑、学习曲线陡
Ninja 搭配 CMake 使用 极快、零配置 不能手写
pkg-config 管理库依赖 简单有效、标准格式 需要库提供 .pc 文件
Meson 新人、新项目 语法现代、快 生态不如 CMake
Bazel Google 式 monorepo 可重现、大规模、跨语言 重、学习成本高

6、写在最后

十一篇文章,我们从手敲 gcc 出发,一路经过了 Makefile、编译四阶段、静态库动态库、pkg-config、CMake 入门与进阶、多目录组织、交叉编译、Ninja、Bazel,最后站在这张全景图前。

构建工具的本质从来没变过:告诉计算机,什么东西变了,就重新编译什么,并且尽可能并行。

变化的只是我们描述这件事的方式——从手写命令,到 Makefile,到 CMake 中的 target 思维,到工具链文件里的 sysroot,到 Bazel 里的 hermetic build。越复杂的工具,解决的越是同一个问题的规模版本。

如果你能穿越回第一次敲 gcc -o hello hello.c 的那个下午,你会发现,所有的构建工具都是在帮你回答一个问题:「当我改了这行代码,我的程序应该怎么重新变成可执行的?」

希望这十一篇文章,帮你把这条脉络理清了。

每天前进一小步,就是一个新的高度!