CMake学习:CMake语法

本期,我们重点来学习一下CMake中的语法

相关项目示例代码在这里:CMake 学习: CMake工具开发介绍仓库,虽然标记的是C++

目录

命令调度

引号规则

变量引用

注释

字符串与列表

列表操作

变量作用域

控制流

[条件命令 if()](#条件命令 if())

循环命令

[函数与宏:CMake 的灵魂](#函数与宏:CMake 的灵魂)

定义与调用

参数处理

cmake_parse_arguments:处理复杂参数

[macro vs function 核心差异](#macro vs function 核心差异)

函数参数中的引号与展开陷阱

[生成器表达式(Generator Expressions)](#生成器表达式(Generator Expressions))

实战中常用的关键命令分类

[1. 目标管理](#1. 目标管理)

[2. 查找与包依赖](#2. 查找与包依赖)

[3. 安装与导出](#3. 安装与导出)

[4. 流程控制与调试](#4. 流程控制与调试)

注意:


命令调度

CMake 脚本由一系列命令调用组成,语法形态很简单:

bash 复制代码
command(arg1 arg2 arg3 ...)
  • 命令名 不区分大小写,但参数通常区分 (取决于命令本身)。社区规范:命令名用全小写

  • 参数之间用空白字符(空格、换行)分隔。

  • 参数可以是字符串、变量引用、列表,或生成器表达式。

深刻要点 :CMake 没有真正的语句或表达式概念 ,一切都是命令。set(VAR value) 是一个命令,而不是赋值语句。

引号规则

这可能是 CMake 初学者最迷惑的地方。规则如下:

  • 未加引号的参数:a b c 会被解析为三个独立参数

  • 加双引号的参数:"a b c" 是一个单个参数,内部可包含空格、分号等。

  • 分号(;)是 CMake 列表的内部分隔符,但在引号内分号被保留为字面字符

bash 复制代码
set(MY_LIST a b c)        # MY_LIST 值为 "a;b;c" (隐式列表)
set(MY_STR "a b c")       # MY_STR 值为 "a b c"
set(MY_LIST2 "a;b;c")     # MY_LIST2 值也是 "a;b;c",因未加引号? 注意:引号内分号保留。
                          # 实际上 "a;b;c" 赋值后变量值是 "a;b;c" 字符串,但仍会被当作列表处。

专业心经

  • 当你需要保留空格时,务必加双引号

  • if() 条件中引用变量,永远加双引号,除非你清楚知道变量非空且不含空格。

  • 列表通常不加引号传递,字符串通常加引号传递。

变量引用

bash 复制代码
${VAR_NAME}     # 标准替换
$ENV{HOME}      # 引用环境变量
${CMAKE_VERSION}# 引用内部预定义变量
  • 变量引用在命令执行前被展开(替换)。

  • 如果 ${VAR} 引用了多个由分号分隔的值,在命令参数中会变成多个参数(除非加上引号)。

bash 复制代码
set(FLAGS -Wall -Wextra)     # FLAGS 值为 "-Wall;-Wextra"
add_compile_options(${FLAGS})  # 会被正确展开为两个参数
add_compile_options("${FLAGS}")# 不合理:单个参数 "-Wall;-Wextra"

注释

bash 复制代码
# 这是注释
command(arg1 # 行内注释(非官方支持,慎用)
        arg2)   # 多行调用用换行和缩进

CMake 支持反斜杠续行,但很少需要,因为命令可自然跨多行。

字符串与列表

CMake 中一切皆是字符串,列表只是用分号(;)分隔的特殊字符串。

列表操作

bash 复制代码
set(LIST a b c)             # a;b;c
list(APPEND LIST d e)       # a;b;c;d;e
list(LENGTH LIST len)
list(GET LIST 2 elem)       # elem = c
foreach(item IN LISTS LIST) # 遍历
    message("${item}")
endforeach()
  • 分号是列表元素的分隔符。在未加引号时,"a;b" 也会被解析为两个参数。

  • 常用命令:list(REMOVE_ITEM ...), list(JOIN ...), list(FILTER ...)

陷阱 :如果一个变量内容包含有分号,且不加引号传递,会被突然展开成多个参数 ,导致难以察觉的错误。优秀实践:给可能含有分号的变量加引号,如 "${MAYBE_LIST}"

变量作用域

CMake 的作用域规则是静态作用域 + 动态作用域的混血儿(与多数语言不同)。

  • set(VAR value) 默认在当前作用域(目录或函数)创建一个变量。

  • function 中,默认会创建一个新的局部作用域,外部同名变量被隐藏。

  • macro 中,没有新作用域,直接修改调用者的变量(类似 C 宏)。

  • 若想修改父作用域的变量,需使用 set(VAR value PARENT_SCOPE)

bash 复制代码
function(my_func)
    set(VAR 1 PARENT_SCOPE)  # 影响调用者作用域
endfunction()

老手技巧 :利用 PARENT_SCOPE 可实现"返回值"。但函数内部无法直接读取修改后的父作用域变量,若需要既读又写,常用命名约定或 macro 替代。

控制流

条件命令 if()

bash 复制代码
if(<constant>)
elseif(<condition>)
else()
endif()

条件表达式形式多样:

  • 布尔常量:ON, YES, TRUE, 1 表示真;OFF, NO, FALSE, 0, 空字符串、未定义变量表示假。

  • 比较:EQUAL, LESS, GREATER, STREQUAL, MATCHES(正则)。

  • 逻辑:AND, OR, NOT

  • 存在性:DEFINED VAR, TARGET my_tgt, COMMAND my_cmd

金科玉律 :在 if() 中引用变量不要加 ${},直接写变量名:

bash 复制代码
if(MYVAR)
    # 判断 MYVAR 是否为真
else()
endif()

如果写 ${MYVAR},当变量未定义或为空时,if() 会变为 if(),这是语法错误。直接用变量名,未定义时等于假,安全。

循环命令

bash 复制代码
while(condition)
    # ...
    break()
    continue()
endwhile()

foreach(item IN LISTS LIST1 LIST2)
    # item 为每个元素
endforeach()

foreach(range RANGE 0 10 2)  # 0,2,4,6,8,10
endforeach()

函数与宏:CMake 的灵魂

CMake 提供 functionmacro 实现代码复用,两者行为差异巨大,精于此道方显功力。

定义与调用

bash 复制代码
function(my_function arg1 arg2)
    # 函数体
endfunction()

macro(my_macro arg1 arg2)
    # 宏体
endmacro()

调用语法一致:my_function(val1 val2)

参数处理

CMake 函数/宏的参数不是传统语言的形参列表

bash 复制代码
cmake_parse_arguments(
    MY                      # 前缀
    "FLAG1;FLAG2"           # 选项(无值)
    "SINGLE_VAL1;SINGLE"    # 单值关键字
    "MULTI_VAL1;LIBS"       # 多值关键字
    ${ARGN}
)

,而是参数名用于接收传递进来的值 。如果需要处理可选参数、关键字参数,需使用 cmake_parse_arguments

基础用法:

bash 复制代码
function(my_func REQUIRED_ARG)
    message("First argument: ${REQUIRED_ARG}")
    message("All args: ${ARGV}, ARGV0=${ARGV0}, ARGC=${ARGC}")
endfunction()
my_func(a b c)  # REQUIRED_ARG 拿到 "a",但 b c 不会丢失,可通过 ${ARGV1}、${ARGV2} 获取。

深度解析${ARGV} 是所有参数列表,${ARGC} 是参数个数。函数/宏内部可这样访问超过声明参数的额外参数。

cmake_parse_arguments:处理复杂参数

这是编写高质量 CMake 模块的必备技能,用于解析形如 PREFIX KEYWORD ... 的参数。

bash 复制代码
cmake_parse_arguments(
    MY                      # 前缀
    "FLAG1;FLAG2"           # 选项(无值)
    "SINGLE_VAL1;SINGLE"    # 单值关键字
    "MULTI_VAL1;LIBS"       # 多值关键字
    ${ARGN}
)
# 结果可用 MY_FLAG1, MY_SINGLE_VAL1, MY_LIBS 等

示例:

bash 复制代码
function(add_my_library TARGET)
    cmake_parse_arguments(MYLIB
        "STATIC;NO_INSTALL"            # 选项
        "VERSION"                      # 单值
        "SOURCES;DEPENDS;INCLUDES"     # 多值
        ${ARGN}
    )
    # 使用 MYLIB_SOURCES, MYLIB_DEPENDS ...
    if(MYLIB_STATIC)
        add_library(${TARGET} STATIC ${MYLIB_SOURCES})
    else()
        add_library(${TARGET} ${MYLIB_SOURCES})
    endif()
endfunction()

add_my_library(foo
    SOURCES a.cpp b.cpp
    DEPENDS bar baz
    STATIC
)

macro vs function 核心差异

特性 function macro
作用域 创建新作用域,变量默认局部 无新作用域,直接修改调用者变量
参数传递 传递实际值(类似值传递) 传递原文字符串(类似宏替换),未执行时替换
返回值 需用 PARENT_SCOPE 直接 set 即可改变父作用域变量
递归调用 支持,每次独立作用域 小心变量名冲突,无作用域隔离
适用场景 大多数封装 需要修改外部变量,或实现真正的"代码生成"

实战选择 :一般尽量用 function,避免意外污染外部变量。当你需要像 C 语言的 #define 那样直接操作调用处变量时,才用 macro

函数参数中的引号与展开陷阱

由于 macro 是文本替换,它对引号和变量展开的处理与 function 截然不同,可能导致意外展开。一个经典踩坑案例:

bash 复制代码
macro(my_macro arg)
    message("Arg is: ${arg}")
endmacro()
my_macro("a;b")  # arg 实际值是 a;b,但在宏展开时,因未加引号,message 会收到两个参数 a 和 b。

更安全的方式:在宏内部使用 ${arg} 时总是加上引号。但对列表,加引号又可能改变含义。因此,除非必须,推荐使用 function

生成器表达式(Generator Expressions)

生成器表达式是在生成构建系统(如 Makefile)时 求值的表达式,不能直接用在所有命令中,只能用在支持它的属性或命令里。

格式:$<...>,例如:

bash 复制代码
$<CONFIG:Debug>              # 当构建类型为 Debug 时为真
$<TARGET_FILE:mylib>         # 目标文件完整路径
$<BUILD_INTERFACE:include>   # 构建接口时的路径
$<INSTALL_INTERFACE:include> # 安装接口路径

它解决了多配置、多平台下需要动态决定的选项问题,是构建系统高阶定制的核心。

实战中常用的关键命令分类

1. 目标管理

  • add_executable, add_library (STATIC / SHARED / INTERFACE / IMPORTED)

  • target_sources, target_include_directories, target_compile_definitions, target_compile_features, target_link_libraries

  • 现代 CMake 基石:一切围绕目标,传播 PUBLIC/INTERFACE 属性。

2. 查找与包依赖

  • find_package (Config / Module 模式)

  • find_library, find_path, find_file (低级查找)

  • include(CheckCXXSourceCompiles) 等编译检查模块

3. 安装与导出

  • install(TARGETS ... EXPORT ...)

  • install(EXPORT ...)

  • configure_package_config_file, write_basic_package_version_file

4. 流程控制与调试

  • message, cmake_print_variables(CMake 3.15+)

  • include, add_subdirectory

  • option, set, list

注意:

  1. 总是为路径加引号 :如 "${CMAKE_CURRENT_SOURCE_DIR}/include",避免路径中有空格时断裂。

  2. target_* 命令代替全局命令target_include_directories 优于 include_directories,可控制传播范围。

  3. 使用 include(CMakePackageConfigHelpers) 生成包文件,拒绝手动拼接路径。

  4. 小心变量的"隐式列表":如果一个变量内容可能包含分号,传递时务必思考是否加引号。

  5. 善用 cmake_parse_arguments 设计函数接口,让函数调用清晰可读。

  6. 优先用 function,再用 macro ;需要修改外部变量时,通过函数 + PARENT_SCOPE 组合。

  7. 尽量不依赖 GLOB 获取源码列表:文件增删不会自动触发 CMake 重新配置,导致诡异错误。显式列出源码是维护性的投资。

  8. 理解生成器表达式,这是解决多配置(Debug/Release)编译选项不同的优雅方案。

本期内容就到这里了,请点个赞谢谢

封面图自取:

相关推荐
无限进步_2 小时前
C++ 继承机制完全解析:从基础原理到菱形继承问题
java·开发语言·数据结构·c++·vscode·后端·算法
nashane2 小时前
HarmonyOS 6学习:加密一致性与安全存储——AES GCM排查与SaveButton实践
学习·安全·harmonyos·harmony app
武子康2 小时前
大数据-278 Spark MLib-GBDT梯度提升决策树详解:从原理到实战案例
大数据·后端·spark
盐焗鹌鹑蛋2 小时前
【C++】vector类
c++
SamDeepThinking2 小时前
适合中小型企业的出口入口网关微服务
java·后端·架构
周末也要写八哥2 小时前
编程初学者学习:句柄(二)
学习
jf加菲猫2 小时前
第15章 文件和目录
开发语言·c++·qt·ui
思麟呀2 小时前
Select多路转接
linux·网络·c++·网络协议·http
aq55356002 小时前
开源吐槽大会:让技术痛点变笑点
c++·mfc