第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是什么 • 跨平台构建系统生成器 • 元构建系统 • 生成Makefile/VS工程等 CMakeLists.txt语法 • 注释: # 开头 • 变量: set() / ${} • 条件: if/elseif/else • 循环: foreach/while 核心命令 • project():定义项目 • add_executable():生成可执行文件 • cmake_minimum_required() 最小化项目实战 • 三行CMakeLists.txt搞定 • cmake -B build • cmake --build build 避坑指南 • 变量名大小写敏感 • 别忘了加ASM语言支持 • 永远在build目录构建 一次编写,到处构建

嗯,看到这里,你应该对CMake有了一个基本的认识。它不是什么高深莫测的东西,就是一个帮你管理构建流程的工具。记住:先跑通最小项目,再逐步添加功能。别一上来就想搞个完美的构建系统,那只会让你陷入细节的泥潭。

我个人建议,你现在就可以打开终端,按照上面的步骤创建一个最小项目。亲手跑一遍,比看十遍文章都管用。

最后一个小建议:刚开始学CMake,别怕犯错。我当年第一次写CMakeLists.txt,连project()都拼错了。多写几次,自然就熟了。

可可精品资料库