CMake 入门:从零开始构建你的C语言项目

说实话,我刚开始做嵌入式开发那会儿,用的还是手写Makefile。每个新项目都要从头写一遍链接脚本、编译选项、源文件列表……那叫一个痛苦。后来接触到CMake,我才发现——原来构建这件事,可以这么清爽。

CMake不是编译器,也不是构建工具。它是个构建系统生成器。你写一份CMakeLists.txt,它就能帮你生成对应平台的Makefile、Ninja文件,甚至是Visual Studio的工程文件。说白了,你只需要学会CMake的语法,剩下的跨平台问题,它替你扛了。

CMake的基本语法

CMake的语法其实很简单。它不像C语言那样有复杂的类型系统,更像是一种声明式的脚本语言。我习惯把它分成三类:变量、命令、控制流

变量

变量用set()定义,用${}引用。举个例子:

set(MY_SOURCES main.c utils.c driver.c)
message("源文件列表: ${MY_SOURCES}")

这里要注意,CMake的变量是字符串类型的。你看着像列表,其实内部是用分号分隔的字符串。我在项目中遇到过一个问题:用set(LIST a b c)set(LIST "a b c"),前者是三个元素,后者是一个带空格的字符串。嗯,这个坑我踩过。

命令

命令就是CMake提供的内置函数。最常用的几个:

  • add_executable() — 生成可执行文件
  • add_library() — 生成库文件
  • target_include_directories() — 指定头文件路径
  • target_link_libraries() — 链接库

你想想看,这些命令的名字本身就说明了用途,基本不用死记硬背。

控制流

CMake支持ifforeachwhile。不过我个人建议,不要在CMakeLists.txt里写太复杂的逻辑。CMake不是编程语言,它的控制流能力有限。我曾经见过一个同事在CMake里写了三层嵌套的foreach,调试起来简直噩梦。

if(WIN32)
    message("这是Windows平台")
elseif(APPLE)
    message("这是macOS平台")
else()
    message("这是Linux或其他平台")
endif()

CMakeLists.txt 编写实战

好,理论说完了,咱们直接上手写一个。假设你有一个C语言项目,结构如下:

project/
├── src/
│   ├── main.c
│   ├── utils.c
│   └── utils.h
├── lib/
│   └── driver.c
└── CMakeLists.txt

对应的CMakeLists.txt长这样:

cmake_minimum_required(VERSION 3.10)
project(MyEmbeddedProject C)

# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# 收集源文件
file(GLOB_RECURSE SOURCES "src/*.c" "lib/*.c")

# 生成可执行文件
add_executable(my_app ${SOURCES})

# 指定头文件路径
target_include_directories(my_app PRIVATE src/ lib/)

这里我用了file(GLOB_RECURSE)来收集源文件。说实话,这个命令很方便,但有个坑:如果你新增了源文件,CMake不会自动重新配置。我建议在开发阶段用GLOB,发布时还是手动列出源文件,更可靠。

小技巧:PRIVATEPUBLICINTERFACE来控制头文件的可见性。PRIVATE表示只有当前目标能看到,PUBLIC表示链接这个目标的其他目标也能看到。这个设计很优雅,能避免头文件污染。

构建与编译

写好了CMakeLists.txt,接下来就是构建了。我习惯用out-of-source构建,也就是在项目根目录外建一个build文件夹。这样所有编译中间文件都在build里,不会污染源码目录。

mkdir build
cd build
cmake ..
make

如果你用的是Ninja(一个更快的构建工具),只需要改一行:

cmake -G Ninja ..
ninja

嗯,这里要注意:cmake ..这一步是配置阶段,它会检查编译器、头文件、库是否存在。如果报错,别慌,仔细看错误信息。我遇到过最离谱的一次,是系统里装了三个版本的Python,CMake死活找不到正确的库路径。最后我手动设置了CMAKE_PREFIX_PATH才搞定。

避坑指南:我曾经在Windows上编译一个嵌入式项目,CMake默认选了MSVC编译器,但我的代码是给GCC写的。结果一堆语法错误。解决方案是在配置时指定编译器:cmake -DCMAKE_C_COMPILER=gcc ..

跨平台支持

CMake最大的价值,就是跨平台。你写一份CMakeLists.txt,就能在Windows、Linux、macOS、甚至嵌入式RTOS上编译。怎么做到的?靠的是平台判断工具链文件

平台判断

CMake内置了一些变量,比如WIN32APPLEUNIX。你可以根据平台设置不同的编译选项:

if(WIN32)
    target_compile_definitions(my_app PRIVATE PLATFORM_WINDOWS)
    target_link_libraries(my_app PRIVATE ws2_32)
elseif(UNIX)
    target_compile_definitions(my_app PRIVATE PLATFORM_LINUX)
    target_link_libraries(my_app PRIVATE pthread)
endif()

工具链文件

对于嵌入式开发,你通常需要交叉编译。比如给ARM Cortex-M4写代码,你得用arm-none-eabi-gcc。这时候就需要工具链文件(toolchain file)。

# arm-gcc-toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-as)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

使用时:

cmake -DCMAKE_TOOLCHAIN_FILE=arm-gcc-toolchain.cmake ..
核心思想:CMake的跨平台不是魔法,而是通过抽象层把平台差异封装起来。你只需要关心逻辑,剩下的交给CMake和工具链文件。

知识体系总览

下面这张图,是我自己总结的CMake知识结构。你看一眼,心里就有数了。

CMake 核心 基本语法 变量: set() / ${} 命令: add_executable() 控制流: if/foreach 构建与编译 out-of-source 构建 cmake + make/ninja 配置阶段 vs 构建阶段 跨平台支持 平台判断: WIN32/UNIX 工具链文件 交叉编译 一句话总结 CMake = 一份配置 + 跨平台生成 + 灵活构建

写在最后

CMake入门其实不难。你只要记住三件事:变量怎么设、命令怎么用、平台怎么判。剩下的,都是在实践中慢慢积累的。我刚开始用CMake时也经常卡住,但每次解决一个问题,就对它多一分理解。

嗯,今天就先聊到这儿。你先把上面的CMakeLists.txt跑通,感受一下从配置到编译的完整流程。后面我们再深入聊模块化、库管理、测试集成这些进阶话题。


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