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_package或FetchContent。
大型项目(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 的那个下午,你会发现,所有的构建工具都是在帮你回答一个问题:「当我改了这行代码,我的程序应该怎么重新变成可执行的?」
希望这十一篇文章,帮你把这条脉络理清了。
每天前进一小步,就是一个新的高度!