3、构建系统选型:Make、CMake、Bazel、Gradle等构建工具的对比与选择
说到项目重构,构建系统往往是第一个要动刀的地方。
为什么?因为旧的构建脚本太乱了。我见过太多项目,Makefile 写了上千行,层层嵌套,改一个编译选项要翻半天。你想想看,重构本身就是要理清依赖、优化结构,如果构建工具本身就成了瓶颈,那还怎么玩?
今天咱们就来聊聊,市面上主流的构建工具该怎么选。
3.1 构建工具的本质是什么?
说白了,构建工具就是帮你把源代码变成可执行文件的那套规则。它要解决三个核心问题:
- 依赖管理:哪些文件改了,哪些需要重新编译
- 任务编排:先编译什么,后链接什么
- 可移植性:换台机器、换个编译器,能不能照样跑
嗯,听起来简单。但实际项目中,这三条每一条都能把人逼疯。
3.2 Make:老当益壮,但别滥用
Make 是最古老的构建工具,没有之一。我刚开始工作那会儿,所有项目都用 Make。它的优点就一个:简单直接。
# 一个简单的 Makefile
CC = gcc
CFLAGS = -Wall -O2
all: main
main: main.o utils.o
$(CC) $(CFLAGS) -o main main.o utils.o
main.o: main.c
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c utils.h
$(CC) $(CFLAGS) -c utils.c
clean:
rm -f *.o main
你看,规则很清晰。但问题也来了——跨平台。我在 Linux 上写的 Makefile,拿到 macOS 上可能就炸了。而且 Make 的语法太古老,条件判断、函数调用写起来像在写汇编。
3.3 CMake:跨平台的工业标准
CMake 的出现,就是为了解决 Make 的跨平台问题。它不直接编译,而是生成对应平台的构建文件(比如 Linux 下生成 Makefile,Windows 下生成 Visual Studio 工程)。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
add_executable(main main.cpp utils.cpp)
target_include_directories(main PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(main PRIVATE pthread)
我个人习惯用 CMake 管理 C/C++ 项目。它的模块化做得不错,find_package 可以自动找第三方库,add_subdirectory 可以组织子项目。而且 CMake 的语法虽然丑,但比 Make 的 tab 缩进友好多了。
不过 CMake 也有坑。我记得有一次,项目里用了 file(GLOB ...) 来收集源文件,结果新加了一个 .cpp 文件,CMake 没检测到变化,编译一直没过。嗯,从那以后我再也不用 GLOB 了,老老实实手动列文件。
Presets 功能(3.19+)可以统一团队的构建配置,避免每个人本地环境不一样导致的问题。强烈推荐。
3.4 Bazel:Google 的增量编译神器
Bazel 是 Google 内部工具 Blaze 的开源版。它的核心思想是精确的增量编译和沙盒化构建。
# BUILD
cc_binary(
name = "main",
srcs = ["main.cpp", "utils.cpp"],
deps = [":utils_lib"],
)
cc_library(
name = "utils_lib",
srcs = ["utils.cpp"],
hdrs = ["utils.h"],
)
Bazel 的厉害之处在于,它知道每个文件的哈希值。你改了一个头文件,它只重新编译依赖这个头文件的目标。对于大型项目(比如 Android、TensorFlow),这种增量编译能节省大量时间。
但 Bazel 的学习曲线很陡。它的 BUILD 文件语法是 Starlark(Python 的子集),规则系统非常复杂。而且 Bazel 对网络依赖的管理比较严格,内网环境配置起来很麻烦。
3.5 Gradle:Java/Android 生态的王者
Gradle 是基于 Groovy/Kotlin DSL 的构建工具。它在 Java 和 Android 领域几乎是事实标准。
// build.gradle.kts (Kotlin DSL)
plugins {
java
application
}
application {
mainClass.set("com.example.Main")
}
dependencies {
implementation("com.google.guava:guava:31.1-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
}
Gradle 的优点是灵活。你可以写自定义 Task,可以写插件,可以控制构建生命周期的每一个环节。而且 Gradle 的缓存机制(Build Cache)和守护进程(Daemon)让增量构建非常快。
但 Gradle 也有槽点。它的配置语言太灵活了,导致很多项目的 build.gradle 写得像意大利面条。我见过一个项目,build.gradle 里嵌了 300 多行 Groovy 代码,各种闭包嵌套,读起来比 C++ 模板还费劲。
3.6 横向对比:一张表说清楚
| 特性 | Make | CMake | Bazel | Gradle |
|---|---|---|---|---|
| 主要语言 | C/C++ | C/C++ | 多语言 | Java/Kotlin |
| 跨平台 | 差 | 好 | 好 | 好 |
| 增量编译 | 基于时间戳 | 基于时间戳 | 基于哈希 | 基于缓存 |
| 学习曲线 | 低 | 中 | 高 | 中高 |
| 适合项目规模 | 小 | 中 | 大/超大 | 中/大 |
| 社区生态 | 成熟 | 成熟 | 增长中 | 成熟 |
3.7 我的选型建议
说了这么多,到底怎么选?我个人的经验是:
- 纯 C/C++ 项目,团队不大:用 CMake。它是最稳妥的选择,生态好,坑少。
- Java/Android 项目:用 Gradle。没得选,这是事实标准。
- 超大型 monorepo,多语言混合:考虑 Bazel。虽然学习成本高,但长期收益巨大。
- 小工具、快速原型:用 Make 就够了。别过度设计。
你可能会问:「那如果我现在用的是 Make,想迁移到 CMake,值得吗?」
我的回答是:看项目规模。如果代码量在 5 万行以内,Make 还能撑。超过 10 万行,我建议尽早迁移。我在一个 30 万行的嵌入式项目上做过迁移,从 Make 到 CMake,花了大概一周时间。迁移完之后,跨平台编译、单元测试集成、CI 配置都顺畅了很多。
3.8 知识体系总览
下面这张图,帮你理清构建工具选型的核心逻辑:
这张图的核心逻辑很简单:项目规模决定复杂度,复杂度决定工具选型。别为了炫技选 Bazel,也别因为偷懒死守 Make。工具是为人服务的,不是反过来。
好了,构建系统选型就聊到这儿。下一节咱们会深入 CMake 的实战技巧——毕竟这是目前最通用的选择。到时候我会分享一些我在迁移项目中踩过的坑,保证有用。
可可精品资料库