3. CMake基础语法:注释、命令、变量、列表、流程控制

好,咱们正式开始啃CMake的语法。说实话,很多初学者一上来就被CMakeLists.txt里那些花里胡哨的写法吓住了。其实你把它拆开看,核心语法就那么几块——注释、命令、变量、列表,再加上if/foreach/while这几个流程控制。今天咱们就把这些基础打扎实。

3.1 注释:给代码加点说明

注释这事儿,我个人的习惯是「该写的地方一定要写,但别废话」。CMake支持三种注释方式:

  • 单行注释:用 # 开头,行内有效
  • 块注释:用 #[[]] 包裹,适合多行说明
  • 行尾注释:在命令后面加 # 跟上说明
# 这是一个单行注释
set(MY_VAR "hello")  # 行尾注释

#[[
  这是一个块注释
  可以跨多行
  适合写版权信息或大段说明
]]
我的小建议:块注释在CMake 3.0之后才支持。如果你要兼容老版本,还是老老实实用单行注释吧。我在一个嵌入式项目里吃过这个亏——同事用的CMake 2.8,块注释直接报错。

3.2 命令:CMake的基本操作单元

CMake里的命令,说白了就是函数调用。写法是:命令名(参数1 参数2 ...)。注意,命令名不区分大小写,但参数是区分大小写的。

常用的命令分几类:

  • 变量操作set()unset()
  • 信息输出message()
  • 文件操作file()configure_file()
  • 流程控制if()foreach()while()
  • 项目配置project()add_executable()add_library()
# 设置项目名称
project(MyProject VERSION 1.0.0)

# 输出信息
message(STATUS "项目名称: ${PROJECT_NAME}")

# 设置变量
set(SRC_FILES main.cpp utils.cpp)
注意:命令的参数之间用空格分隔,不要用逗号。我见过有人写成 set(MY_VAR, "hello"),结果MY_VAR的值变成了 "hello" 加上前面的逗号——嗯,踩过这个坑。

3.3 变量:存储和传递数据

CMake的变量,说白了就是字符串。但它的行为有点特殊——变量引用用 ${},设置用 set()

# 设置变量
set(MY_NAME "CMake教程")
set(MY_VERSION 3.20)

# 引用变量
message("名称: ${MY_NAME}, 版本: ${MY_VERSION}")

# 环境变量
message("HOME目录: $ENV{HOME}")

# 缓存变量(会写入CMakeCache.txt)
set(MY_CACHE_VAR "缓存值" CACHE STRING "这是一个缓存变量")

变量作用域这块,我简单说两句:

  • 普通变量:只在当前CMakeLists.txt及其子目录的add_subdirectory()中可见
  • 缓存变量:全局可见,会持久化到CMakeCache.txt
  • 环境变量:用 $ENV{} 访问,只读(除非用 set(ENV{...})
避坑指南:我曾经在一个大型项目中,因为没注意变量作用域,在子目录里修改了一个变量,结果父目录完全没感知到。后来我养成了一个习惯——跨目录传递变量,要么用缓存变量,要么用 PARENT_SCOPE 关键字。

3.4 列表:CMake里的数组

CMake没有真正的数组,它用「分号分隔的字符串」来模拟列表。你想想看,这其实挺巧妙的——字符串本身就是列表。

# 创建列表
set(MY_LIST a b c)          # 空格分隔,等价于 "a;b;c"
set(MY_LIST "a;b;c")        # 显式用分号

# 列表操作
list(LENGTH MY_LIST len)     # 获取长度
list(GET MY_LIST 0 first)    # 获取第0个元素
list(APPEND MY_LIST d e)     # 追加元素
list(REMOVE_ITEM MY_LIST b)  # 删除元素
list(SORT MY_LIST)           # 排序

# 遍历列表
foreach(item ${MY_LIST})
  message("元素: ${item}")
endforeach()

这里有个容易混淆的点:set(MY_LIST a b c)set(MY_LIST "a b c") 是不一样的。前者是三个元素的列表,后者是一个字符串。嗯,写代码的时候要留个心眼。

3.5 流程控制:让CMake有逻辑

3.5.1 if/elseif/else 条件判断

CMake的if语法,说实话跟其他语言不太一样。它没有大括号,用的是 if() ... endif() 这种配对方式。

set(USE_OPENGL ON)

if(USE_OPENGL)
  message("启用OpenGL支持")
  find_package(OpenGL REQUIRED)
elseif(USE_VULKAN)
  message("启用Vulkan支持")
  find_package(Vulkan REQUIRED)
else()
  message("使用软件渲染")
endif()

条件表达式支持:

  • if(DEFINED var):变量是否已定义
  • if(var STREQUAL "value"):字符串相等
  • if(var GREATER 10):数值比较
  • if(EXISTS file_path):文件是否存在
  • if(var MATCHES regex):正则匹配
我的经验:CMake的if判断中,ONYESTRUEY、非0数字都视为真。但为了可读性,我建议统一用 ONOFF

3.5.2 foreach 循环

foreach在CMake里用得特别多,尤其是处理源文件列表的时候。

# 遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(src ${SOURCES})
  message("编译源文件: ${src}")
endforeach()

# 带索引遍历
set(idx 0)
foreach(src ${SOURCES})
  message("源文件[${idx}]: ${src}")
  math(EXPR idx "${idx} + 1")
endforeach()

# 范围遍历
foreach(i RANGE 1 10)
  message("计数: ${i}")
endforeach()

# 同时遍历多个列表(CMake 3.17+)
foreach(src IN ZIP_LISTS SOURCES HEADERS)
  message("源文件: ${src_0}, 头文件: ${src_1}")
endforeach()

3.5.3 while 循环

while循环用得相对少一些,但某些场景下很实用,比如等待某个条件满足。

set(counter 0)
while(counter LESS 5)
  message("循环次数: ${counter}")
  math(EXPR counter "${counter} + 1")
endwhile()
注意:CMake的while循环没有 breakcontinue 关键字(至少在3.20之前没有)。所以写while循环的时候,一定要确保条件最终会变为假,否则就是死循环。我曾经在CI脚本里写过一次死循环,直接把构建机跑挂了...

3.6 知识体系总览

下面这张图,是我梳理的CMake基础语法知识体系。你可以把它当作一个「语法地图」,以后遇到问题,先看这张图定位一下属于哪个模块。

CMake基础语法知识体系 CMake语法核心 注释 命令 变量 列表 流程控制 单行 # 块注释 #[[ ]] 行尾注释 set() / unset() message() file() 普通变量 缓存变量 环境变量 创建/追加 获取/删除 排序/查找 if/elseif/else foreach while 核心要点 变量用 ${} 引用 · 列表本质是分号分隔字符串 · 命令不区分大小写 if/foreach/while 必须配对 endif()/endforeach()/endwhile()

3.7 实战小练习

光看不练假把式。我建议你动手写一个CMakeLists.txt,包含以下功能:

  1. 定义一个列表,包含三个源文件名
  2. 用foreach遍历列表,输出每个文件名
  3. 用if判断,如果某个文件存在,就输出「找到文件」
  4. 用while循环打印1到5的数字

写完之后,用 cmake -P 直接运行这个脚本(不需要项目结构)。嗯,这是最快验证语法的方式。

总结一下:CMake的语法其实不复杂,核心就是「命令+参数」的模式。变量和列表是数据载体,流程控制让脚本有了逻辑。把这些基础打牢,后面学高级特性就轻松多了。

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