一、CMake概述:什么是CMake,为什么需要CMake,CMake与传统Makefile的对比

1.1 什么是CMake?

CMake,全称是Cross-platform Make。说白了,它是一个跨平台的构建工具生成器。

你可能会问:「构建工具生成器?这又是什么鬼?」

嗯,我换个说法。CMake不直接编译你的代码,它帮你生成Makefile或者Visual Studio的工程文件。然后你再拿着这些文件去编译。

举个例子。你写了一套C++代码,想在Linux上用GCC编译,在Windows上用MSVC编译,在macOS上用Clang编译。传统做法是维护三套Makefile或者工程文件。累不累?

CMake的做法是:你只写一份CMakeLists.txt,它自动帮你生成对应平台的构建文件。

核心概念:CMake = 构建系统的构建系统

输入:CMakeLists.txt(你写的)

输出:Makefile / .sln / Xcode项目(自动生成的)

1.2 为什么需要CMake?

我在嵌入式行业摸爬滚打了十几年,见过太多因为构建系统混乱导致的悲剧。

有一次,一个同事把整个项目的Makefile重写了一遍,结果漏了一个依赖,编译出来的固件在产线上烧了三天才发现问题。那三天产线停摆,损失不小。

从那以后,我下定决心:必须用CMake来统一管理构建流程。

为什么需要CMake?我总结了几个核心原因:

  • 跨平台:一套代码,Windows/Linux/macOS/嵌入式RTOS都能编译
  • 跨编译器:GCC、ARMCC、IAR、Clang随意切换
  • 依赖管理:自动处理头文件路径、库文件链接顺序
  • 模块化:大型项目拆成多个子模块,每个模块独立管理
  • 可复用:写好的CMake模块,下个项目直接拿来用

我的经验:如果你在做一个超过10个源文件的嵌入式项目,别犹豫,直接上CMake。手写Makefile的维护成本,远比你想象的高。

1.3 CMake vs 传统Makefile

咱们来做个对比。我直接上表格,一目了然。

对比维度 传统Makefile CMake
跨平台 需要手动维护多套 自动生成,一套搞定
语法复杂度 Tab缩进、隐式规则,容易踩坑 清晰简洁,类似脚本语言
依赖管理 手动写依赖规则,容易遗漏 自动分析头文件依赖
第三方库集成 手动配置路径和链接参数 find_package一键搞定
调试难度 报错信息晦涩难懂 错误提示相对友好
学习曲线 入门简单,精通极难 入门稍高,但上限更高

你想想看,传统Makefile里那些隐式规则,什么$@、$^、$<,新手看了直接懵圈。而且Makefile对Tab缩进极其敏感,一个空格就能让你编译失败。

我曾经见过一个项目,Makefile里有一行用了空格代替Tab,结果排查了整整一个下午。从那以后,我写Makefile都养成了习惯:先按一下Esc,再按一下i,确保编辑器不会自动把Tab转成空格。

CMake就没有这些破事。它的语法更像Python或者Shell,清晰直观。

1.4 一个简单的对比示例

咱们用同一个项目,分别用Makefile和CMake来写,你感受一下差异。

项目结构:

my_project/
├── src/
│   ├── main.c
│   └── utils.c
└── include/
    └── utils.h

传统Makefile写法:

CC = gcc
CFLAGS = -I./include -Wall -O2
SRCS = src/main.c src/utils.c
OBJS = $(SRCS:.c=.o)
TARGET = my_app

$(TARGET): $(OBJS)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean

CMake写法:

cmake_minimum_required(VERSION 3.10)
project(my_app C)

set(CMAKE_C_STANDARD 11)

# 收集源文件
file(GLOB SOURCES "src/*.c")

# 添加头文件路径
target_include_directories(my_app PRIVATE include)

# 生成可执行文件
add_executable(my_app ${SOURCES})

看到了吗?CMake的代码可读性明显更好。而且你不需要手动写那些%.o的隐式规则,CMake全帮你处理了。

1.5 CMake的工作流程

CMake的工作流程分为两步:

  1. 配置阶段(Configure):读取CMakeLists.txt,检测编译器、库文件、头文件路径,生成构建文件
  2. 构建阶段(Build):用生成的构建文件(比如Makefile)去编译代码

我画个流程图,你一看就明白:

CMakeLists.txt CMake引擎 (cmake命令) Makefile .elf 配置阶段 → 生成构建文件 → 构建阶段 → 生成可执行文件

注意:CMake本身不编译代码。它只负责生成构建文件。真正编译的是make、ninja或者Visual Studio这些构建工具。

我曾经见过有人以为CMake是编译器,折腾了半天没编译出东西来。嗯,这个坑我踩过。

1.6 什么时候用CMake?

我个人习惯是:只要项目超过3个源文件,或者需要跨平台编译,我就上CMake。

但也不是所有场景都适合。比如:

  • 适合用CMake的场景:
    • 嵌入式项目(STM32、ESP32、ARM Linux等)
    • 需要支持多种编译器的项目
    • 大型C/C++项目(几十上百个源文件)
    • 需要集成第三方库的项目
  • 不太适合的场景:
    • 只有一个源文件的玩具项目
    • 纯汇编项目(CMake对汇编支持有限)
    • 已经用其他构建系统(如Bazel、Meson)稳定运行的项目

我的建议:如果你刚开始学嵌入式,可以先用手写Makefile练练手,理解编译链接的底层原理。等你有了一定基础,再切换到CMake。这样你既知道CMake帮你做了什么,也知道它为什么这么做。

1.7 本章小结

好了,咱们来捋一捋这一章的核心内容:

  • CMake是一个跨平台的构建工具生成器,不是编译器
  • 它解决了传统Makefile跨平台难、依赖管理复杂、可读性差的问题
  • CMake的工作流程分为配置阶段和构建阶段
  • 对于嵌入式项目,CMake是当前最主流的构建方案

下一章,咱们会手把手搭建CMake的开发环境。包括Windows、Linux、macOS三个平台的安装配置,以及VS Code的插件推荐。到时候我会分享一些我踩过的坑,保证让你少走弯路。

嗯,今天就到这里。记住一句话:工欲善其事,必先利其器。CMake就是那把好用的「器」。