一、CMake 的哲学:别重复造轮子,也别硬编码
我第一次接触 CMake 是在一个跨平台项目里。当时 Makefile 已经写了上百行,换了个编译器就全崩了。说实话,那感觉挺崩溃的。
CMake 的设计理念其实很朴素:你描述项目长什么样,它帮你生成对应的构建文件。你不需要告诉它「用 gcc 编译 main.c」,而是说「我有一个可执行文件,它由 main.c 构成」。至于用什么编译器、加什么 flag,那是平台的事,CMake 替你操心。
我个人理解,CMake 的哲学可以总结为三点:
- 声明式而非命令式:你描述「是什么」,而不是「怎么做」
- 平台无关:一份 CMakeLists.txt,Windows/Linux/Mac 都能用
- 模块化:每个子目录可以有自己的 CMakeLists.txt,互不干扰
核心思想:CMake 不是构建工具,而是构建系统的生成器。它生成 Makefile、Ninja 文件、Visual Studio 工程等,然后由这些工具真正干活。
二、CMakeLists.txt 的基本结构
每个 CMake 项目都从 CMakeLists.txt 开始。这个文件说白了就是一份「项目说明书」。我习惯把它放在项目根目录,结构大概是这样:
project_root/
├── CMakeLists.txt # 根目录的构建文件
├── src/
│ ├── CMakeLists.txt # 子目录的构建文件(可选)
│ └── main.c
├── inc/
│ └── utils.h
└── build/ # 构建目录,我一般在这里执行 cmake ..
一个最简的 CMakeLists.txt 只需要三行:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_executable(myapp main.c)
嗯,就这么简单。但实际项目中,这三行背后藏着不少门道。我们一个一个说。
2.1 cmake_minimum_required:版本红线
这条命令告诉 CMake:「低于这个版本别跑」。为什么需要它?
我记得有一次,同事用 CMake 2.8 打开我写的 3.15 项目,结果一堆命令不认识。从那以后,我每条 CMakeLists.txt 第一行必定是版本声明。
写法很简单:
cmake_minimum_required(VERSION 3.10)
# 也可以指定范围
cmake_minimum_required(VERSION 3.10...3.20)
注意:版本号写太低,新特性用不了;写太高,用户环境可能不满足。我一般写 3.10,兼容性和功能都够用。
2.2 project:给项目起个名
project 命令不只是起名字。它还干几件事:
- 设置
PROJECT_NAME、PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR等变量 - 可以指定语言:
project(MyProject C CXX ASM) - 可以指定版本号:
project(MyProject VERSION 1.0.0)
我常用的写法:
project(MyEmbeddedProject
VERSION 1.2.0
DESCRIPTION "一个嵌入式项目示例"
LANGUAGES C CXX ASM
)
你想想看,如果项目里既有 C 又有汇编,不声明 ASM 语言,CMake 可能就不认识 .s 文件了。这个坑我踩过。
2.3 add_executable:生成可执行文件
这条命令告诉 CMake:「我要生成一个可执行文件,源文件是这些」。
add_executable(myapp main.c utils.c)
# 也可以先定义源文件列表,再引用
set(SOURCES main.c utils.c)
add_executable(myapp ${SOURCES})
对于嵌入式项目,我经常需要指定链接脚本、启动文件:
add_executable(firmware
startup_stm32f4.s
main.c
system_stm32f4xx.c
)
target_link_options(firmware PRIVATE
-T stm32f4_flash.ld
)
个人习惯:源文件列表单独用 set 定义,方便后续添加编译选项或条件编译。别把所有源文件都塞在 add_executable 里,那样改起来很痛苦。
三、一个完整的嵌入式 CMakeLists.txt 示例
下面是我给 STM32 项目写的典型 CMakeLists.txt。你可以直接拿来改:
cmake_minimum_required(VERSION 3.10)
project(STM32F4_Demo VERSION 1.0.0 LANGUAGES C CXX ASM)
# 设置芯片型号
set(MCU_FAMILY STM32F4)
set(MCU_MODEL STM32F407VG)
# 源文件列表
set(SOURCES
src/main.c
src/system_stm32f4xx.c
src/stm32f4xx_hal.c
startup/startup_stm32f407xx.s
)
# 头文件路径
set(INCLUDE_DIRS
inc
inc/hal
CMSIS/Include
)
# 生成可执行文件
add_executable(${PROJECT_NAME} ${SOURCES})
# 指定头文件路径
target_include_directories(${PROJECT_NAME} PRIVATE ${INCLUDE_DIRS})
# 编译选项
target_compile_options(${PROJECT_NAME} PRIVATE
-mcpu=cortex-m4
-mthumb
-mfloat-abi=hard
-mfpu=fpv4-sp-d16
-Os
-Wall
)
# 链接选项
target_link_options(${PROJECT_NAME} PRIVATE
-mcpu=cortex-m4
-mthumb
-mfloat-abi=hard
-mfpu=fpv4-sp-d16
-T ${CMAKE_SOURCE_DIR}/linker/stm32f4_flash.ld
-Wl,--gc-sections
)
你看,整个构建逻辑一目了然。换芯片?改 MCU_MODEL 和链接脚本就行。换编译器?CMake 自动处理。
四、CMake 的核心工作流程
为了让你更直观地理解 CMake 怎么工作,我画了张图:
这张图展示了 CMake 的完整工作流。注意看:CMake 本身不编译代码,它只负责生成构建规则。真正的编译是 make 或 ninja 干的。
五、常见问题与避坑指南
做嵌入式 CMake 项目这几年,我遇到过不少坑。挑几个典型的说说:
5.1 源文件路径写错了
我曾经把源文件路径写成绝对路径,结果项目换了个目录就编译不过。后来我统一用相对路径,或者用 ${CMAKE_SOURCE_DIR} 变量。
5.2 忘记声明 ASM 语言
嵌入式项目经常有 .s 启动文件。如果你在 project 里没写 LANGUAGES ASM,CMake 会直接忽略这些文件。链接的时候就会报 undefined reference。
5.3 链接脚本路径不对
target_link_options 里的 -T 参数,路径要是相对于构建目录的。我习惯用 ${CMAKE_SOURCE_DIR} 来定位链接脚本:
target_link_options(myapp PRIVATE
-T ${CMAKE_SOURCE_DIR}/linker/stm32f4_flash.ld
)
总结一下:CMake 的核心就三个命令——cmake_minimum_required 定版本,project 定项目,add_executable 定输出。把这三点吃透,剩下的都是锦上添花。
我个人觉得,CMake 最大的价值不是省掉你写 Makefile 的时间,而是让构建逻辑变得清晰、可维护。你想想看,一个 500 行的 Makefile 和 50 行的 CMakeLists.txt,哪个更容易看懂?
好了,这一章就到这里。记住:CMake 是描述,不是指令。你告诉它「要什么」,它帮你想「怎么做」。
公众号:蓝海资料掘金营,微信deep3321