3. CMake语法基础:注释、命令调用、变量、列表、字符串、数学表达式、流程控制
好,咱们正式开始接触CMake的语法。说实话,很多初学者一上来就被CMakeLists.txt里那些奇怪的写法搞懵了。其实CMake的语法并不复杂,它有自己的小脾气,摸透了就好。
我个人习惯把CMake语法分成两大块:基础元素和控制结构。基础元素就是注释、命令、变量这些砖块,控制结构就是if-else、循环这些水泥。今天咱们先把砖块码好。
核心观点:CMake语法本质上是「命令+参数」的模式。所有操作都是调用命令,参数可以是变量、字符串、列表。理解了这个,你就抓住了CMake的命门。
3.1 注释
注释这玩意儿,看着简单,但我在项目里见过不少坑。先看语法:
# 这是单行注释,用井号开头
# 我建议每段逻辑前都加一行注释,不然三个月后你自己都看不懂
# 多行注释?CMake没有原生的多行注释语法
# 但你可以用 if(FALSE) 来模拟
if(FALSE)
这里面的内容都不会被执行
相当于被注释掉了
但要注意,里面的字符串不能有未闭合的引号
endif()
我曾经在一个大型项目中,看到有人用一堆#号画了个大框来注释代码块。结果后来改需求时,框里的代码被误删了——因为根本没人注意到那是注释。嗯,这里要注意:注释是给人看的,不是给机器看的。别在注释里写太复杂的逻辑。
小技巧:用 # ---- 分隔线 ---- 这种形式来划分代码段落,比单纯用空行更清晰。我自己的项目里一直这么用。
3.2 命令调用
CMake里的一切都是命令调用。格式很简单:
命令名(参数1 参数2 参数3 ...)
命令名不区分大小写,但我个人习惯全小写。参数之间用空格分隔。看几个例子:
# 最基本的命令
message("Hello, CMake!")
# 带多个参数
add_executable(myapp main.cpp utils.cpp)
# 参数可以跨行
target_include_directories(myapp
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party
)
你想想看,为什么参数要跨行写?说白了就是为了可读性。当你的项目有几十个源文件时,全挤在一行里,谁看了都头疼。
注意:命令名和左括号之间不能有空格。写成 message ("hello") 会报错。我刚开始学CMake时就被这个坑过,排查了半天。
3.3 变量
变量是CMake的灵魂。定义变量用 set() 命令,引用变量用 ${}:
# 定义变量
set(MY_VAR "hello world")
# 引用变量
message(${MY_VAR}) # 输出:hello world
# 变量名是大小写敏感的
set(my_var "small")
set(MY_VAR "BIG")
message(${my_var}) # 输出:small
message(${MY_VAR}) # 输出:BIG
这里有个容易混淆的点:变量作用域。CMake的变量作用域是函数级的,不是文件级的。我在项目中遇到过这样的情况:在子目录的CMakeLists.txt里set了一个变量,回到父目录就找不到了。解决办法是用 set(... PARENT_SCOPE) 或者用缓存变量。
# 缓存变量,全局可见
set(MY_GLOBAL_VAR "I am global" CACHE STRING "Description")
# 父作用域变量
set(MY_LOCAL_VAR "I am local")
# 在子目录中想修改父目录的变量:
set(MY_LOCAL_VAR "modified" PARENT_SCOPE)
经验之谈:我一般只在顶层CMakeLists.txt里用缓存变量,子目录里尽量用普通变量+PARENT_SCOPE。这样变量传递路径清晰,出了问题好排查。
3.4 列表
列表在CMake里其实就是用分号分隔的字符串。你没看错,就是字符串。看例子:
# 定义一个列表
set(MY_LIST a b c) # 方式1:空格分隔
set(MY_LIST "a;b;c") # 方式2:分号分隔,效果一样
# 列表操作
list(LENGTH MY_LIST len)
list(GET MY_LIST 0 first_item)
list(APPEND MY_LIST d e f)
list(REMOVE_ITEM MY_LIST b)
message(${MY_LIST}) # 输出:a;c;d;e;f
为什么说列表就是字符串?因为 ${MY_LIST} 展开后就是 a;b;c。当你把它传给一个命令时,CMake会自动按分号拆分成多个参数。这就是CMake的「列表展开」机制。
我曾经踩过一个坑:
set(SOURCES "main.cpp;utils.cpp")
# 我想把SOURCES当成一个整体传给add_executable
add_executable(myapp ${SOURCES}) # 这会被展开成两个参数:main.cpp 和 utils.cpp
# 如果我想传一个包含分号的字符串,得用引号包起来
add_executable(myapp "${SOURCES}") # 这会把"main.cpp;utils.cpp"当成一个参数
嗯,这里要注意:引号会阻止列表展开。这是CMake里最容易出bug的地方之一。
3.5 字符串
字符串可以用双引号包起来,也可以不包。但区别很大:
# 不带引号:会被解析为命令或变量
set(foo bar) # foo的值是"bar"
message(${foo}) # 输出:bar
# 带引号:保留原样
set(foo "bar baz") # foo的值是"bar baz",注意中间有空格
message(${foo}) # 输出:bar baz
# 字符串拼接
set(hello "Hello, ")
set(world "World!")
set(greeting "${hello}${world}") # 输出:Hello, World!
字符串里可以转义:
# 转义字符
set(path "C:\\Program Files\\CMake") # 反斜杠需要转义
set(text "他说:\"你好\"") # 双引号需要转义
# 更推荐用方括号语法(CMake 3.0+)
set(path [=[C:\Program Files\CMake]=]) # 不用转义
set(text [=[他说:"你好"]=]) # 不用转义
建议:凡是包含空格、特殊字符的字符串,一律用双引号或方括号包起来。这是我在代码审查里反复强调的一条规则。
3.6 数学表达式
CMake支持基本的数学运算,用 math() 命令:
# 数学运算
math(EXPR result "1 + 2 * 3") # result = 7
math(EXPR result "(1 + 2) * 3") # result = 9
math(EXPR result "10 / 3") # result = 3(整数除法)
math(EXPR result "10 % 3") # result = 1(取模)
# 可以用变量
set(a 5)
set(b 3)
math(EXPR sum "${a} + ${b}") # sum = 8
说实话,CMake的数学运算能力很弱。只有整数运算,没有浮点数。如果你需要复杂计算,我建议用Python脚本生成CMake变量,再include进来。我在一个嵌入式项目里就这么干过——用Python算好内存地址,生成一个.cmake文件,然后include。
注意:CMake的数学表达式里,变量引用可以不加 ${},直接写变量名也行:math(EXPR sum "a + b")。但这容易混淆,我建议统一加 ${},保持风格一致。
3.7 流程控制
流程控制是CMake的「大脑」。主要有if-else、foreach、while三种。
3.7.1 if-else
set(USE_OPENGL ON)
if(USE_OPENGL)
message("Building with OpenGL support")
target_link_libraries(myapp OpenGL::GL)
elseif(USE_VULKAN)
message("Building with Vulkan support")
target_link_libraries(myapp Vulkan::Vulkan)
else()
message("Building without GPU acceleration")
endif()
条件判断的语法有点特别:
# 判断变量是否定义
if(DEFINED MY_VAR)
message("MY_VAR is defined")
endif()
# 判断字符串相等
set(compiler "gcc")
if(compiler STREQUAL "gcc")
message("Using GCC")
endif()
# 判断版本号
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11.0")
message("C++17 support guaranteed")
endif()
我曾经在项目里见过这样的代码:
if(WIN32)
# Windows-specific code
elseif(APPLE)
# macOS-specific code
elseif(UNIX)
# Linux-specific code
endif()
嗯,这里有个坑:APPLE 也是 UNIX,所以顺序很重要。正确的写法是先判断 APPLE,再判断 UNIX。不然macOS的代码永远不会被执行。
3.7.2 foreach循环
# 遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(src ${SOURCES})
message("Compiling: ${src}")
endforeach()
# 范围遍历
foreach(i RANGE 1 10)
message("Iteration ${i}")
endforeach()
# 带步长的范围遍历
foreach(i RANGE 0 10 2) # 0, 2, 4, 6, 8, 10
message("Even: ${i}")
endforeach()
foreach循环在生成大量目标时特别有用。我在一个微服务项目里,用foreach循环生成了20多个可执行文件,每个的编译选项略有不同。代码量从2000行缩减到了100行。
3.7.3 while循环
set(counter 0)
while(counter LESS 5)
message("Counter: ${counter}")
math(EXPR counter "${counter} + 1")
endwhile()
while循环用得不多,但偶尔有用。比如在解析配置文件时,需要循环读取直到文件末尾。
避坑指南:foreach和while里如果要修改列表,记得用 set() 配合 PARENT_SCOPE,否则修改只在循环内部生效。我曾经因为这个bug调试了一下午。
3.8 本章小结
好了,CMake的语法基础就这些。说白了就是:
- 注释:用#,别整花活
- 命令:命令名(参数),括号前不能有空格
- 变量:set定义,${}引用,注意作用域
- 列表:本质是分号分隔的字符串,引号会阻止展开
- 字符串:引号保空格,方括号免转义
- 数学:只有整数,别指望它做复杂计算
- 流程控制:if-else注意顺序,foreach最常用,while偶尔用
这些基础语法,你写个两三个CMakeLists.txt就熟练了。关键是理解CMake的「命令-参数」模型和变量展开机制。理解了这两点,后面学函数、模块、自定义命令就会轻松很多。
一句话总结:CMake语法不复杂,但细节多。每个细节都可能让你多调试两小时。记住我今天说的这些坑,能帮你省下不少时间。