CMake入门——进阶使用

CMake 进阶

在前面的学习中,我们已经掌握了 CMake 的基本使用方法。本文章将进一步深入学习 CMake 的进阶功能,包括函数定义、宏定义、文件操作以及交叉编译配置等内容。


1. 定义函数

CMake 提供了 function() 命令用于定义函数,基本语法如下:

function(<name> [arg1 [arg2 [arg3 ...]]])
  command1(args ...)
  command2(args ...)
  ...
endfunction(<name>)

其中 <name> 为函数名,arg1arg2…为函数的形式参数。endfunction 括号中的 <name> 可写可不写,如果写了则必须与 function 括号中的 <name> 一致。调用函数的方式与使用命令一样。

1.1 基本使用方法

# 定义函数 xyz
function(xyz arg1 arg2)
    message("${arg1} ${arg2}")
endfunction()

# 调用函数
xyz(Hello World)

打印结果:

Hello World

1.2 使用 return() 退出函数

在函数中可以使用 return() 命令退出函数,类似于 C 语言中的 return 语句。但需要注意的是,return() 并不能用于返回参数给调用者。

function(xyz)
    message(Hello)
    return()    # 退出函数
    message(World)
endfunction()

xyz()

执行结果只会打印 Hello,不会打印 World,说明 return() 执行后即退出了当前函数。

1.3 可变参函数

在 CMake 中,调用函数时实际传入的参数个数可以大于函数定义的参数个数(甚至函数定义时参数个数为 0),但实际传入的参数个数必须大于或等于函数定义的参数个数。例如:

function(xyz arg1)
    message(${arg1})
endfunction()

xyz(Hello World China)

打印结果:

Hello

函数 xyz 定义时只有一个参数,但实际传入 3 个参数,这并不会报错,message() 会打印出第一个参数 Hello

这种设计可用于实现可变参函数(类似于 C 语言中的可变参数函数)。要通过这些额外参数,需要借助函数内部变量。

1.4 函数的内部变量

function() 函数中可以使用以下内部变量:

内部变量 说明
ARGVX X 为数字(如 ARGV0ARGV1ARGV2…),表示函数的第 X 个参数
ARGV 存放实际调用时传入的所有参数(多个参数时为参数列表)
ARGN 若定义时参数为 2 个,实际传入 4 个,则 ARGN 存放剩余 2 个参数(参数列表)
ARGC 实际传入的参数个数

示例:

function(xyz arg1 arg2)
    message("ARGC: ${ARGC}")
    message("ARGV: ${ARGV}")
    message("ARGN: ${ARGN}")
    message("ARGV0: ${ARGV0}")
    message("ARGV1: ${ARGV1}")

    # 循环打印出各个参数
    set(i 0)
    foreach(loop ${ARGV})
        message("arg${i}: " ${loop})
        math(EXPR i "${i} + 1")
    endforeach()
endfunction()

xyz(A B C D E F G)

打印结果:

ARGC: 7
ARGV: A;B;C;D;E;F;G
ARGN: C;D;E;F;G
ARGV0: A
ARGV1: B
arg0: A
arg1: B
arg2: C
arg3: D
arg4: E
arg5: F
arg6: G

由此可见,函数定义了两个形参 arg1arg2,实际传入了 7 个参数(A~G),因此 ARGC 为 7,ARGV 包含了所有传入的参数,ARGN 则存放了超出形参个数的剩余 5 个参数(C~G)。通过 ARGV0ARGV1 等可以单独访问每一个参数,也可结合 foreach 循环遍历 ARGV 列表。

1.5 函数的作用域

通过 function() 定义的函数具有全局作用域。也就是说,父源码中定义的函数可以在子源码中调用,子源码中定义的函数也可以在父源码中调用。

示例 1:父源码中定义函数,子源码中调用

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(HELLO VERSION 1.1.0)

function(xyz)
    message("Hello World!")
endfunction()

add_subdirectory(hello)

hello/CMakeLists.txt

message("这是子源码")
xyz()       # 调用父源码中定义的 xyz() 函数

示例 2:子源码中定义函数,父源码中调用

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(HELLO VERSION 1.1.0)

add_subdirectory(hello)
message("这是父源码")
xyz()

hello/CMakeLists.txt

message("这是子源码")

function(xyz)
    message("Hello World!")
endfunction()

两种方式均可正常调用,说明通过 function() 定义的函数是全局有效的,不局限于当前源码文件。


2. 宏定义

CMake 提供了 macro() 命令用于定义宏,从某种程度上来说,宏(macro)和函数(function)是类似的,都是创建一段有名字的代码以便后续调用,并且都可以传递参数。

macro(<name> [arg1 [arg2 [arg3 ...]]])
  COMMAND1(ARGS ...)
  COMMAND2(ARGS ...)
  ...
endmacro(<name>)

endmacro 括号中的 <name> 可写可不写,如果写了则必须与 macro 括号中的 <name> 一致。

宏同样支持 ARGVX(X 为数字)、ARGCARGVARGN 这些内部变量。

2.1 宏的基本使用

macro(XYZ arg1 arg2)
    message("ARGC: ${ARGC}")
    message("ARGV: ${ARGV}")
    message("ARGN: ${ARGN}")
    message("ARGV0: ${ARGV0}")
    message("ARGV1: ${ARGV1}")

    set(i 0)
    foreach(loop ${ARGV})
        message("arg${i}: " ${loop})
        math(EXPR i "${i} + 1")
    endforeach()
endmacro()

XYZ(A B C D E)

打印结果:

ARGC: 5
ARGV: A;B;C;D;E
ARGN: C;D;E
ARGV0: A
ARGV1: B
arg0: A
arg1: B
arg2: C
arg3: D
arg4: E

2.2 宏与函数的区别

虽然宏和函数在定义上看起来几乎一样,但它们之间存在重要区别:

区别一:宏的参数和内部变量是字符串替换,而非真正的变量

在宏定义中,宏的参数以及 ARGCARGVARGN 等值并不是真正的 CMake 变量,而是字符串替换。当 CMake 执行宏时,会先将这些标识符替换为对应的字符串值,然后再执行宏代码——类似于 C 语言预处理器的行为。

因此,宏中不能使用像 if(ARGV1)if(DEFINED ARGC)foreach(loop_var IN LISTS ARGN) 这样的命令,因为这些标识符在宏中不是变量。

验证对比:

macro(abc arg1 arg2)
    if(DEFINED ARGC)
        message(true)
    else()
        message(false)
    endif()
endmacro()

function(xyz arg1 arg2)
    if(DEFINED ARGC)
        message(true)
    else()
        message(false)
    endif()
endfunction()

abc(A B C D)   # 打印 false — ARGC 不是变量
xyz(A B C D)   # 打印 true  — ARGC 是变量

abcARGC 不是变量,在执行宏之前会被替换为实际值(例如 4),因此 if(DEFINED ARGC) 会判定为 false。而函数中 ARGC 是真正的变量,因此判定为 true。

区别二:作用域

这里需要区分两个层面的"作用域":

  • 变量作用域:函数有自己的变量作用域,函数内 set() 的变量是局部变量,不会影响调用者。而宏没有独立的变量作用域,宏内设置的变量会直接渗透到调用者的作用域中(因为宏本质是字符串替换,代码展开在调用处)。

  • 定义可见性:在这一层面上,函数和宏是一样的——它们都是全局可见的。与函数类似,父源码中定义的宏可以在子源码中调用,子源码中定义的宏也可以在父源码中调用。

# 演示宏的变量渗透
macro(set_xyz)
    set(my_var "Hello from macro")  # 这个变量会渗透到调用者作用域
endmacro()

function(set_abc)
    set(my_var "Hello from function")  # 这个变量是函数局部变量,不会影响调用者
endfunction()

set_xyz()
message("${my_var}")  # 输出: Hello from macro(宏设置的变量仍然存在)

set_abc()
message("${my_var}")  # 仍然输出: Hello from macro(函数内设置的变量未渗透出来)

2.3 如何选择

  • 如果需要封装一段代码并希望它有自己的变量作用域(不污染调用者),应使用函数(function)。
  • 如果需要一段代码与调用者共享作用域,或需要类似 C 语言宏的行为(字符串替换),可使用宏(macro)。

3. 文件操作

CMake 提供了功能强大的 file() 命令,可以对文件进行读写、删除、重命名、创建目录等一系列操作。

3.1 写文件:写入与追加内容

file(WRITE <filename> <content>...)
file(APPEND <filename> <content>...)
  • WRITE:将 <content> 写入 <filename>。如果文件不存在则创建;如果文件已存在则覆盖。
  • APPEND:将 <content> 追加到 <filename> 末尾。

文件路径可使用绝对路径或相对路径,相对路径解释为相对于当前源码路径

file(WRITE wtest.txt "Hello World!")    # 生成 wtest.txt 文件
file(APPEND wtest.txt " China")         # 追加内容到文件末尾

3.2 写文件:由内容生成文件

file(GENERATE OUTPUT output-file
     <INPUT input-file|CONTENT content>
     [CONDITION expression])
  • output-file:输出文件名,可带路径。
  • INPUT input-file:指定输入文件,以输入文件的内容生成输出文件。
  • CONTENT content:直接指定内容生成输出文件。
  • CONDITION expression:条件表达式,为真时生成文件,否则不生成。

注意:相对路径解释为相对于当前源码的 BINARY_DIR 路径(即 build 目录),而非当前源码路径。

# 由 wtest.txt 内容生成 out1.txt
file(GENERATE OUTPUT out1.txt INPUT "${PROJECT_SOURCE_DIR}/wtest.txt")

# 由指定内容生成 out2.txt
file(GENERATE OUTPUT out2.txt CONTENT "This is the out2.txt file")

# 由指定内容生成 out3.txt,带条件控制
file(GENERATE OUTPUT out3.txt CONTENT "This is the out3.txt file" CONDITION 1)

3.3 读文件:字节读取

file(READ <filename> <variable>
     [OFFSET <offset>] [LIMIT <max-in>] [HEX])

<filename> 中读取内容并存储到 <variable> 中。可选的参数:

  • OFFSET <offset>:从指定偏移位置开始读取。
  • LIMIT <max-in>:最多读取 <max-in> 字节。
  • HEX:将数据转换为十六进制表示(对二进制数据有用)。
file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var)       # 读取整个文件
message(${out_var})

file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var OFFSET 0 LIMIT 10)  # 读取前 10 字节
message(${out_var})

file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var OFFSET 0 LIMIT 5 HEX)  # 以十六进制读取前 5 字节
message(${out_var})

打印结果:

Hello World! China
Hello Worl
48656C6C6F

第一条语句读取了 wtest.txt 的完整内容(Hello World! China)。第二条语句从偏移 0 开始读取 10 字节,得到 Hello Worl。第三条语句同样从偏移 0 开始读取 5 字节,但以十六进制格式输出,Hello 对应的十六进制为 48656C6C6F

3.4 以字符串形式读取文件

file(STRINGS <filename> <variable> [<options>...])

从文件中解析 ASCII 字符串列表并存储到 <variable> 中。该命令专用于读取字符串,会忽略二进制数据以及回车符(\r、CR)。

可选的 options 参数包括:

选项 说明
LENGTH_MAXIMUM <max-len> 读取字符串的最大长度
LENGTH_MINIMUM <min-len> 读取字符串的最小长度
LIMIT_COUNT <max-num> 读取的行数
LIMIT_INPUT <max-in> 读取的字节数
LIMIT_OUTPUT <max-out> 存储到变量的限制字节数
NEWLINE_CONSUME 把换行符也考虑进去
NO_HEX_CONVERSION 禁止 Intel Hex 和 Motorola S-record 文件自动转换为二进制
REGEX <regex> 只读取符合正则表达式的行
ENCODING <encoding-type> 指定编码格式(UTF-8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE)
# 从 input.txt 读取所有字符串
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var)
message("${out_var}")

# 限定字符串最大长度
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LENGTH_MAXIMUM 5)
message("${out_var}")

# 限定字符串最小长度
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LENGTH_MINIMUM 4)
message("${out_var}")

# 限定读取行数
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LIMIT_COUNT 3)
message("${out_var}")

注:上述代码的打印结果取决于 input.txt 文件的具体内容。简而言之,不加选项会读取所有行;LENGTH_MAXIMUM 限制每行最大字符数;LENGTH_MINIMUM 限制每行最小字符数;LIMIT_COUNT 限制读取的行数。其余选项(如 REGEXENCODING 等)可按需组合使用。

3.5 计算文件的 Hash 值

file(<MD5|SHA1|SHA224|SHA256|SHA384|SHA512> <filename> <variable>)

计算指定文件内容的加密散列值(hash)并存储到变量中。必须指定一种哈希算法。

file(SHA256 "${PROJECT_SOURCE_DIR}/input.txt" out_var)
message("${out_var}")

打印结果示例(实际输出取决于文件内容):

2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

3.6 文件重命名

file(RENAME <oldname> <newname>)

<oldname> 重命名为 <newname>。路径可使用绝对路径或相对路径,相对路径解释为相对于当前源码路径。

file(RENAME "${PROJECT_SOURCE_DIR}/input.txt" "${PROJECT_SOURCE_DIR}/output.txt")

3.7 删除文件

file(REMOVE [<files>...])
file(REMOVE_RECURSE [<files>...])
  • REMOVE:删除给定的文件,不能删除目录。
  • REMOVE_RECURSE:删除给定的文件或目录(包括非空目录)。
file(REMOVE "${PROJECT_SOURCE_DIR}/out1.txt")
file(REMOVE_RECURSE "${PROJECT_SOURCE_DIR}/out2.txt"
     "${PROJECT_SOURCE_DIR}/empty-dir"
     "${PROJECT_SOURCE_DIR}/Non_empty-dir")

file() 命令功能非常强大,除了上述基本功能外,还支持文件下载、文件锁等高级功能,有兴趣可以进一步了解。


4. 设置交叉编译

默认情况下,CMake 使用主机系统(运行 cmake 命令的操作系统)的编译器来编译工程,生成的可执行文件只能在主机系统上运行。如果希望编译得到的程序能在 ARM 开发板等目标平台上运行,则需要配置交叉编译。

4.1 交叉编译的基本配置

配置交叉编译的核心是设置以下几个 CMake 变量(配置代码必须放在 project() 命令之前,否则不会生效):

set(CMAKE_SYSTEM_NAME Linux)           # 设置目标系统名称
set(CMAKE_SYSTEM_PROCESSOR arm)         # 设置目标处理器架构

# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)

# 指定交叉编译器
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-g++)

# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

各变量说明:

  • CMAKE_SYSTEM_NAME:目标主机的操作系统名称,设置为 Linux 表示目标为 Linux 系统。
  • CMAKE_SYSTEM_PROCESSOR:目标架构名称(如 arm)。
  • CMAKE_SYSROOT:该值会传递给编译器的 --sysroot 选项,指定编译器的系统根目录。编译过程中需要链接的库和头文件会去该目录下寻找(如标准 C 库和头文件)。
  • CMAKE_C_COMPILER / CMAKE_CXX_COMPILER:指定交叉编译的 C/C++ 编译器路径。
  • CMAKE_C_FLAGS / CMAKE_CXX_FLAGS:为编译器添加编译选项。
  • CMAKE_FIND_ROOT_PATH_MODE_PROGRAM:控制 find_program() 是否搜索 CMAKE_SYSROOT 中的路径。设为 NEVER 则仅使用主机系统路径。
  • CMAKE_FIND_ROOT_PATH_MODE_LIBRARY:控制 find_library() 是否搜索 CMAKE_SYSROOT 中的路径。设为 ONLY 则仅搜索 CMAKE_SYSROOT 中的路径。
  • CMAKE_FIND_ROOT_PATH_MODE_INCLUDE:控制 find_file()find_path() 是否搜索 CMAKE_SYSROOT 中的路径。设为 ONLY 则仅搜索 CMAKE_SYSROOT 中的路径。

注意CMAKE_SYSROOTCMAKE_C_COMPILERCMAKE_CXX_COMPILER 等变量中涉及交叉编译工具的安装路径,需要根据实际安装路径进行设置。

4.2 CMake 版本问题

需要特别注意的是,低版本的 CMake 可能不支持上述交叉编译配置方式。例如 Ubuntu 系统自带的 CMake 3.5.1 版本可能会报错。建议下载使用高版本的 CMake(如 3.16.0)。

安装高版本 CMake 的简便方法:从 CMake 的 GitHub Release 页面(https://github.com/Kitware/CMake/releases ) 下载预编译的压缩包(如 cmake-3.16.0-Linux-x86_64.tar.gz),解压后即可直接使用其中的 cmake 可执行程序,无需自行编译。

4.3 使用工具链文件(推荐做法)

将交叉编译配置直接写入 CMakeLists.txt 虽然可行,但不够规范。更规范的做法是将所有交叉编译配置项单独写在一个配置文件中(工具链文件),然后通过 cmake 命令的 -DCMAKE_TOOLCHAIN_FILE 选项来指定该文件。

步骤:

  1. 在工程源码目录下创建工具链配置文件,如 arm-linux-setup.cmake,内容与上述配置代码相同。

  2. CMakeLists.txt 中移除这些交叉编译配置项,只保留工程基本配置:

cmake_minimum_required(VERSION 3.5)
project(HELLO)
add_executable(main main.c)
  1. 进入 build 目录,执行 cmake 时指定工具链文件:
cmake -DCMAKE_TOOLCHAIN_FILE=../arm-linux-setup.cmake ..

通过这种方式,CMakeLists.txt 更加清晰简洁,交叉编译配置独立管理,便于在不同平台间切换。

4.4 验证交叉编译结果

配置完成后,执行 make 编译,生成的二进制文件即为 ARM 架构的可执行程序(可通过 file 命令查看确认),可以将其拷贝到开发板上运行。

4.5 适配其他交叉编译器(以 arm-linux-gnueabihf-gcc 为例)

如果使用不同的交叉编译器,只需修改工具链文件中对应的变量即可。以常见的 arm-linux-gnueabihf-gcc 为例,对比原示例需要修改以下内容:

配置项 原示例(poky) 改用 arm-linux-gnueabihf
C 编译器 arm-poky-linux-gnueabi-gcc arm-linux-gnueabihf-gcc
C++ 编译器 arm-poky-linux-gnueabi-g++ arm-linux-gnueabihf-g++
sysroot 路径 /opt/fsl-imx-x11/.../cortexa7hf-neon-poky-linux-gnueabi /usr/arm-linux-gnueabihf/(或工具链实际安装路径)
浮点选项 -mfloat-abi=hard gnueabihf 中的 hf 已隐含 hard-float,但仍可显式指定

对应的工具链文件 arm-linux-gnueabihf-setup.cmake 示例如下:

##################################
# 配置 ARM 交叉编译 (arm-linux-gnueabihf)
##################################
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 方法一(推荐):交叉编译器已添加到 PATH 环境变量中
# 此时只需指定编译器名称,无需完整路径
set(CMAKE_C_COMPILER   arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# apt 安装的 sysroot 通常在此路径,用以下命令确认:
#   arm-linux-gnueabihf-gcc --print-sysroot
set(CMAKE_SYSROOT /usr/arm-linux-gnueabihf)

# ============================================================
# 方法二(自定义路径,如 /opt/toolchains):
# 使用方法二时,必须把上面方法一的 3 行 set() 全部注释掉,
# 否则方法二的设置会被方法一覆盖,不会生效。
# ============================================================
# set(TOOLCHAIN_DIR /opt/toolchains/gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf)
# set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/libc)
# set(CMAKE_C_COMPILER   ${TOOLCHAIN_DIR}/bin/arm-linux-gnueabihf-gcc)
# set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/bin/arm-linux-gnueabihf-g++)

# 为编译器添加编译选项(根据目标 ARM 架构调整)
set(CMAKE_C_FLAGS "-march=armv7-a -mfpu=neon -mfloat-abi=hard")
set(CMAKE_CXX_FLAGS "-march=armv7-a -mfpu=neon -mfloat-abi=hard")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
##################################
# end
##################################

关键变化说明:

  1. 编译器名称:直接将 arm-poky-linux-gnueabi-* 替换为 arm-linux-gnueabihf-*。如果编译器已加入 PATH 环境变量,只需写名称即可;否则需写完整路径。
  2. sysroot 路径arm-linux-gnueabihf 工具链的 sysroot 通常是 /usr/arm-linux-gnueabihf/(apt 安装时)或工具链目录下的 arm-linux-gnueabihf/libc(自定义安装时)。务必通过 arm-linux-gnueabihf-gcc --print-sysroot 命令确认实际路径。
  3. 编译选项-mfloat-abi=hard 虽已由 gnueabihf 隐含,但显式指定更安全。-march-mfpu 需根据具体目标芯片调整(例如 Cortex-A7 可使用 -mcpu=cortex-a7 替代 -march=armv7-a)。
  4. 其余变量CMAKE_SYSTEM_NAMEFIND_ROOT_PATH_MODE_* 等)保持不变。

通用规则:无论使用哪种交叉编译器,核心都是替换 CMAKE_C_COMPILERCMAKE_CXX_COMPILERCMAKE_SYSROOT 这三个变量,其余配置保持通用即可。


5. 变量的作用域

与 C 语言类似,CMake 中的变量也有作用域的概念。本节从三个方面来介绍:函数作用域、目录作用域和全局作用域。

5.1 函数作用域(Function Scope)

当在函数内通过 set() 将变量与当前函数作用域绑定时,该变量仅在函数作用域内有效。出了这个作用域,如果外部也有同名变量,则使用外部的同名变量。若函数内嵌套调用其他函数,内层函数会依次向外搜索变量。

5.1.1 函数内引用外部定义的变量

function(xyz)
    message(${ABC})  # 引用外部变量 ABC
endfunction()

set(ABC "Hello World")  # 定义变量 ABC

xyz()  # 调用函数

打印结果:Hello World

由此可见,函数内部可以引用函数外部定义的变量。

5.1.2 函数内定义的变量外部不可引用

function(xyz)
    set(ABC "Hello World")  # 函数内定义变量 ABC
endfunction()

xyz()

if(DEFINED ABC)
    message("true")
    message("${ABC}")
else()
    message("false")
endif()

打印结果:false

这说明函数内部定义的变量仅在函数内部可用,出了函数便无效,类似于 C 语言中的局部变量。

5.1.3 函数内定义与外部同名的变量

function(xyz)
    message("函数内部")
    message("${ABC}")
    set(ABC "Hello China!")  # 设置变量 ABC
    message("${ABC}")
endfunction()

set(ABC "Hello World!")  # 外部定义变量 ABC
xyz()
message("函数外部")
message("${ABC}")

打印结果:

函数内部
Hello World!
Hello China!
函数外部
Hello World!

从打印信息可知,函数内部调用 set() 并不是修改外部变量,而是新建了一个同名的局部变量。在调用 set() 之前,函数内部没有定义 ABC,所以会向外搜索到外部定义的 Hello World!;调用 set() 之后,函数内创建了自己的 ABC,此时引用的是局部变量 Hello China!。函数外部打印的 ABC 仍然是原来的 Hello World!,并未被修改。

5.1.4 使用 PARENT_SCOPE 修改外部变量

如果需要在函数内修改外部定义的变量,可以在 set() 命令末尾加上 PARENT_SCOPE 关键字:

function(xyz)
    set(ABC "Hello China!" PARENT_SCOPE)  # 加上 PARENT_SCOPE
endfunction()

set(ABC "Hello World!")
xyz()
message("${ABC}")

打印结果:Hello China!

PARENT_SCOPE 的含义:如果添加了 PARENT_SCOPE 选项,则变量将设置在当前作用域的上一层作用域(父作用域)中,而不是在当前作用域内。每个目录(包含 CMakeLists.txt 的目录)或函数都会创建一个新作用域。

示例 1:函数的上一层作用域是调用者所在的作用域

function(xyz)
    set(ABC "Hello China!" PARENT_SCOPE)
endfunction()

set(ABC "Hello World!")
xyz()
message("${ABC}")  # Hello China! — 修改了调用者作用域中的 ABC

函数 xyz 的上一层作用域就是调用 xyz() 时所在的作用域(当前目录作用域)。

示例 2:嵌套函数调用

function(func2)
    set(ABC "Hello People!" PARENT_SCOPE)
endfunction()

function(func1)
    set(ABC "Hello China!")
    func2()
endfunction()

set(ABC "Hello World!")
func1()
message("${ABC}")

打印结果:Hello People!

函数 func2 的上一层作用域是 func1 函数的作用域,所以 func2 中通过 PARENT_SCOPE 修改的是 func1 中的 ABC(将 Hello China! 改为 Hello People!)。但 func1 中的 ABC 是局部变量,不影响外部,所以最终外部打印的仍是 Hello People!(因为 func2 修改了 func1 的局部变量)。

示例 3:跨目录的 PARENT_SCOPE

假设有如下目录结构:

├── CMakeLists.txt
└── src
    └── CMakeLists.txt

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(TEST)

add_subdirectory(src)
message("${ABC}")

src/CMakeLists.txt

set(ABC "Hello World!" PARENT_SCOPE)

这种情况下,PARENT_SCOPE 的上一层作用域是顶层目录作用域,因此变量 ABC 会在顶层源码中被设置。

5.1.5 函数返回值的实现

前面提到过,return() 不能用于返回参数。函数的返回值实际上是通过 PARENT_SCOPE 来实现的。

cmake_minimum_required(VERSION 3.5)
project(TEST)

# 定义一个函数,实现两个数相加,并将结果通过 out 参数返回给调用者
function(xyz out var1 var2)
    math(EXPR temp "${var1} + ${var2}")
    set(${out} ${temp} PARENT_SCOPE)
endfunction()

xyz(out_var 5 10)
message("${out_var}")

打印结果:15

原理拆解:

  1. 调用 xyz(out_var 5 10) 时,形参 out 接收到的值是字符串 "out_var",形参 var15var210
  2. 函数内 set(${out} ${temp} PARENT_SCOPE) 中的 ${out} 展开为 "out_var",等价于执行 set(out_var 15 PARENT_SCOPE)
  3. 所以实际上是以传入的参数值out_var)作为变量名,在父作用域中创建了一个变量。调用结束后,外部便多了一个名为 out_var、值为 15 的变量。

如果调用时传入的是其他名字,例如 xyz(result 5 10),那么外部创建的变量名就是 result。这就是实现"参数返回"的机制——函数通过 ${out} 间接引用调用者想要的变量名。

5.2 目录作用域(Directory Scope)

子目录会将父目录的所有变量拷贝一份到当前 CMakeLists.txt 中,当前 CMakeLists.txt 中变量的作用域仅在当前目录有效。

目录作用域有两个特点:

  • 向下有效:上层作用域中定义的变量在下层作用域中有效。
  • 值拷贝:子目录从父目录中拷贝一份变量的副本,子目录修改变量不影响父目录中的原变量。

示例:目录结构如下

├── CMakeLists.txt
└── sub_dir
    └── CMakeLists.txt

父目录 CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(TEST)

set(parent_var "Hello parent")
message("parent-<parent_var>: ${parent_var}")
add_subdirectory(sub_dir)
message("parent-<parent_var>: ${parent_var}")

子目录 sub_dir/CMakeLists.txt

message("subdir-<parent_var>: ${parent_var}")
set(parent_var "Hello child")
message("变量修改之后")
message("subdir-<parent_var>: ${parent_var}")

打印结果:

parent-<parent_var>: Hello parent
subdir-<parent_var>: Hello parent
变量修改之后
subdir-<parent_var>: Hello child
parent-<parent_var>: Hello parent

分析:

  1. 父目录定义了 parent_var = "Hello parent"
  2. add_subdirectory(sub_dir) 加载子目录时,子目录会拷贝父目录的所有变量,因此子目录中第一次打印 parent_varHello parent
  3. 子目录中执行 set(parent_var "Hello child") 修改的是子目录自己的副本,不影响父目录中的原变量。
  4. 回到父目录后,parent_var 仍然是原来的 Hello parent

5.3 全局作用域(缓存变量 / Cache Variables)

缓存变量(Cache Variables)在整个 CMake 工程的编译生命周期内都有效,作用域是全局范围的,工程内其他任意目录都可以访问。

定义缓存变量有多种方式:

方式一:使用 set() 命令添加 CACHE 选项

set(VAR_NAME value CACHE STRING "Description")

方式二:使用 cmake -D 选项

cmake -DVAR_NAME=value ..

-D 选项会创建一个名为 VAR_NAME 的缓存变量(即全局变量),在整个工程中生效。


6. 属性(Properties)

CMake 中的属性会影响一些行为,大致可分为全局属性、目录属性、目标属性等。官方文档:cmake-properties

6.1 目录属性(Directory Properties)

目录属性即 CMakeLists.txt 源码的属性。常用的目录属性包括:

属性名 说明
CACHE_VARIABLES 当前目录中可用的缓存变量列表
CLEAN_NO_CUSTOM 若为 true,make clean 时不删除自定义命令的输出文件
INCLUDE_DIRECTORIES 头文件搜索路径列表(include_directories() 命令所添加的目录)
LINK_DIRECTORIES 库文件搜索路径列表(link_directories() 命令所添加的目录)
MACROS 当前目录中可用的宏命令列表
PARENT_DIRECTORY 当前子目录的父源码路径(顶级目录时为空字符串,只读)
VARIABLES 当前目录中定义的变量列表(只读)

6.1.1 获取和设置目录属性

获取目录属性使用 get_directory_property() 命令:

get_directory_property(<variable> [DIRECTORY <dir>] <prop-name>)
  • 将属性的值存储在 variable 变量中
  • DIRECTORY <dir> 为可选参数,指定目录(默认是当前源码目录)
  • prop-name 为属性名称

设置目录属性使用 set_directory_properties() 命令:

set_directory_properties(PROPERTIES prop1 value1 prop2 value2)

6.1.2 INCLUDE_DIRECTORIES 属性详解

INCLUDE_DIRECTORIES 是头文件搜索路径列表。include_directories() 命令会将目录添加到此属性中。

cmake_minimum_required(VERSION 3.5)
project(TEST)

# 获取 INCLUDE_DIRECTORIES 属性(此时应为空)
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")

# 添加头文件搜索目录
include_directories(include)

# 再次获取
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")

# 使用 BEFORE 将目录添加到列表前面
include_directories(BEFORE hello)

# 再次获取
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")

include_directories() 默认将目录添加到列表末尾,可使用 BEFOREAFTER 控制添加到前面或后面。

直接设置 INCLUDE_DIRECTORIES 属性替代 include_directories()

除了使用 include_directories() 命令,也可以直接设置 INCLUDE_DIRECTORIES 属性来添加头文件搜索路径:

cmake_minimum_required(VERSION 3.5)
project(TEST)

# 直接设置 INCLUDE_DIRECTORIES 属性(必须使用绝对路径)
set_directory_properties(PROPERTIES INCLUDE_DIRECTORIES /home/dt/vscode_ws/cmake_test/include)

get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")

add_executable(main main.c)

注意:使用 set_directory_properties() 设置属性时,路径需要使用绝对路径

父目录属性向下传递

父目录的 INCLUDE_DIRECTORIES 属性可以初始化、填充子目录的 INCLUDE_DIRECTORIES 属性:

# 父源码
cmake_minimum_required(VERSION 3.5)
project(TEST)

include_directories(include hello)
get_directory_property(p_list INCLUDE_DIRECTORIES)
message("${p_list}")

add_subdirectory(subdir)
# 子源码
get_directory_property(c_list INCLUDE_DIRECTORIES)
message("${c_list}")

子目录中的 INCLUDE_DIRECTORIES 会继承父目录的设置。

LINK_DIRECTORIES 是库文件搜索路径列表。link_directories() 命令会将目录添加到此属性中。

cmake_minimum_required(VERSION 3.5)
project(TEST)

# 获取 LINK_DIRECTORIES 属性(此时应为空)
get_directory_property(out_var LINK_DIRECTORIES)
message("${out_var}")

# 添加库文件搜索目录
link_directories(include hello)

get_directory_property(out_var LINK_DIRECTORIES)
message("${out_var}")

同样,父目录的 LINK_DIRECTORIES 属性也会传递给子目录,也可以直接设置该属性来替代 link_directories() 命令。

6.2 目标属性(Target Properties)

目标属性是与目标(可执行文件、库等)关联的属性。可通过 get_target_property()set_target_property() 命令获取或设置。

常用的目标属性包括:

属性名 说明
BINARY_DIR 定义目标的目录中 CMAKE_CURRENT_BINARY_DIR 的值(只读)
SOURCE_DIR 定义目标的目录中 CMAKE_CURRENT_SOURCE_DIR 的值(只读)
INCLUDE_DIRECTORIES 目标的头文件搜索路径列表,target_include_directories() 会向此列表添加目录,初始值拷贝自目录属性的 INCLUDE_DIRECTORIES
INTERFACE_INCLUDE_DIRECTORIES target_include_directories() 使用 PUBLICINTERFACE 关键字时填充此属性
INTERFACE_LINK_LIBRARIES target_link_libraries() 使用 PUBLICINTERFACE 关键字时填充此属性
LIBRARY_OUTPUT_DIRECTORY 库文件的输出目录
LIBRARY_OUTPUT_NAME 库文件的输出名称
LINK_LIBRARIES 目标的链接依赖库列表
OUTPUT_NAME 目标文件的输出名称
TYPE 目标类型,取值为 STATIC_LIBRARYMODULE_LIBRARYSHARED_LIBRARYINTERFACE_LIBRARYEXECUTABLE 之一或内部目标类型

posted @ 2026-07-24 14:41  Javenwww  阅读(14)  评论(0)    收藏  举报