文章目录
-
- 核心心智模型
- 一、项目根命令
-
- [1.1 `cmake_minimum_required(VERSION <版本号>)`](#1.1
cmake_minimum_required(VERSION <版本号>)) - [1.2 `project(<项目名> 语言 VERSION \<版本\>)`](#1.2
project(<项目名> [语言] [VERSION <版本>]))
- [1.1 `cmake_minimum_required(VERSION <版本号>)`](#1.1
- 二、变量与流程控制
-
- [2.1 `set(变量名 值 CACHE 类型 描述)` --- 设置变量](#2.1
set(变量名 值 [CACHE 类型 描述])— 设置变量) - [2.2 `option(变量名 描述 默认值)` --- 布尔开关](#2.2
option(变量名 描述 默认值)— 布尔开关) - [2.3 `message(模式 "消息")` --- 打印信息](#2.3
message([模式] "消息")— 打印信息) - [2.4 条件判断](#2.4 条件判断)
- [2.5 循环](#2.5 循环)
- [2.1 `set(变量名 值 CACHE 类型 描述)` --- 设置变量](#2.1
- 三、核心构建命令:定义目标
-
- [3.1 `add_executable(<目标名> <源文件...>)` --- 生成可执行文件](#3.1
add_executable(<目标名> <源文件...>)— 生成可执行文件) - [3.2 `add_library(<目标名> <类型> <源文件...>)` --- 生成库](#3.2
add_library(<目标名> <类型> <源文件...>)— 生成库)
- [3.1 `add_executable(<目标名> <源文件...>)` --- 生成可执行文件](#3.1
- 四、目标属性设置命令
-
- [4.1 `target_compile_options(<目标> <可见性> <选项...>)` --- 编译选项](#4.1
target_compile_options(<目标> <可见性> <选项...>)— 编译选项) - [4.2 `target_include_directories(<目标> <可见性> <路径...>)` --- 头文件搜索路径](#4.2
target_include_directories(<目标> <可见性> <路径...>)— 头文件搜索路径) - [4.3 `target_compile_definitions(<目标> <可见性> <宏...>)` --- 定义预处理器宏](#4.3
target_compile_definitions(<目标> <可见性> <宏...>)— 定义预处理器宏) - [4.4 `target_link_libraries(<目标> <可见性> <库...>)` --- 链接库](#4.4
target_link_libraries(<目标> <可见性> <库...>)— 链接库)
- [4.1 `target_compile_options(<目标> <可见性> <选项...>)` --- 编译选项](#4.1
- [五、可见性关键字:PUBLIC / PRIVATE / INTERFACE](#五、可见性关键字:PUBLIC / PRIVATE / INTERFACE)
- [六、生成器表达式:`\<...\>\`](#六、生成器表达式:`<...>`)
- 七、自定义命令与目标
-
- [7.1 `add_custom_command()` --- 添加自定义构建步骤](#7.1
add_custom_command()— 添加自定义构建步骤) - [7.2 `add_custom_target()` --- 添加纯命令目标](#7.2
add_custom_target()— 添加纯命令目标)
- [7.1 `add_custom_command()` --- 添加自定义构建步骤](#7.1
- 八、其他实用命令速览
-
- [8.1 `file()` --- 文件操作](#8.1
file()— 文件操作) - [8.2 `find_package()` --- 查找外部库](#8.2
find_package()— 查找外部库) - [8.3 `add_subdirectory()` --- 引入子项目](#8.3
add_subdirectory()— 引入子项目) - [8.4 `include()` --- 包含外部 CMake 文件](#8.4
include()— 包含外部 CMake 文件) - [8.5 `install()` --- 安装规则](#8.5
install()— 安装规则)
- [8.1 `file()` --- 文件操作](#8.1
- [九、完整实战:PID 项目进阶 CMakeLists.txt](#九、完整实战:PID 项目进阶 CMakeLists.txt)
- 十、命令速查表
- [十一、调试 CMakeLists.txt 的小技巧](#十一、调试 CMakeLists.txt 的小技巧)
好的,我将从零开始,系统性地讲解 CMakeLists.txt 中的脚本命令。这是 CMake 的核心------用声明式的方式描述你的项目如何构建。
核心心智模型
CMakeLists.txt 不是一种编程语言,而是一种构建描述语言。你告诉 CMake:
- 你的项目叫什么,用什么语言
- 你想要生成什么(可执行文件?库?)
- 这些目标由哪些源文件组成
- 它们需要什么编译选项、头文件路径、链接库
CMake 根据这些描述,为你生成对应平台的构建文件。
一、项目根命令
1.1 cmake_minimum_required(VERSION <版本号>)
必须是第一行命令。指定项目需要的最低 CMake 版本,避免用户在老版本上遇到奇怪的错误。
cmake
cmake_minimum_required(VERSION 3.15)
1.2 project(<项目名> [语言] [VERSION <版本>])
定义项目名称、版本和使用的编程语言。这会自动设置许多变量。
cmake
project(PID_Sim VERSION 1.0.0 LANGUAGES C)
执行后,CMake 自动创建以下变量(你可以直接使用):
| 变量 | 值 | 含义 |
|---|---|---|
PROJECT_NAME |
PID_Sim |
项目名称 |
PROJECT_VERSION |
1.0.0 |
项目版本 |
PROJECT_SOURCE_DIR |
D:/Desktop/demoCmk |
源码根目录 |
PROJECT_BINARY_DIR |
D:/Desktop/demoCmk/build |
构建根目录 |
CMAKE_C_COMPILER |
gcc |
自动检测到的C编译器 |
重要 :project() 还隐式调用了 enable_language(C),告诉 CMake 去检测 C 编译器。这也是为什么 project() 必须在所有编译相关命令之前。
二、变量与流程控制
2.1 set(变量名 值 [CACHE 类型 描述]) --- 设置变量
普通变量(只在当前 CMakeLists.txt 及子目录可见):
cmake
set(SOURCES main.c pid.c)
set(COMPILE_OPTS -O0 -g -Wall)
缓存变量 (可在命令行用 -D 覆盖,存储在 CMakeCache.txt 中):
cmake
set(CMAKE_C_STANDARD 11 CACHE STRING "C standard to use")
set(ENABLE_LOGGING OFF CACHE BOOL "Enable debug logging")
列表变量(用分号或空格分隔,实际存储为分号分隔的字符串):
cmake
set(MY_LIST a b c) # 等价于 "a;b;c"
set(MY_LIST "a;b;c") # 同上
2.2 option(变量名 描述 默认值) --- 布尔开关
option 是 set(... CACHE BOOL ...) 的简写,专门用于开关。
cmake
option(ENABLE_DEBUG "Enable debug output" OFF)
命令行覆盖:cmake -B build -DENABLE_DEBUG=ON
2.3 message([模式] "消息") --- 打印信息
cmake
message(STATUS "Building project: ${PROJECT_NAME}") # 配置时打印,带 --
message(WARNING "This is a warning") # 警告,继续执行
message(FATAL_ERROR "Cannot continue, missing dependency") # 错误,停止配置
2.4 条件判断
cmake
if(条件)
# ...
elseif(条件)
# ...
else()
# ...
endif()
常用条件表达式:
cmake
# 比较
if(CMAKE_BUILD_TYPE STREQUAL "Debug") # 字符串相等
if(MY_VAR MATCHES "pattern") # 正则匹配
if(NOT DEFINED MY_VAR) # 变量是否未定义
# 布尔
if(ENABLE_LOGGING) # 如果为 ON/TRUE/1
if(NOT ENABLE_LOGGING) # 如果为 OFF/FALSE/0
# 文件系统
if(EXISTS "${CMAKE_SOURCE_DIR}/pid.h") # 文件/目录存在
if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/libs") # 是否目录
完整示例(应用到 PID 项目):
cmake
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build: using -O0 -g")
set(EXTRA_FLAGS -O0 -g)
else()
message(STATUS "Release build: using -O2")
set(EXTRA_FLAGS -O2)
endif()
2.5 循环
cmake
# foreach 循环
foreach(ITEM IN LISTS MY_LIST)
message(STATUS "Item: ${ITEM}")
endforeach()
# 范围循环
foreach(IDX RANGE 0 5) # 0,1,2,3,4,5
message(STATUS "Index: ${IDX}")
endforeach()
# while 循环(较少用)
set(COUNT 0)
while(COUNT LESS 5)
message(STATUS "Count: ${COUNT}")
math(EXPR COUNT "${COUNT} + 1")
endwhile()
三、核心构建命令:定义目标
这是 CMakeLists.txt 的灵魂。所有构建都围绕"目标"(target)展开。
3.1 add_executable(<目标名> <源文件...>) --- 生成可执行文件
cmake
set(SOURCES main.c pid.c)
add_executable(pid_offline ${SOURCES})
- 第一个参数是目标名(可执行文件名,Windows 自动加
.exe) - 后面是所有
.c文件 - 执行后,CMake 会自动为每个
.c生成编译规则,最后链接成pid_offline
3.2 add_library(<目标名> <类型> <源文件...>) --- 生成库
库的类型:
| 类型 | 含义 |
|---|---|
STATIC |
静态库 (.a / .lib),编译时打包进可执行文件 |
SHARED |
动态库 (.so / .dll),运行时加载 |
MODULE |
插件式动态库,不能被链接,只能 dlopen 加载 |
INTERFACE |
只有头文件的库(如 header-only 库),不生成实际文件 |
示例(把 PID 算法做成静态库):
cmake
# 静态库
add_library(pidlib STATIC pid.c)
# 使用 pidlib 的可执行文件
add_executable(pid_test test.c)
target_link_libraries(pid_test PRIVATE pidlib)
为什么推荐做库?
- 代码复用:多个可执行文件可以共用同一个库
- 清晰架构:核心算法和测试/应用分离
- 编译加速:库单独编译,修改应用代码不会触发库重新编译
四、目标属性设置命令
这是现代 CMake 最强大的部分:所有设置都作用在目标上,而不是全局的。
4.1 target_compile_options(<目标> <可见性> <选项...>) --- 编译选项
cmake
target_compile_options(pid_offline PRIVATE -O0 -g -Wall)
4.2 target_include_directories(<目标> <可见性> <路径...>) --- 头文件搜索路径
cmake
# 假设头文件在 include/ 目录下
target_include_directories(pid_offline PRIVATE include)
等效于 gcc 的 -I include。
4.3 target_compile_definitions(<目标> <可见性> <宏...>) --- 定义预处理器宏
cmake
target_compile_definitions(pid_offline PRIVATE PID_DEBUG=1 VERSION="1.0")
等效于在源代码中写 #define PID_DEBUG 1 和 #define VERSION "1.0",或 gcc 的 -DPID_DEBUG=1。
4.4 target_link_libraries(<目标> <可见性> <库...>) --- 链接库
cmake
# 链接数学库
target_link_libraries(pid_offline PRIVATE m)
# 链接自己的库
target_link_libraries(pid_offline PRIVATE pidlib)
五、可见性关键字:PUBLIC / PRIVATE / INTERFACE
这是现代 CMake 的精髓,决定了依赖如何"传染"。
| 关键字 | 含义 | 使用场景 |
|---|---|---|
PRIVATE |
只用于当前目标的构建,不传染给依赖者 | 内部实现细节,如 -Wall |
INTERFACE |
不用于当前目标,但传染给依赖者 | 只有头文件的库 |
PUBLIC |
既用于当前目标,也传染给依赖者 | 当前目标头文件中暴露的依赖 |
示例:
cmake
# pidlib 是一个静态库,它的头文件 pid.h 中包含了 <math.h>
# 所以任何链接 pidlib 的目标也需要链接数学库
target_link_libraries(pidlib PUBLIC m)
# pid_offline 链接了 pidlib,因为 PUBLIC,它自动获得了 m
target_link_libraries(pid_offline PRIVATE pidlib)
# 不需要再写 target_link_libraries(pid_offline PRIVATE m)
可见性决策口诀:
- 你的
.c文件需要的 →PRIVATE - 你的
.h文件需要的 →PUBLIC - 你不编译代码,只是传递接口 →
INTERFACE
六、生成器表达式:$<...>
生成器表达式在生成构建系统时才求值,可以做更复杂的条件逻辑。
语法 :$<表达式>
常用表达式:
| 表达式 | 含义 |
|---|---|
$<CONFIG:Debug> |
如果当前配置是 Debug,返回 1 |
$<TARGET_FILE:目标> |
目标文件的完整路径 |
$<INSTALL_INTERFACE:路径> |
安装后的路径 |
示例:根据构建类型设置不同编译选项:
cmake
target_compile_options(pid_offline PRIVATE
$<$<CONFIG:Debug>:-O0;-g>
$<$<CONFIG:Release>:-O2>
)
Debug 时加 -O0 -g,Release 时加 -O2。
示例:在自定义命令中获取可执行文件路径:
cmake
add_custom_target(run
COMMAND $<TARGET_FILE:pid_offline> > result.csv
DEPENDS pid_offline
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
$<TARGET_FILE:pid_offline> 在构建时会被替换成 D:/Desktop/demoCmk/build/pid_offline.exe。
七、自定义命令与目标
7.1 add_custom_command() --- 添加自定义构建步骤
两种主要形式:
形式 1:生成文件
cmake
add_custom_command(
OUTPUT generated.c
COMMAND some_tool input.txt -o generated.c
DEPENDS input.txt
COMMENT "Generating source file..."
)
形式 2:作为目标的构建前/后事件
cmake
add_custom_command(
TARGET pid_offline POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:pid_offline> ${CMAKE_BINARY_DIR}/backup/
COMMENT "Backing up executable..."
)
PRE_BUILD:编译前PRE_LINK:链接前POST_BUILD:构建后(最常用)
7.2 add_custom_target() --- 添加纯命令目标
这就是你用的 run 目标。
cmake
add_custom_target(run
COMMAND ${CMAKE_COMMAND} -E echo "Running simulation..."
COMMAND $<TARGET_FILE:pid_offline> > ${CMAKE_BINARY_DIR}/output.csv
DEPENDS pid_offline
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating CSV..."
)
要点:
- 没有
OUTPUT,所以总是被视为过期的 ,每次--target run都会执行 COMMAND可以写多个,按顺序执行DEPENDS保证依赖的目标先构建WORKING_DIRECTORY设置执行时的当前目录
八、其他实用命令速览
8.1 file() --- 文件操作
cmake
# 收集所有 .c 文件(等同于 Makefile 的 wildcard)
file(GLOB SRC_FILES "*.c")
# 递归收集
file(GLOB_RECURSE ALL_SOURCES "src/*.c" "src/*.h")
# 读取文件内容到变量
file(READ "config.txt" CONFIG_CONTENT)
# 写入内容到文件
file(WRITE "output.txt" "Hello, CMake!")
注意 :file(GLOB) 很方便,但 CMake 官方不推荐用于收集源文件,因为新增文件后 CMake 不会自动检测到,需要手动重新运行 cmake -B build。
8.2 find_package() --- 查找外部库
cmake
# 查找并链接数学库
find_package(OpenGL REQUIRED)
target_link_libraries(my_app PRIVATE OpenGL::GL)
8.3 add_subdirectory() --- 引入子项目
cmake
# 项目结构:
# CMakeLists.txt (根)
# src/CMakeLists.txt
# tests/CMakeLists.txt
add_subdirectory(src)
add_subdirectory(tests)
每个子目录的 CMakeLists.txt 可以独立管理自己的目标。
8.4 include() --- 包含外部 CMake 文件
cmake
include(cmake/compiler_flags.cmake)
用于复用公共的编译设置等。
8.5 install() --- 安装规则
cmake
# 安装可执行文件到 bin/
install(TARGETS pid_offline RUNTIME DESTINATION bin)
# 安装头文件到 include/
install(FILES pid.h DESTINATION include)
然后用户可以通过 cmake --install build --prefix /usr/local 安装。
九、完整实战:PID 项目进阶 CMakeLists.txt
结合上面所有知识,这是你 PID 项目的"毕业版" CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.15)
project(PID_Sim VERSION 1.0.0 LANGUAGES C)
# ---------- 全局设置 ----------
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 可配置选项
option(ENABLE_SANITIZER "Enable AddressSanitizer" ON)
option(BUILD_TESTS "Build test programs" OFF)
# ---------- 核心算法库 ----------
add_library(pidlib STATIC pid.c)
target_include_directories(pidlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(pidlib PRIVATE -Wall -Wextra)
if(ENABLE_SANITIZER)
target_compile_options(pidlib PRIVATE -fsanitize=address)
target_link_libraries(pidlib PUBLIC -fsanitize=address)
endif()
# ---------- 离线仿真程序 ----------
add_executable(pid_offline main.c)
target_link_libraries(pid_offline PRIVATE pidlib)
target_compile_options(pid_offline PRIVATE -g -O0)
target_compile_definitions(pid_offline PRIVATE APP_VERSION="${PROJECT_VERSION}")
# ---------- 自定义任务 ----------
# 生成 CSV
add_custom_target(run
COMMAND $<TARGET_FILE:pid_offline> > ${CMAKE_BINARY_DIR}/output.csv
DEPENDS pid_offline
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Running PID simulation..."
)
# 内存检查
add_custom_target(check
COMMAND $<TARGET_FILE:pid_offline>
DEPENDS pid_offline
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Running with AddressSanitizer enabled..."
)
# ---------- 单元测试(可选) ----------
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# ---------- 安装规则 ----------
install(TARGETS pid_offline RUNTIME DESTINATION bin)
install(TARGETS pidlib ARCHIVE DESTINATION lib)
install(FILES pid.h DESTINATION include)
十、命令速查表
| 命令 | 作用 | 简记 |
|---|---|---|
cmake_minimum_required |
最低版本要求 | 第一行 |
project() |
项目定义 | 第二行 |
set() |
设置变量 | 存储 |
option() |
布尔选项 | 开关 |
message() |
打印信息 | 调试 |
if/elseif/else/endif |
条件 | 分支 |
foreach/while |
循环 | 遍历 |
add_executable() |
可执行文件 | 程序 |
add_library() |
库 | 复用 |
target_compile_options() |
编译选项 | -O -g |
target_include_directories() |
头文件路径 | -I |
target_compile_definitions() |
宏定义 | -D |
target_link_libraries() |
链接库 | -l |
add_custom_command() |
自定义构建步骤 | 生成文件 |
add_custom_target() |
自定义命令目标 | 运行脚本 |
find_package() |
查找外部库 | 依赖 |
add_subdirectory() |
子目录 | 模块化 |
file() |
文件操作 | 工具 |
install() |
安装规则 | 分发 |
十一、调试 CMakeLists.txt 的小技巧
-
打印变量值:
cmakemessage(STATUS "SOURCES = ${SOURCES}") message(STATUS "Build type = ${CMAKE_BUILD_TYPE}") -
打印所有缓存变量:
powershellcmake -B build -N -LAH-LAH列出所有缓存变量及其帮助信息。 -
使用
cmake --trace跟踪执行:powershellcmake -B build --trace会输出每一行 CMake 脚本的执行过程,用于定位复杂 bug。
-
查看构建生成的详细命令:
powershellcmake --build build --verbose或
cmake --build build -v,等价于make VERBOSE=1。
现在你手中的 CMake 知识,已经覆盖了从单文件编译到中型项目的全部需求。把上面的"毕业版" CMakeLists.txt 保存起来,未来的项目都可以以此为模板开始。CMake 最大的门槛已经跨过,剩下的只是经验的积累。