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语法不复杂,但细节多。每个细节都可能让你多调试两小时。记住我今天说的这些坑,能帮你省下不少时间。

CMake语法基础知识体系 CMake语法 基础元素 数据结构 控制结构 注释 (#) 命令调用 变量 (set/${}) 列表 (list) 字符串 数学表达式 if-else foreach while 核心:命令 + 参数 = 一切 变量展开机制是理解CMake的关键

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