3、CMake语法基础:注释、变量定义与引用、set()与unset()、message()调试输出、list()操作
好,咱们正式开始写CMake了。说实话,很多初学者一上来就盯着那些复杂的模块和第三方库,结果连最基本的变量怎么赋值都没搞明白。我个人习惯是,先把语法地基打牢,后面写多复杂的CMakeLists.txt都不慌。
这一章,咱们就聚焦在CMake最核心的语法元素上。你想想看,写代码无非就是定义数据、操作数据、然后输出调试信息。CMake也一样,只不过它的“数据”就是变量和列表。
3.1 注释:给未来的自己留个话
注释这事儿,看着简单,但我在项目中见过太多“此地无银三百两”的注释了。CMake支持两种注释方式:
- 单行注释:使用
#符号,从该符号开始到行尾的内容都会被忽略。 - 块注释(括号注释):使用
#[[和]]包裹起来的内容,可以跨越多行。
# 这是一个单行注释,说明下面这行是干啥的
set(MY_VAR "hello")
#[[
这是一个块注释
可以写很多行
比如解释一个复杂的逻辑
或者临时屏蔽一大段代码
]]
message("这条消息会输出")
#[[ 和 ]] 把半个CMakeLists.txt给注释掉,然后一段一段放开,很快就定位到了问题。比一行一行加 # 快多了。
3.2 变量定义与引用:CMake的“内存条”
变量是CMake里最基础的数据单元。说白了,就是给一个值起个名字,方便后面反复用。
定义变量: 直接用 set() 命令。
引用变量: 用 ${变量名} 这种形式。注意,花括号不能省,这是CMake的规矩。
# 定义一个字符串变量
set(PROJECT_NAME "MyAwesomeApp")
# 定义一个路径变量
set(SOURCE_DIR "/home/user/src")
# 引用变量
message("项目名称是:${PROJECT_NAME}")
message("源码路径是:${SOURCE_DIR}")
这里有个坑,我刚开始学的时候踩过。如果变量名本身包含特殊字符,或者你想引用一个可能没定义的变量,CMake会怎么处理?它会直接当成空字符串。所以,引用前最好确保变量已经定义,或者用 if(DEFINED ...) 检查一下。
3.3 set() 与 unset():变量的生与死
set() 是创造变量,unset() 就是销毁变量。这两个命令是成对出现的。
set() 的完整语法其实比你想的丰富:
set(<variable> <value> [CACHE <type> <docstring> [FORCE]])
咱们先看最常用的几种用法:
| 用法 | 示例 | 说明 |
|---|---|---|
| 普通变量 | set(MY_VAR "hello") |
在当前作用域内有效 |
| 缓存变量 | set(MY_VAR "hello" CACHE STRING "描述") |
会出现在cmake-gui中,用户可修改 |
| 环境变量 | set(ENV{PATH} "/usr/local/bin") |
修改系统环境变量(慎用) |
unset() 的用法更简单:
unset(MY_VAR) # 删除普通变量
unset(MY_VAR CACHE) # 删除缓存变量
set() 修改了它,结果死活不生效。后来才发现,缓存变量一旦被设置,除非加 FORCE 关键字,否则 set() 不会覆盖它。所以,如果你想让用户通过cmake-gui配置,就用缓存变量;如果只是内部逻辑用,普通变量就够了。
3.4 message() 调试输出:你的“printf”
写CMake最痛苦的是什么?就是出了问题不知道变量值是多少。这时候 message() 就是你的救星。它相当于C语言里的 printf,能把信息打印到终端上。
基本语法:
message([<mode>] "消息内容")
<mode> 参数控制消息的显示方式和行为:
| 模式 | 效果 | 典型场景 |
|---|---|---|
(无) |
普通消息,直接输出 | 日常调试 |
STATUS |
带前缀 -- 的消息 |
告知用户当前进度 |
WARNING |
警告,不会终止 | 提醒用户注意潜在问题 |
AUTHOR_WARNING |
开发者警告(后续版本可能废弃) | 标记过时用法 |
SEND_ERROR |
错误,继续执行但跳过生成 | 严重问题,但还想看后续错误 |
FATAL_ERROR |
致命错误,立即终止 | 条件不满足,无法继续 |
DEPRECATION |
弃用警告 | 提示用户某个功能即将移除 |
# 普通输出
message("Hello, CMake!")
# 带状态前缀
message(STATUS "正在检测编译器...")
# 警告
message(WARNING "你使用的CMake版本较旧,建议升级")
# 致命错误
if(NOT EXISTS "${SOURCE_DIR}")
message(FATAL_ERROR "源码目录 ${SOURCE_DIR} 不存在!")
endif()
message(STATUS "变量值: ${VAR}");第二斧,用 message(STATUS "--- 分割线 ---") 标记代码段;第三斧,实在不行就上 message(FATAL_ERROR "停在这里看输出")。这三招帮我解决了90%的CMake问题。
3.5 list() 操作:管理你的数据集合
单个变量不够用怎么办?用列表。CMake里的列表,说白了就是用分号 ; 分隔的字符串。比如 a;b;c 就是一个包含三个元素的列表。
list() 命令提供了丰富的操作:
list(LENGTH <list> <output_variable>)
list(GET <list> <index> [<index> ...] <output_variable>)
list(APPEND <list> [<element> ...])
list(FIND <list> <value> <output_variable>)
list(INSERT <list> <index> [<element> ...])
list(REMOVE_ITEM <list> <value> [<value> ...])
list(REMOVE_AT <list> <index> [<index> ...])
list(REMOVE_DUPLICATES <list>)
list(REVERSE <list>)
list(SORT <list>)
来看个完整的例子:
# 定义一个源文件列表
set(SOURCES main.cpp utils.cpp network.cpp)
# 获取列表长度
list(LENGTH SOURCES NUM_SOURCES)
message(STATUS "源文件数量: ${NUM_SOURCES}")
# 获取第一个元素
list(GET SOURCES 0 FIRST_SOURCE)
message(STATUS "第一个源文件: ${FIRST_SOURCE}")
# 追加元素
list(APPEND SOURCES database.cpp)
message(STATUS "追加后的列表: ${SOURCES}")
# 查找元素位置
list(FIND SOURCES "utils.cpp" INDEX)
message(STATUS "utils.cpp 的索引: ${INDEX}")
# 删除元素
list(REMOVE_ITEM SOURCES "network.cpp")
message(STATUS "删除后的列表: ${SOURCES}")
# 排序
list(SORT SOURCES)
message(STATUS "排序后的列表: ${SOURCES}")
输出结果:
-- 源文件数量: 3
-- 第一个源文件: main.cpp
-- 追加后的列表: main.cpp;utils.cpp;network.cpp;database.cpp
-- utils.cpp 的索引: 1
-- 删除后的列表: main.cpp;utils.cpp;database.cpp
-- 排序后的列表: database.cpp;main.cpp;utils.cpp
list(APPEND) 时,如果元素包含空格,一定要用引号括起来,否则会被当成多个元素。
嗯,到这里,CMake最基础的语法操作就讲完了。你可能会觉得,就这?别急,这些是积木块。下一章,咱们就用这些积木块,搭出真正的项目结构来。
对了,list() 操作里有个 REMOVE_DUPLICATES,我在处理大型项目时经常用它来清理重复的源文件列表,效果拔群。你想想看,如果一不小心把同一个文件加了两次,链接器会报重复定义的错误,用这个命令就能提前规避。