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支持if、foreach、while。不过我个人建议,不要在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,发布时还是手动列出源文件,更可靠。
PRIVATE、PUBLIC、INTERFACE来控制头文件的可见性。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才搞定。
cmake -DCMAKE_C_COMPILER=gcc ..。
跨平台支持
CMake最大的价值,就是跨平台。你写一份CMakeLists.txt,就能在Windows、Linux、macOS、甚至嵌入式RTOS上编译。怎么做到的?靠的是平台判断和工具链文件。
平台判断
CMake内置了一些变量,比如WIN32、APPLE、UNIX。你可以根据平台设置不同的编译选项:
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时也经常卡住,但每次解决一个问题,就对它多一分理解。
嗯,今天就先聊到这儿。你先把上面的CMakeLists.txt跑通,感受一下从配置到编译的完整流程。后面我们再深入聊模块化、库管理、测试集成这些进阶话题。