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)    # 删除缓存变量
我曾经踩过的坑: 有一次我定义了一个缓存变量,然后在子目录的CMakeLists.txt里用 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
重要提醒: CMake的列表索引从0开始。另外,列表元素是字符串,所以 list(APPEND) 时,如果元素包含空格,一定要用引号括起来,否则会被当成多个元素。

嗯,到这里,CMake最基础的语法操作就讲完了。你可能会觉得,就这?别急,这些是积木块。下一章,咱们就用这些积木块,搭出真正的项目结构来。

对了,list() 操作里有个 REMOVE_DUPLICATES,我在处理大型项目时经常用它来清理重复的源文件列表,效果拔群。你想想看,如果一不小心把同一个文件加了两次,链接器会报重复定义的错误,用这个命令就能提前规避。