11、CMake入门:CMake是什么、CMakeLists.txt结构、cmake与cmake --build命令

说实话,我刚开始接触嵌入式开发那会儿,用的还是纯Makefile。每个项目都得手写一堆规则,链接路径、编译选项、头文件依赖……改一次平台就得重写半套。后来转到CMake,第一感觉是:这玩意儿终于让我从重复劳动里解脱出来了

CMake不是编译器,也不是构建工具。它是个构建系统生成器。你写一份CMakeLists.txt,它能给你生成Makefile、Ninja文件、甚至Visual Studio的工程文件。说白了,你只需要描述“我要编译哪些源文件、链接哪些库”,剩下的平台适配、编译器选择,CMake帮你搞定。

CMake是什么?

CMake全称是Cross-Platform Make。它诞生于2000年,最初是为了解决VTK(可视化工具包)的跨平台构建问题。我当年在做一个ARM+Linux的项目时,需要在Windows上写代码、在Linux服务器上编译、再交叉编译到ARM板子上。如果没有CMake,光是维护三套Makefile就能让人崩溃。

CMake的核心工作流程分两步:

  1. 配置阶段:运行cmake命令,读取CMakeLists.txt,检测系统环境,生成构建文件(比如Makefile)。
  2. 构建阶段:运行cmake --build命令,调用底层构建工具(make、ninja等)真正编译代码。

为什么要分开?因为配置阶段只做一次,除非你改了CMakeLists.txt或者换了编译器。构建阶段可以反复执行,只编译修改过的文件。这种分离设计,在大型项目里特别有用。

关键点:CMake本身不编译代码。它是个“包工头”,负责调度真正的编译器、链接器、归档器。

CMakeLists.txt结构

每个CMake项目,根目录下必须有一个CMakeLists.txt。这是CMake的“剧本”。我见过新手一上来就写几百行的CMakeLists.txt,结果到处报错。其实核心结构很简单,就几个命令:

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.10)

# 定义项目名称和语言
project(MyProject C CXX)

# 添加可执行文件
add_executable(my_app main.c utils.c)

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

# 链接外部库
target_link_libraries(my_app PRIVATE m pthread)

嗯,这里要注意:cmake_minimum_required必须写在第一行。我曾经在一个项目里把它写在了project后面,结果CMake报了个莫名其妙的警告,排查了半天才发现是顺序问题。

常用的CMake命令,我按使用频率排了个序:

命令 作用 使用频率
cmake_minimum_required 指定最低CMake版本 必写
project 定义项目名称、语言 必写
add_executable 添加可执行文件目标 极高
add_library 添加库目标(静态/动态)
target_include_directories 指定头文件路径
target_link_libraries 链接库
set 设置变量
find_package 查找外部包

你想想看,一个嵌入式项目,无非就是:源文件 → 编译 → 链接成可执行文件或库。CMakeLists.txt就是把这个流程用声明式的方式写出来。

cmake与cmake --build命令

这两个命令,是CMake使用中最频繁的。我习惯在项目根目录下建一个build文件夹,所有构建产物都放里面,保持源码目录干净。

配置阶段:

mkdir build
cd build
cmake ..

cmake .. 表示CMakeLists.txt在上一级目录。CMake会检测当前系统有什么编译器、有什么库,然后生成Makefile。如果你要指定编译器,可以加参数:

cmake .. -DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc

这就是交叉编译的入口。我在做树莓派裸机开发时,就是用这个参数指定ARM交叉编译器。

构建阶段:

cmake --build .

这个命令会自动调用make(或者你指定的构建工具)。等价于在build目录下直接执行make。但cmake --build的好处是:它不依赖具体的构建系统。你换Ninja、换MSBuild,命令都不用改。

小技巧cmake --build . -- -j4 可以传递参数给底层构建工具。这里的--后面跟的是make的参数,-j4表示4个并行任务。我一般设成CPU核心数+1。

还有一个常用参数:--target。如果你只想编译某个特定的目标(比如只编译库,不编译可执行文件):

cmake --build . --target my_lib

这在大型项目里特别实用。我维护过一个包含20多个子模块的嵌入式项目,每次只改了一个驱动文件,用--target指定编译那个模块,能省下大半时间。

一个完整的入门示例

我们写一个最简单的嵌入式风格项目:一个LED控制程序,包含主程序和驱动库。

目录结构:

project/
├── CMakeLists.txt
├── main.c
├── led/
│   ├── CMakeLists.txt
│   ├── led.c
│   └── led.h

根目录CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(LEDController C)

# 添加子目录
add_subdirectory(led)

# 主程序
add_executable(led_controller main.c)

# 链接驱动库
target_link_libraries(led_controller PRIVATE led_driver)

led/CMakeLists.txt:

# 生成静态库
add_library(led_driver STATIC led.c)

# 暴露头文件路径
target_include_directories(led_driver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

编译步骤:

mkdir build && cd build
cmake ..
cmake --build .

搞定。你会在build目录下看到led_controller可执行文件和libled_driver.a静态库。

避坑指南:我曾经在target_include_directories里用了PRIVATE,结果链接主程序时报找不到头文件。记住:如果头文件要被其他目标引用,必须用PUBLICINTERFACE。库自己的内部头文件才用PRIVATE

知识体系图

下面这张图,概括了CMake的核心工作流和CMakeLists.txt的结构关系。我画的时候特意把配置阶段和构建阶段分开,因为这是新手最容易混淆的地方。

CMake核心工作流与CMakeLists.txt结构 CMakeLists.txt 结构 cmake_minimum_required(VERSION 3.10) project(MyProject C CXX) add_executable(my_app main.c) target_include_directories(...) target_link_libraries(...) add_subdirectory(led) CMake 工作流 配置阶段 cmake .. → 生成 Makefile 构建阶段 cmake --build . → 编译链接 输出产物 可执行文件 / 静态库 / 动态库 读取

从图上可以看得很清楚:CMakeLists.txt是输入,经过配置阶段生成构建文件,再经过构建阶段得到最终产物。整个过程是线性的,每一步都有明确的输入输出。

总结

CMake入门其实就三件事:

  • 理解CMake的角色:它不是编译器,是构建系统生成器。
  • 掌握CMakeLists.txt的基本结构:版本、项目、目标、依赖,四行命令就能跑起来。
  • 区分两个命令cmake做配置,cmake --build做编译。别混在一起。

我个人觉得,CMake最大的价值在于声明式思维。你告诉它“我要什么”,它帮你搞定“怎么做”。这种抽象能力,在嵌入式交叉编译场景下尤其珍贵。下一章我们会深入CMake的变量和条件控制,到时候你会发现,原来构建脚本也可以写得像高级语言一样灵活。

一句话记住:CMakeLists.txt是“需求文档”,cmake是“项目经理”,cmake --build是“施工队”。


公众号:蓝海资料掘金营,微信deep3321