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 的语法太古老,条件判断、函数调用写起来像在写汇编。

注意:Make 适合小型项目(几千行代码以内)。一旦项目超过 10 万行,Makefile 的维护成本会指数级上升。我曾经接手过一个项目,Makefile 里光变量定义就有 200 多行,改一个路径要全局搜索替换——那感觉,酸爽。

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 了,老老实实手动列文件。

小技巧:CMake 的 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 对网络依赖的管理比较严格,内网环境配置起来很麻烦。

适用场景:多语言混合项目(C++、Java、Python 一起上)、超大型单体仓库(monorepo)、对构建速度有极致要求的团队。

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++ 模板还费劲。

避坑指南:我曾经把一个项目的 Gradle 从 Groovy DSL 迁移到 Kotlin DSL,花了整整两天。虽然 Kotlin DSL 有类型检查和自动补全,但迁移过程中各种隐式转换和 API 差异让人抓狂。如果你不是重度 Gradle 用户,建议先别急着迁移。

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 知识体系总览

下面这张图,帮你理清构建工具选型的核心逻辑:

构建系统选型决策树 项目重构 → 选构建工具 小型项目(<5万行) 中型项目(5-20万行) 大型项目(>20万行) Make 足够 CMake 首选 Bazel 或 Gradle C/C++ → CMake Java/Android → Gradle 多语言 → Bazel 决策节点 推荐方案 备选方案

这张图的核心逻辑很简单:项目规模决定复杂度,复杂度决定工具选型。别为了炫技选 Bazel,也别因为偷懒死守 Make。工具是为人服务的,不是反过来。

好了,构建系统选型就聊到这儿。下一节咱们会深入 CMake 的实战技巧——毕竟这是目前最通用的选择。到时候我会分享一些我在迁移项目中踩过的坑,保证有用。


可可精品资料库