第2章:CMake基础入门
说实话,我刚开始接触嵌入式开发那会儿,用的还是各种厂商自带的IDE。点一下编译按钮,等个几十秒,程序就跑起来了。那时候觉得,这玩意儿挺方便啊,干嘛要学CMake?
直到有一次,我需要把一个项目从Keil移植到GCC,再移植到IAR。三个IDE,三套工程配置,每次改代码都得同步三份。那感觉,就像同时维护三个不同的项目。嗯,从那以后,我老老实实开始学CMake了。
CMake是什么?
CMake,全称Cross-platform Make。说白了,它是一个跨平台的构建系统生成器。
你可能会问:它跟Makefile什么关系?
我打个比方:Makefile就像是你手写的一份详细菜谱,告诉厨师每一步该怎么做。而CMake,是一个帮你自动生成菜谱的工具。你只需要告诉它「我要做红烧肉」,它就能根据你厨房的实际情况(Linux、Windows、还是Mac),生成一份合适的菜谱(Makefile、Visual Studio工程、Xcode工程等)。
我个人习惯把CMake看作一个元构建系统。它不直接编译代码,而是生成其他构建系统能识别的文件。这样,你的项目就能在Linux上用Make编译,在Windows上用Visual Studio编译,在Mac上用Xcode编译——同一份CMakeLists.txt,搞定所有平台。
核心思想:Write once, build anywhere. 一次编写,到处构建。
CMakeLists.txt基本语法
CMake的配置文件叫CMakeLists.txt。每个有源码的目录下,都可以放一个。我见过不少新手,一上来就写几百行的CMakeLists.txt,结果到处报错。其实,CMake的语法没那么复杂,核心就几条命令。
注释
CMake的注释用#开头,跟Shell脚本一样。
# 这是一条注释
# 我习惯在关键逻辑前加注释,方便后来人(包括三个月后的自己)
变量
CMake里变量用${}引用。定义变量用set()命令。
set(MY_VARIABLE "hello")
message("变量的值是:${MY_VARIABLE}")
这里有个坑,我曾经踩过:变量名大小写敏感。${MY_VARIABLE}和${my_variable}是两个不同的变量。建议统一用小写或大写,别混着用。
条件判断
CMake支持if、elseif、else、endif。语法跟C语言有点像,但更灵活。
if(WIN32)
message("这是Windows平台")
elseif(APPLE)
message("这是macOS平台")
else()
message("这是Linux或其他平台")
endif()
小技巧:调试CMakeLists.txt时,多用message()打印变量值。这比猜来猜去高效多了。
循环
foreach和while,用得不多,但偶尔能救命。比如批量处理源文件时。
set(SOURCES main.cpp utils.cpp driver.cpp)
foreach(src ${SOURCES})
message("处理源文件:${src}")
endforeach()
project()与add_executable()
这两个命令,是CMakeLists.txt里最核心的。几乎每个项目都离不开它们。
project()
这条命令定义项目名称,还能指定语言版本。我习惯在第一行就写上。
project(MyProject C CXX ASM)
参数说明:
- 第一个参数:项目名称。会生成
PROJECT_NAME变量。 - 后续参数:项目使用的语言。C表示C语言,CXX表示C++,ASM表示汇编。不写的话默认全支持。
我曾经犯过一个错:在嵌入式项目中忘了加ASM,结果启动文件编译不过。折腾了半天才发现,原来CMake默认不处理汇编文件。
注意:project()必须在add_executable()之前调用。否则CMake会报错。
add_executable()
这条命令告诉CMake:我要生成一个可执行文件。你需要哪些源文件?
add_executable(my_app main.cpp utils.cpp)
第一个参数是目标名称,也就是生成的可执行文件名。后续参数是源文件列表。
我个人的习惯是:源文件列表单独用变量定义,这样后期维护方便。
set(SOURCES
main.cpp
utils.cpp
driver/gpio.cpp
driver/uart.cpp
)
add_executable(my_app ${SOURCES})
这样写的好处是:哪天要加个源文件,只需要在set里加一行,不用去改add_executable那行。项目大了以后,这个习惯能省不少事。
最小化CMake项目实战
光说不练假把式。我们来写一个最简的CMake项目。
假设项目结构如下:
my_project/
├── CMakeLists.txt
└── main.cpp
main.cpp内容:
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
CMakeLists.txt内容:
cmake_minimum_required(VERSION 3.10)
project(HelloCMake CXX)
add_executable(hello main.cpp)
就这么三行。第一行指定CMake最低版本,第二行定义项目,第三行生成可执行文件。
编译步骤:
mkdir build
cd build
cmake ..
make
或者用更现代的方式:
cmake -B build
cmake --build build
我个人推荐第二种方式。它更简洁,而且跨平台兼容性更好。在Windows上,cmake --build会自动调用MSBuild;在Linux上,它会调用make。你不需要关心底层用的是哪个构建工具。
避坑指南:我曾经在Windows上直接运行cmake ..,结果生成的工程文件散落在源码目录里,搞得一团糟。后来养成习惯:永远在单独的build目录下构建。这样想清理的时候,直接删掉build目录就行,源码干干净净。
本章知识体系
下面这张图,是我梳理的CMake入门知识结构。你可以把它当作一个快速索引。
嗯,看到这里,你应该对CMake有了一个基本的认识。它不是什么高深莫测的东西,就是一个帮你管理构建流程的工具。记住:先跑通最小项目,再逐步添加功能。别一上来就想搞个完美的构建系统,那只会让你陷入细节的泥潭。
我个人建议,你现在就可以打开终端,按照上面的步骤创建一个最小项目。亲手跑一遍,比看十遍文章都管用。
最后一个小建议:刚开始学CMake,别怕犯错。我当年第一次写CMakeLists.txt,连project()都拼错了。多写几次,自然就熟了。