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的核心工作流程分两步:
- 配置阶段:运行
cmake命令,读取CMakeLists.txt,检测系统环境,生成构建文件(比如Makefile)。 - 构建阶段:运行
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,结果链接主程序时报找不到头文件。记住:如果头文件要被其他目标引用,必须用PUBLIC或INTERFACE。库自己的内部头文件才用PRIVATE。
知识体系图
下面这张图,概括了CMake的核心工作流和CMakeLists.txt的结构关系。我画的时候特意把配置阶段和构建阶段分开,因为这是新手最容易混淆的地方。
从图上可以看得很清楚:CMakeLists.txt是输入,经过配置阶段生成构建文件,再经过构建阶段得到最终产物。整个过程是线性的,每一步都有明确的输入输出。
总结
CMake入门其实就三件事:
- 理解CMake的角色:它不是编译器,是构建系统生成器。
- 掌握CMakeLists.txt的基本结构:版本、项目、目标、依赖,四行命令就能跑起来。
- 区分两个命令:
cmake做配置,cmake --build做编译。别混在一起。
我个人觉得,CMake最大的价值在于声明式思维。你告诉它“我要什么”,它帮你搞定“怎么做”。这种抽象能力,在嵌入式交叉编译场景下尤其珍贵。下一章我们会深入CMake的变量和条件控制,到时候你会发现,原来构建脚本也可以写得像高级语言一样灵活。
一句话记住:CMakeLists.txt是“需求文档”,cmake是“项目经理”,cmake --build是“施工队”。