3. CMake基础语法:注释、命令、变量、列表、流程控制
好,咱们正式开始啃CMake的语法。说实话,很多初学者一上来就被CMakeLists.txt里那些花里胡哨的写法吓住了。其实你把它拆开看,核心语法就那么几块——注释、命令、变量、列表,再加上if/foreach/while这几个流程控制。今天咱们就把这些基础打扎实。
3.1 注释:给代码加点说明
注释这事儿,我个人的习惯是「该写的地方一定要写,但别废话」。CMake支持三种注释方式:
- 单行注释:用
#开头,行内有效 - 块注释:用
#[[和]]包裹,适合多行说明 - 行尾注释:在命令后面加
#跟上说明
# 这是一个单行注释
set(MY_VAR "hello") # 行尾注释
#[[
这是一个块注释
可以跨多行
适合写版权信息或大段说明
]]
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):正则匹配
ON、YES、TRUE、Y、非0数字都视为真。但为了可读性,我建议统一用 ON 和 OFF。
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()
break 和 continue 关键字(至少在3.20之前没有)。所以写while循环的时候,一定要确保条件最终会变为假,否则就是死循环。我曾经在CI脚本里写过一次死循环,直接把构建机跑挂了...
3.6 知识体系总览
下面这张图,是我梳理的CMake基础语法知识体系。你可以把它当作一个「语法地图」,以后遇到问题,先看这张图定位一下属于哪个模块。
3.7 实战小练习
光看不练假把式。我建议你动手写一个CMakeLists.txt,包含以下功能:
- 定义一个列表,包含三个源文件名
- 用foreach遍历列表,输出每个文件名
- 用if判断,如果某个文件存在,就输出「找到文件」
- 用while循环打印1到5的数字
写完之后,用 cmake -P 直接运行这个脚本(不需要项目结构)。嗯,这是最快验证语法的方式。
公众号:蓝海资料掘金营,微信deep3321