本期,我们重点来学习一下CMake中的语法
相关项目示例代码在这里:CMake 学习: CMake工具开发介绍仓库,虽然标记的是C++
目录
[条件命令 if()](#条件命令 if())
[函数与宏:CMake 的灵魂](#函数与宏:CMake 的灵魂)
[macro vs function 核心差异](#macro vs function 核心差异)
[生成器表达式(Generator Expressions)](#生成器表达式(Generator Expressions))
[1. 目标管理](#1. 目标管理)
[2. 查找与包依赖](#2. 查找与包依赖)
[3. 安装与导出](#3. 安装与导出)
[4. 流程控制与调试](#4. 流程控制与调试)
命令调度
CMake 脚本由一系列命令调用组成,语法形态很简单:
bash
command(arg1 arg2 arg3 ...)
-
命令名 不区分大小写,但参数通常区分 (取决于命令本身)。社区规范:命令名用全小写。
-
参数之间用空白字符(空格、换行)分隔。
-
参数可以是字符串、变量引用、列表,或生成器表达式。
深刻要点 :CMake 没有真正的语句或表达式概念 ,一切都是命令。set(VAR value) 是一个命令,而不是赋值语句。
引号规则
这可能是 CMake 初学者最迷惑的地方。规则如下:
-
未加引号的参数:
a b c会被解析为三个独立参数。 -
加双引号的参数:
"a b c"是一个单个参数,内部可包含空格、分号等。 -
分号(
;)是 CMake 列表的内部分隔符,但在引号内分号被保留为字面字符。
bash
set(MY_LIST a b c) # MY_LIST 值为 "a;b;c" (隐式列表)
set(MY_STR "a b c") # MY_STR 值为 "a b c"
set(MY_LIST2 "a;b;c") # MY_LIST2 值也是 "a;b;c",因未加引号? 注意:引号内分号保留。
# 实际上 "a;b;c" 赋值后变量值是 "a;b;c" 字符串,但仍会被当作列表处。
专业心经:
-
当你需要保留空格时,务必加双引号。
-
在
if()条件中引用变量,永远加双引号,除非你清楚知道变量非空且不含空格。 -
列表通常不加引号传递,字符串通常加引号传递。
变量引用
bash
${VAR_NAME} # 标准替换
$ENV{HOME} # 引用环境变量
${CMAKE_VERSION}# 引用内部预定义变量
-
变量引用在命令执行前被展开(替换)。
-
如果
${VAR}引用了多个由分号分隔的值,在命令参数中会变成多个参数(除非加上引号)。
bash
set(FLAGS -Wall -Wextra) # FLAGS 值为 "-Wall;-Wextra"
add_compile_options(${FLAGS}) # 会被正确展开为两个参数
add_compile_options("${FLAGS}")# 不合理:单个参数 "-Wall;-Wextra"
注释
bash
# 这是注释
command(arg1 # 行内注释(非官方支持,慎用)
arg2) # 多行调用用换行和缩进
CMake 支持反斜杠续行,但很少需要,因为命令可自然跨多行。
字符串与列表
CMake 中一切皆是字符串,列表只是用分号(;)分隔的特殊字符串。
列表操作
bash
set(LIST a b c) # a;b;c
list(APPEND LIST d e) # a;b;c;d;e
list(LENGTH LIST len)
list(GET LIST 2 elem) # elem = c
foreach(item IN LISTS LIST) # 遍历
message("${item}")
endforeach()
-
分号是列表元素的分隔符。在未加引号时,
"a;b"也会被解析为两个参数。 -
常用命令:
list(REMOVE_ITEM ...),list(JOIN ...),list(FILTER ...)。
陷阱 :如果一个变量内容包含有分号,且不加引号传递,会被突然展开成多个参数 ,导致难以察觉的错误。优秀实践:给可能含有分号的变量加引号,如 "${MAYBE_LIST}"。
变量作用域
CMake 的作用域规则是静态作用域 + 动态作用域的混血儿(与多数语言不同)。
-
set(VAR value)默认在当前作用域(目录或函数)创建一个变量。 -
在
function中,默认会创建一个新的局部作用域,外部同名变量被隐藏。 -
在
macro中,没有新作用域,直接修改调用者的变量(类似 C 宏)。 -
若想修改父作用域的变量,需使用
set(VAR value PARENT_SCOPE)。
bash
function(my_func)
set(VAR 1 PARENT_SCOPE) # 影响调用者作用域
endfunction()
老手技巧 :利用 PARENT_SCOPE 可实现"返回值"。但函数内部无法直接读取修改后的父作用域变量,若需要既读又写,常用命名约定或 macro 替代。
控制流
条件命令 if()
bash
if(<constant>)
elseif(<condition>)
else()
endif()
条件表达式形式多样:
-
布尔常量:
ON,YES,TRUE,1表示真;OFF,NO,FALSE,0, 空字符串、未定义变量表示假。 -
比较:
EQUAL,LESS,GREATER,STREQUAL,MATCHES(正则)。 -
逻辑:
AND,OR,NOT。 -
存在性:
DEFINED VAR,TARGET my_tgt,COMMAND my_cmd。
金科玉律 :在 if() 中引用变量不要加 ${},直接写变量名:
bash
if(MYVAR)
# 判断 MYVAR 是否为真
else()
endif()
如果写 ${MYVAR},当变量未定义或为空时,if() 会变为 if(),这是语法错误。直接用变量名,未定义时等于假,安全。
循环命令
bash
while(condition)
# ...
break()
continue()
endwhile()
foreach(item IN LISTS LIST1 LIST2)
# item 为每个元素
endforeach()
foreach(range RANGE 0 10 2) # 0,2,4,6,8,10
endforeach()
函数与宏:CMake 的灵魂
CMake 提供 function 和 macro 实现代码复用,两者行为差异巨大,精于此道方显功力。
定义与调用
bash
function(my_function arg1 arg2)
# 函数体
endfunction()
macro(my_macro arg1 arg2)
# 宏体
endmacro()
调用语法一致:my_function(val1 val2)。
参数处理
CMake 函数/宏的参数不是传统语言的形参列表
bash
cmake_parse_arguments(
MY # 前缀
"FLAG1;FLAG2" # 选项(无值)
"SINGLE_VAL1;SINGLE" # 单值关键字
"MULTI_VAL1;LIBS" # 多值关键字
${ARGN}
)
,而是参数名用于接收传递进来的值 。如果需要处理可选参数、关键字参数,需使用 cmake_parse_arguments。
基础用法:
bash
function(my_func REQUIRED_ARG)
message("First argument: ${REQUIRED_ARG}")
message("All args: ${ARGV}, ARGV0=${ARGV0}, ARGC=${ARGC}")
endfunction()
my_func(a b c) # REQUIRED_ARG 拿到 "a",但 b c 不会丢失,可通过 ${ARGV1}、${ARGV2} 获取。
深度解析 :${ARGV} 是所有参数列表,${ARGC} 是参数个数。函数/宏内部可这样访问超过声明参数的额外参数。
cmake_parse_arguments:处理复杂参数
这是编写高质量 CMake 模块的必备技能,用于解析形如 PREFIX KEYWORD ... 的参数。
bash
cmake_parse_arguments(
MY # 前缀
"FLAG1;FLAG2" # 选项(无值)
"SINGLE_VAL1;SINGLE" # 单值关键字
"MULTI_VAL1;LIBS" # 多值关键字
${ARGN}
)
# 结果可用 MY_FLAG1, MY_SINGLE_VAL1, MY_LIBS 等
示例:
bash
function(add_my_library TARGET)
cmake_parse_arguments(MYLIB
"STATIC;NO_INSTALL" # 选项
"VERSION" # 单值
"SOURCES;DEPENDS;INCLUDES" # 多值
${ARGN}
)
# 使用 MYLIB_SOURCES, MYLIB_DEPENDS ...
if(MYLIB_STATIC)
add_library(${TARGET} STATIC ${MYLIB_SOURCES})
else()
add_library(${TARGET} ${MYLIB_SOURCES})
endif()
endfunction()
add_my_library(foo
SOURCES a.cpp b.cpp
DEPENDS bar baz
STATIC
)
macro vs function 核心差异
| 特性 | function |
macro |
|---|---|---|
| 作用域 | 创建新作用域,变量默认局部 | 无新作用域,直接修改调用者变量 |
| 参数传递 | 传递实际值(类似值传递) | 传递原文字符串(类似宏替换),未执行时替换 |
| 返回值 | 需用 PARENT_SCOPE |
直接 set 即可改变父作用域变量 |
| 递归调用 | 支持,每次独立作用域 | 小心变量名冲突,无作用域隔离 |
| 适用场景 | 大多数封装 | 需要修改外部变量,或实现真正的"代码生成" |
实战选择 :一般尽量用 function,避免意外污染外部变量。当你需要像 C 语言的 #define 那样直接操作调用处变量时,才用 macro。
函数参数中的引号与展开陷阱
由于 macro 是文本替换,它对引号和变量展开的处理与 function 截然不同,可能导致意外展开。一个经典踩坑案例:
bash
macro(my_macro arg)
message("Arg is: ${arg}")
endmacro()
my_macro("a;b") # arg 实际值是 a;b,但在宏展开时,因未加引号,message 会收到两个参数 a 和 b。
更安全的方式:在宏内部使用 ${arg} 时总是加上引号。但对列表,加引号又可能改变含义。因此,除非必须,推荐使用 function。
生成器表达式(Generator Expressions)
生成器表达式是在生成构建系统(如 Makefile)时 求值的表达式,不能直接用在所有命令中,只能用在支持它的属性或命令里。
格式:$<...>,例如:
bash
$<CONFIG:Debug> # 当构建类型为 Debug 时为真
$<TARGET_FILE:mylib> # 目标文件完整路径
$<BUILD_INTERFACE:include> # 构建接口时的路径
$<INSTALL_INTERFACE:include> # 安装接口路径
它解决了多配置、多平台下需要动态决定的选项问题,是构建系统高阶定制的核心。
实战中常用的关键命令分类
1. 目标管理
-
add_executable,add_library(STATIC / SHARED / INTERFACE / IMPORTED) -
target_sources,target_include_directories,target_compile_definitions,target_compile_features,target_link_libraries -
现代 CMake 基石:一切围绕目标,传播
PUBLIC/INTERFACE属性。
2. 查找与包依赖
-
find_package(Config / Module 模式) -
find_library,find_path,find_file(低级查找) -
include(CheckCXXSourceCompiles)等编译检查模块
3. 安装与导出
-
install(TARGETS ... EXPORT ...) -
install(EXPORT ...) -
configure_package_config_file,write_basic_package_version_file
4. 流程控制与调试
-
message,cmake_print_variables(CMake 3.15+) -
include,add_subdirectory -
option,set,list
注意:
-
总是为路径加引号 :如
"${CMAKE_CURRENT_SOURCE_DIR}/include",避免路径中有空格时断裂。 -
用
target_*命令代替全局命令 :target_include_directories优于include_directories,可控制传播范围。 -
使用
include(CMakePackageConfigHelpers)生成包文件,拒绝手动拼接路径。 -
小心变量的"隐式列表":如果一个变量内容可能包含分号,传递时务必思考是否加引号。
-
善用
cmake_parse_arguments设计函数接口,让函数调用清晰可读。 -
优先用
function,再用macro;需要修改外部变量时,通过函数 +PARENT_SCOPE组合。 -
尽量不依赖
GLOB获取源码列表:文件增删不会自动触发 CMake 重新配置,导致诡异错误。显式列出源码是维护性的投资。 -
理解生成器表达式,这是解决多配置(Debug/Release)编译选项不同的优雅方案。
本期内容就到这里了,请点个赞谢谢
封面图自取:
