七、系统自检

CMake 提供一系列以 Check 开头的系统自检模块(如 CheckIncludeFilesCheckCompilerFlagCheckSourceCompiles 等),通过编译小型测试程序来查询头文件是否存在、编译器是否支持某标志或某段代码能否编译,从而获取 CMake 无法直接确定的工具链与系统信息。

检查包含文件

CheckIncludeFiles 模块

检测一个或多个 C/C++ 头文件是否存在,并验证它们是否可以被同时包含。
# 加载
include(CheckIncludeFiles)
# 命令
check_include_files(<includes> <variable> [LANGUAGE <language>])
  • includes:分号分隔的头文件列表,如 "sys/socket.h;net/if.h"。
  • variable:保存检查结果的变量名(内部缓存变量,TRUE 或 FALSE)。
  • LANGUAGE(可选):指定编译器语言,CCXX;未提供时优先使用 C,若 C 未启用则用 C++。

示例

# 方法一:根据系统加载头文件
include(CheckIncludeFiles)
check_include_files(sys/socket.h HAVE_SYS_SOCKET_H)
if(HAVE_SYS_SOCKET_H)
    # 某些系统上 <net/if.h> 需依赖 <sys/socket.h>
    check_include_files("sys/socket.h;net/if.h" HAVE_NET_IF_H)
else()
    check_include_files(net/if.h HAVE_NET_IF_H)
endif()

# 方法二:检查 C++ 头文件
check_include_files("header1.hpp;header2.hpp" HAVE_CXX_HEADERS LANGUAGE CXX)


# 下面是 教学案例

# 第三步,在源码文件中,比如 xxx.cpp
#ifdef TUTORIAL_USE_SSE2
#  include <emmintrin.h>
#endif

控制编译测试环境的变量(在调用前设置)

  • CMAKE_REQUIRED_FLAGS – 附加编译标志(空格分隔字符串)。
  • CMAKE_REQUIRED_DEFINITIONS – 宏定义(分号分隔,如 -DFOO)。
  • CMAKE_REQUIRED_INCLUDES – 头文件搜索路径(分号分隔,会覆盖默认包含路径)。
  • CMAKE_REQUIRED_LINK_OPTIONS – 附加链接选项。
  • CMAKE_REQUIRED_LIBRARIES – 需链接的库(名称或导入目标)。
  • CMAKE_REQUIRED_LINK_DIRECTORIES – 库搜索路径。
  • CMAKE_REQUIRED_QUIET – 设为真值则抑制状态消息。
  • 常用CMAKE_REQUIRED_INCLUDESCMAKE_REQUIRED_DEFINITIONSCMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_QUIET

示例

include(CheckIncludeFiles)

cmake_push_check_state()
set(CMAKE_REQUIRED_INCLUDES /opt/custom/include)   # 如果头文件在非标路径
set(CMAKE_REQUIRED_DEFINITIONS -D_BSD_SOURCE)       # 某些系统需要此宏
set(CMAKE_REQUIRED_QUIET ON)
check_include_files("sys/socket.h;net/if.h" HAVE_NET_IF_H)
cmake_pop_check_state()

教学案例

MathFunctions/CMakeLists.tx

# 第一步,查询这个文件是否存在,将结果存入HAS_EMMINTRIN,并指定语言为 CPP
include(CheckIncludeFiles)
check_include_files(emmintrin.h HAS_EMMINTRIN LANGUAGE CXX)
# 第二步,判断 HAS_EMMINTRIN结果来定义一个宏
if(HAS_EMMINTRIN)
  target_compile_definitions(MathFunctions PRIVATE TUTORIAL_USE_SSE2)
endif()

MathFunctions/MathFunctions.cx

// 第三步:根据是否有这个文件的结果来判断是否要加载这个头文件。
#ifdef TUTORIAL_USE_SSE2
#  include <emmintrin.h>
#endif

检查源代码编译

CheckSourceCompiles 模块

检查一段完整的源代码能否在当前工具链下成功编译并链接成可执行文件。与仅检查头文件的 CheckIncludeFiles 不同,它适合探测没有对应头文件的特性(如编译器内置函数、语言特性支持)。

# 加载模块
include(CheckSourceCompiles)
# 语法
check_source_compiles(<LANG> <SOURCE_CODE> <RESULT_VAR>)
  • <LANG>:语言标识,C 或 CXX。
  • <SOURCE_CODE>:要测试的源代码字符串,必须包含一个有效的 int main() 函数。
  • <RESULT_VAR>:存储结果的变量名(内部缓存,值为 TRUE 或 FALSE)。

典型场景:检测内置函数
GCC/Clang 的 __builtin_add_overflow 函数是否存在

check_source_compiles(CXX
  "
    int main() {
      int a, b, c;
      __builtin_add_overflow(a, b, &c);
      return 0;
    }
  "
  HAS_CHECKED_ADDITION
)

# 结果的使用
if(HAS_CHECKED_ADDITION)
    target_compile_definitions(my_app PRIVATE HAS_BUILTIN_ADD_OVERFLOW)
    # HAS_BUILTIN_ADD_OVERFLOW 是自定义宏
endif()
  • 为什么需要 int main():该命令会尝试将提供的代码编译并链接为一个可执行文件,然后运行(但不检查运行结果)。没有 main 函数会导致链接失败,因此必须提供一个合法的入口点。

控制编译测试环境的变量(在调用前设置)

  • CMAKE_REQUIRED_FLAGS – 附加编译标志(空格分隔字符串)。
  • CMAKE_REQUIRED_DEFINITIONS – 宏定义(分号分隔,如 -DFOO)。
  • CMAKE_REQUIRED_INCLUDES – 头文件搜索路径(分号分隔,会覆盖默认包含路径)。
  • CMAKE_REQUIRED_LINK_OPTIONS – 附加链接选项。
  • CMAKE_REQUIRED_LIBRARIES – 需链接的库(名称或导入目标)。
  • CMAKE_REQUIRED_LINK_DIRECTORIES – 库搜索路径。
  • CMAKE_REQUIRED_QUIET – 设为真值则抑制状态消息。
  • 常用CMAKE_REQUIRED_INCLUDESCMAKE_REQUIRED_DEFINITIONSCMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_QUIET

检查过程间优化

过程间优化(IPO)是一种跨编译单元(源文件)进行优化的编译器技术,当优化发生在链接阶段时,通常称为链接时优化(LTO)。它允许编译器对整个程序进行分析,从而实现更激进的内联和死代码消除,提升运行时性能。

CMake 中的启用方式

  • 目标属性:INTERPROCEDURAL_OPTIMIZATION 设为 TRUE
  • 全局变量:设置 CMAKE_INTERPROCEDURAL_OPTIMIZATION 可为当前作用域内所有目标启用。
  • 在启用之前,应使用 CheckIPOSupported 模块检查编译器是否支持。
# 加载
include(CheckIPOSupported)

# 核心命令
check_ipo_supported(
  [RESULT <result-var>]
  [OUTPUT <output-var>]
  [LANGUAGES <lang>...]
)
  • RESULT:若支持 IPO,变量值为 YES,否则为 NO。若省略该选项且编译器不支持 IPO,命令将直接产生致命错误(终止配置)。
  • OUTPUT:存储详细错误信息,便于警告或调试。
  • LANGUAGES:指定要检查的语言:CCXXCUDAFortran(3.25+)。默认使用当前已启用的语言(ENABLED_LANGUAGES 全局属性)。

注意事项

  • CMake 并不掌握所有编译器的全部 IPO/LTO 标志。为单一目标设置 INTERPROCEDURAL_OPTIMIZATION 只会影响该目标的编译选项,不会自动传递给其链接的依赖项。IPO 仅能作用于同样以 IPO 模式编译的目标。
  • 对于包含多个项目的依赖树,更可靠的做法是通过外部机制(如 CMake 预设、-DCMAKE_CXX_FLAGS、工具链文件)统一注入优化标志,确保所有目标都采用一致的 LTO 编译。

示例 1:要求必须支持 IPO(不支持则中止)

include(CheckIPOSupported)
check_ipo_supported()  # 不支持则致命错误
set_property(TARGET MyApp PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)

示例 2:可选启用,并带有用户开关和优雅降级

option(ENABLE_IPO "Enable interprocedural optimization" ON)

if(ENABLE_IPO)
  include(CheckIPOSupported)
  check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error)
  if(ipo_supported)
    set_property(TARGET MyApp PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
  else()
    message(WARNING "IPO not supported: ${ipo_error}")
  endif()
endif()

八、自定义命令与生成文件

在 CMake 中,代码生成是一种常见的扩展手段,用来突破编程语言自身的表达限制。像 Qt 的元对象编译器(MOC),CMake 提供了原生支持,但绝大多数代码生成器都是为特定项目定制的,不值得在构建系统层面做那么深度的集成。为此,CMake 提供了一套通用机制,让项目可以按需描述任意的代码生成过程,其中核心工具就是 add_custom_command()。

这套机制的关键在于把代码生成视作一个普通的构建步骤,其行为和编译器、链接器并无本质区别。你需要明确地描述出这个步骤的输入文件和输出文件,CMake 会基于时间戳自动判断是否需要重新执行:只有当输出不存在,或者任何输入比输出更新时,对应的命令才会运行。换句话说,只要定义好依赖关系,代码生成器就能无缝融入整个增量构建流程。

这种模型有一个重要前提:输出的文件名和位置在配置阶段就必须完全确定。如果某个代码生成器的输出会随着输入内容动态变化,CMake 的原生接口就难以描述,虽然存在一些变通手段,但那已经超出了常规使用范畴。

在 CMake 中描述一个代码生成器通常分两步走。第一步是独立定义生成过程,只关心“输入什么、输出什么、运行什么命令”,这一步通过 add_custom_command() 完成,不涉及任何构建目标。第二步是将生成的文件纳入 CMake 的目标模型,让它们真正参与编译或后续构建。

具体来说,如果生成的是源文件(.cpp 等),直接把输出的文件路径加到某个 STATIC、SHARED 或 OBJECT 库的源文件列表中即可,CMake 会自动识别依赖并先触发生成。如果生成的是纯头文件(或只有头文件的库),由于 INTERFACE 库本身没有构建步骤,通常需要借助 add_custom_target() 创建一个中间自定义目标,把头文件的生成过程挂载上去,再通过依赖关系驱动它执行。

这样,无论是源码级还是头文件级的代码生成,都可以通过同一套“先声明生成规则,再关联到目标”的模式,灵活且可控地融入 CMake 构建体系。

add_custom_command()

add_custom_command() 用于向构建系统添加自定义构建规则。简单来说就是 让Cmake 在编译过程中额外执行一些命令。

经典用途是:

  • 生成源代码(代码生成)
  • 调用 Python、Lua、Protobuf 等工具
  • 拷贝资源文件
  • 自动生成头文件
  • 编译完成后执行脚本
  • 打包、签名、计算 Hash 等

生成文件(常用)

add_custom_command(
    OUTPUT out.cpp # 通过命令的输出文件
    COMMAND python generator.py # 自定义命令创建文件
    DEPENDS input.txt # 需要依赖的文件
)

add_library(MyLib out.cpp) # 使用 add_custom_command 创建的文件
  • 执行流程:input.txt -> generator.py -> out.cpp -> 编译
  • 只有当 OUTPUT 被其它 target 使用时,这条命令才会执行。
  • OUTPUT指定生成的文件。可以 指定多个文件。生成的文件会自动拥有 GENERATED 属性。
  • DEPENDS指定依赖文件。可以 指定多个文件。当以来发生变化时,重新执行 COMMAND

    • 依赖对象可以是文件源文件Target自定义输出.
  • COMMENT:构建时在终端输出自定义提示。COMMENT "Generating source..."
  • VERBATIM:正确处理所有参数转义。Cmake官方 推荐 几乎所有的自定义命令都应该加上 VERBATIM
  • USES_TERMINAL:允许命令直接占用终端。
  • APPEND:向已有规则追加命令。
  • COMMAND_EXPAND_LISTS :用的不多了。强制把命令参数中的 CMake 列表逐个展开成独立的命令行参数。
  • 还有一些属性,查阅官方文档吧。

构建事件

add_custom_command(
    TARGET MyApp
    POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
            config.json
            $<TARGET_FILE_DIR:MyApp>
    # 构建时将config.json复制到 Myapp 可执行文件的目录。
)
  • PRE_BUILD:构建前执行。(仅 Visual Studio 真正支持,一般不推荐使用)
  • PRE_LINK:链接前执行。(常用于:生成链接资源、更新版本号)
  • POST_BUILD:链接完成后执行。(最常见的)

    • POST_BUILD 的用途有:拷 DLL、拷资源、打包、签名、Hash、部署

注意事项

  • 多个 Target 不要共享 OUTPUT
  • 当依赖文件时间戳比生成目标的时间戳新,则会重新执行 COMMAND 命令。
  • OUTPUT 文件不存在时,也会重新执行 COMMAND 命令。
  • 多个 POST_BUILD 命令的执行顺序。
  • DEPENDS 可以直接依赖 CMake 目标。
  • OUTPUT 路径最好使用绝对路径(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/out.cpp)。

add_custom_target()

add_custom_target() 用于添加一个无输出、始终被视为过期的构建目标,适合执行不产生构建产物的辅助任务(如测试、打包、部署等)。
典型用途:
  • 运行测试、代码检查
  • 生成文档、代码覆盖率报告
  • 打包、签名、部署资源
  • 驱动代码生成(配合 add_custom_command() 触发)
  • 执行清理、初始化等维护操作

基本用法(始终运行的任务)

# 基本语法
add_custom_target(Name [ALL] [command1 [<args1>...]]
                  [COMMAND command2 [<args2>...]] ...
                  [DEPENDS <depend>...]
                  [BYPRODUCTS <file>...]
                  [WORKING_DIRECTORY <dir>]
                  [COMMENT <comment>]
                  [JOB_POOL <job_pool>]
                  [JOB_SERVER_AWARE <bool>]
                  [VERBATIM] [USES_TERMINAL]
                  [COMMAND_EXPAND_LISTS]
                  [SOURCES <source>...])
# 示例
add_custom_target(run_tests ALL
    COMMAND ctest -C $<CONFIG>
    COMMENT "Running tests..."
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
  • ALL:加入默认构建,每次 make 都会执行。
  • 没有 OUTPUT 文件,目标永远“过期”,命令总会被调用。

配合代码生成(有条件执行)

# 1. 定义生成规则(仅定义,绝不主动执行)
# 此命令仅声明:如果构建系统需要 generated.cpp,就执行 Python 脚本生成它。
# 但它不会被主动触发,因为构建工具不会主动扫描孤立的 OUTPUT。
add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated.cpp
    COMMAND python gen.py -o ${CMAKE_BINARY_DIR}/generated.cpp
    DEPENDS gen.py data.txt
)

# 2. 创建依赖目标,将生成文件拉入构建依赖链
# gen_sources 被标记为 ALL,因此执行 make/cmake --build 时会默认构建它。
# 构建工具发现该目标依赖于 generated.cpp,便会去检查该文件是否存在/是否过期。
# 只有此时(构建时),上方的 add_custom_command 才有机会被触发执行。
add_custom_target(gen_sources ALL
    DEPENDS ${CMAKE_BINARY_DIR}/generated.cpp
)
  • 将生成文件的规则放在 add_custom_command 中(有输出、有依赖,按需执行)。
  • 用 add_custom_target 依赖该输出,驱动生成;若输出已是最新,实际命令不会重复运行。

常用选项速查

  • ALL – 目标加入默认构建(不加则需显式 make target 才执行)。
  • COMMAND – 要执行的命令,可多条(顺序执行)。支持生成器表达式。
  • COMMENT – 执行前打印提示。
  • DEPENDS – 依赖的文件或 add_custom_command 输出(同目录下)。也可通过 add_dependencies() 依赖其他目标。
  • WORKING_DIRECTORY – 命令执行的工作目录,默认为 CMAKE_CURRENT_BINARY_DIR
  • BYPRODUCTS – 声明命令可能产生的文件(Ninja 需要此信息以便重建)。
  • SOURCES – 额外源文件,仅供 IDE 显示,无构建规则。
  • VERBATIM – 正确转义所有参数,推荐总是使用
  • USES_TERMINAL – 允许命令直接占用终端(Ninja 中放入 console 池)。
  • COMMENT – 显示在构建日志中的提示信息。
  • JOB_POOL – 为 Ninja 指定作业池。
  • JOB_SERVER_AWARE – 标记该命令支持 GNU Make 的 job server(配方前自动加 +)。

注意事项

  • 不要用 add_custom_target 去生成正式构建产物(没有输出追踪,会导致不必要的重复构建)。需要生成文件时请用 add_custom_command
  • DEPENDS 可直接写同目录下 add_custom_commandOUTPUT 文件名;若跨目录,可借助文件路径或目标依赖。
  • 多个 COMMAND 按顺序执行,但不保证在同一 shell 中(跨平台脚本请用 configure_file 生成完整脚本后再调用)。
  • 建议总是加上 VERBATIM,避免参数转义问题。

add_dependencies()

add_dependencies() 用于在顶层目标之间建立构建顺序依赖,确保依赖目标先构建完成,再构建自身。
用途:
  • 先运行生成工具,再编译用到这些文件的程序。
  • 强制一个库在另一个库之前编译。
  • 控制自定义目标之间的执行顺序。

基本语法

add_dependencies(<target> <target-dependency>...)
  • <target>:要添加依赖的目标(由 add_executableadd_library 或 add_custom_target 创建)
  • <target-dependency>:需先构建的顶层目标(可多个)

示例

# 自定义目标:生成文件
add_custom_target(gen_config
    COMMAND python generate_config.py
    BYPRODUCTS config.h
)

# 可执行文件依赖生成步骤
add_executable(MyApp main.cpp)
add_dependencies(MyApp gen_config)   # 确保 config.h 先生成
  • 构建 MyApp 时,必定先执行 gen_config
# 强制两个库按顺序构建
add_library(Util util.cpp)
add_library(Core core.cpp)
add_dependencies(Core Util)   # 先编译 Util,再编译 Core
  • 在注意 add_dependencies 中 目标构建顺序是从右向左, 保证所有依赖项(右侧参数)都先于 <target>(左侧参数)完成。

注意事项

  • 不要用 add_dependencies 替代 target_link_libraries 来传递编译/链接依赖;后者会传递头文件路径、链接库等,add_dependencies 只控制构建顺序。
  • 依赖循环会导致构建错误。
  • add_custom_target 配合时,若自定义目标没有 ALL 关键字,手动构建该目标才会触发依赖链。
  • Ninja 下依赖目标的编译可能尚未完成,仅保证生成命令执行完毕,因此不要假定依赖目标可执行文件已存在(除非它确实是自定义命令的输出文件)。
  • add_executableadd_libraryadd_custom_target 创建的目标 add_dependencies 才能使用。

八、测试与 CTest

历史上,测试并非构建系统的职责,但 CMake 的 CTest 生态正好相反:CTest 本质上是一个任务启动器,通过运行命令并检查返回零值或非零值来报告测试结果,CMake 则借助 enable_testing() 和 add_test() 命令在构建目录中铺设好基础设施,使 CTest 能够发现、执行并报告各类测试,最简单的调用方式就是在构建目录下直接运行 ctest,本文仅浅尝辄止地介绍它的部分功能。

CTest

CTest 是 CMake 生态里的测试运行器,它不负责编译代码,而是在构建完成之后,按照你的设定批量执行命令,并通过命令的退出码(零/非零)来判断测试通过与否。它最大的特点就是简单、可扩展,既可以管理几十个单元测试,也能驱动大规模的自动化测试流水线。

常用命令

  • ctest:全部运行测试。
  • ctest -R "正则表达式" :运行匹配的测试 。
  • ctest -E "正则表达式" :排除匹配的测试。
  • ctest -N:只列出即将运行的测试列表。
  • ctest -j N:同时启动最多 N 个测试进程,进行测试。
  • ctest --output-on-failure:只输出失败测试的详细信息。
  • ctest --rerun-failed:仅重新运行上次失败的测试。
  • ctest --repeat until-fail:10:最多重复 10 次,直到失败(解决偶发性失败)
  • ctest --test-dir path/to/build:指定测试目录,不在当前目录时使用。
  • ctest -C Debugctest -C Release:告诉 CTest 要运行哪个构建配置下的测试。

BUILD_TESTING

BUILD_TESTING 用于控制 CTest 模块是否调用 enable_testing()

CTest 模块通过 include(CTest) 加载时,会执行如下核心逻辑:

option(BUILD_TESTING "Build the testing tree." ON)
if(BUILD_TESTING)
    enable_testing()
    # 可能还有一些辅助宏定义(如覆盖率、CDash 等)
endif()

这会在项目中创建一个 BUILD_TESTING 选项(默认 ON),用于控制是否启用测试生成。当该选项为 ON 时,enable_testing() 被调用,随后 add_test() 定义的测试才能被 ctest(1) 识别和执行。

关键注意事项:

  • include(CTest) 必须在顶层源码目录的 CMakeLists.txt 中调用,因为 ctest(1) 期望在顶层构建目录中找到测试相关文件。
  • 在顶层 CMakeLists.txt 中调用 include(CTest)等价于手动编写上述 option + if 代码块。

实际开发建议:

  • 优先使用 include(CTest),而非手动编写。
  • 它是社区和工业界的标准实践,符合开发者预期。
  • 为未来集成 CDash、代码覆盖率等高级功能预留了兼容性。
  • 自动提供标准的 BUILD_TESTING 开关,便于 CI 等场景灵活控制。

enable_testing()

为当前目录及其子目录启用测试
enable_testing()
  • enable_testing() 命令应在顶层源码目录中调用,因为 ctest 期望在对应的顶层构建目录中找到测试文件。
  • 当包含 CTest 模块时,除非将 BUILD_TESTING 选项关闭,否则该命令也会被自动调用。
  • 关于 enable_testing() 的调用位置,存在以下限制:

    • 它必须在文件作用域中调用,不能放在 function()block() 内部。

示例:

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 包含 CTest 模块,自动调用 enable_testing(),并引入 BUILD_TESTING 选项
include(CTest)

# 如果 BUILDTESTING 为 ON(默认),则可以继续添加测试
if(BUILD_TESTING)
    add_subdirectory(tests)
endif()

add_test()

1. 核心作用

向项目中添加一个由 CTest 运行的测试。

2. 前提条件

必须先调用 enable_testing(),否则测试不会被生成。(include(CTest) 会自动调用它,除非 BUILD_TESTING 设为 OFF

3. 推荐语法(NAME 签名)

add_test(NAME <name> COMMAND <command> [<arg>...]
         [CONFIGURATIONS <config>...]
         [WORKING_DIRECTORY <dir>]
         [COMMAND_EXPAND_LISTS]
         [BUILD_DEPENDS <dependencies>...])
  • 优势:支持生成器表达式、目标自动替换、TEST_LAUNCHER 和 CROSSCOMPILING_EMULATOR 属性。
  • 测试属性:可通过 set_tests_properties() 在测试所在目录设置,且支持生成器表达式。

4. 关键参数说明

  • COMMAND

    • 若指定的是 add_executable() 目标,会自动替换为实际可执行文件路径。
    • 交叉编译时自动附加 CROSSCOMPILING_EMULATOR;若同时有 TEST_LAUNCHER,则组合为:<launcher> <emulator> <command>
    • 支持生成器表达式(如 $<TARGET_FILE:myexe>)。
  • CONFIGURATIONS
    将测试限制在特定构建配置(如 Debug、Release)下运行。
  • WORKING_DIRECTORY
    设置测试执行时的当前目录,未指定则默认为 CMAKE_CURRENT_BINARY_DIR。支持生成器表达式
  • COMMAND_EXPAND_LISTS(CMake ≥ 3.16)
    命令参数中的列表(包括生成器表达式产生的列表)会被展开为多个参数。
  • BUILD_DEPENDS(CMake ≥ 4.4)
    指定测试运行前必须构建的目标或文件。需配合 CMAKE_TEST_BUILD_DEPENDS(仅 Ninja 生成器)使用,会在 test_prep/<name> 目标中建立依赖,确保可执行文件最新。

5. 测试通过/失败判定

  • 退出代码 0 → 通过;非 0 → 失败。
  • WILL_FAIL 属性可反转上述逻辑(但系统级异常如段错误仍会导致失败)。
  • 标准输出/错误由 CTest 捕获,仅通过 PASS_REGULAR_EXPRESSIONFAIL_REGULAR_EXPRESSIONSKIP_REGULAR_EXPRESSION 属性影响结果。

6. 旧语法(不推荐)

add_test(<name> <command> [<arg>...])
  • 不支持生成器表达式、目标自动替换、TEST_LAUNCHER 和 CROSSCOMPILING_EMULATOR
  • 仅用于简单场景,新项目应使用 add_test(NAME ...) 形式。

7. 简单示例

enable_testing()
add_executable(myexe main.cpp)

add_test(NAME mytest
         COMMAND testDriver --config $<CONFIG>
                            --exe $<TARGET_FILE:myexe>
         WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
         CONFIGURATIONS Debug Release)

该测试名为 mytest,运行 testDriver 工具,自动传入构建配置和 myexe 的完整路径,仅在 Debug 和 Release 配置下执行。

教程示例

项目结构

.
├── CMakeLists.txt                # 顶层 CMake 配置
├── MathFunctions/                # 库源码(假设已存在)
└── Tests/
    ├── CMakeLists.txt            # 测试子目录配置
    └── TestMathFunctions.cxx     # 测试程序源码

./CMakeLists.txt (顶层)

cmake_minimum_required(VERSION 3.15)
project(Tutorial)

# TODO 6:添加 BUILD_TESTING 选项,默认开启测试构建
# 该选项允许用户通过 -DBUILD_TESTING=OFF 跳过测试
option(BUILD_TESTING "Enable testing and build tests" ON)

# ... 添加 MathFunctions 库等其他配置 ...

# TODO 7:条件化地启用测试并添加测试子目录
if(BUILD_TESTING)
  # 为构建树生成 CTest 所需的元数据,使 ctest 命令能够发现测试
  enable_testing()

  # 添加包含测试的 Tests 子目录
  add_subdirectory(Tests)
endif()

Tests/CMakeLists.txt(完整测试配置)

# TODO 1-2:创建测试可执行文件 TestMathFunctions
add_executable(TestMathFunctions)

# 添加测试程序的源文件
target_sources(TestMathFunctions
  PRIVATE
    TestMathFunctions.cxx
)

# TODO 3:将待测试的 MathFunctions 库链接到测试程序
target_link_libraries(TestMathFunctions
  PRIVATE
    MathFunctions
)

# TODO 4:定义一个 CMake 函数,简化重复的 add_test 调用
# 参数 op 为要测试的数学操作名称(add、mul、sqrt、sub)
function(MathFunctionTest op)
  # 向 CTest 注册一条测试:
  #   NAME <op>    -> 测试名称为操作名,例如 "sqrt"
  #   COMMAND ...  -> 运行 TestMathFunctions 并传入操作名
  add_test(
    NAME ${op}
    COMMAND TestMathFunctions ${op}
  )
endfunction()

# TODO 5:使用上述函数批量添加四个具体测试
MathFunctionTest(add)
MathFunctionTest(mul)
MathFunctionTest(sqrt)
MathFunctionTest(sub)

Tests/TestMathFunctions.cxx

// 该程序接收一个命令行参数,指定要测试的数学函数名称。
// 如果计算正确则返回 0(测试通过),否则返回非零值(测试失败)。
#include <iostream>
#include <string>
// ... 包含 MathFunctions 头文件 ...

int main(int argc, char* argv[]) {
  if (argc != 2) {
    std::cerr << "Usage: " << argv[0] << <操作名>\n";
    return 1;
  }
  std::string op = argv[1];
  if (op == "add") {
    // 验证加法函数
    return (add(2, 3) == 5) ? 0 : 1;
  } else if (op == "mul") {
    // 验证乘法函数
    return (mul(2, 3) == 6) ? 0 : 1;
  } else if (op == "sqrt") {
    // 验证平方根函数(允许微小误差)
    double result = sqrt(9.0);
    return (std::abs(result - 3.0) < 1e-6) ? 0 : 1;
  } else if (op == "sub") {
    // 验证减法函数
    return (sub(5, 3) == 2) ? 0 : 1;
  } else {
    std::cerr << "未知操作: " << op << "\n";
    return 1;
  }
}

运行与验证

# 第一步:配置项目
cmake --preset tutorial
# 或者
cmake -B build

#第二步:构建项目
cmake --build build

#第三步:运行全部测试
ctest --test-dir build

#第四步:仅运行名称包含 "sqrt" 的测试:
ctest --test-dir build -R sqrt

九、安装命令与概念

安装的本质,是把构建产物从零散的构建树移动到一套标准化、可发现的目标结构中,让其他项目能像使用系统库一样使用你的代码。CMake 为此提供了统一的 install() 命令,并围绕 目标(Target) 这一抽象来组织整个过程。

通常我们不需要手动复制文件,只需告诉 CMake“我要安装这个目标”,它就会根据工件类型(可执行文件→bin,库→lib,头文件→include 等)自动放到合适的位置。更关键的是,通过 install(TARGETS … EXPORT …)install(EXPORT …),CMake 可以生成一份“目标导出文件”,将库的路径、头文件位置、编译选项、依赖关系全部封装成 CMake 可识别的导入目标。其他项目只需 find_package() 就能像链接自己构建的目标一样直接使用这些已安装库,完全不需要关心文件实际落在哪里。

换句话说,安装不只是搬家,更是为目标的使用者重建 CMake 的抽象模型,让使用安装库和直接编译源码拥有几乎一致的体验。

==这里强烈告知 install() 命令语言内容居多,笔者看着CMake 官方文档都头大,无从下笔,最后还是按CMake教程来写了。关于 install 详细命令细节真的多和杂乱。我想的是写出来也是杂乱的为什么不直接去看官网呢==
==这里强烈告知 install() 命令语言内容居多,笔者看着CMake 官方文档都头大,无从下笔,最后还是按CMake教程来写了。关于 install 详细命令细节真的多和杂乱==
==这里强烈告知 install() 命令语言内容居多,笔者看着CMake 官方文档都头大,无从下笔,最后还是按CMake教程来写了。关于 install 详细命令细节真的多和杂乱==
重要的事情说三遍

install

指定在安装时运行的规则。

概要

install(TARGETS <target>... [...])                     # 安装构建目标(可执行文件、库等)及关联文件
install(IMPORTED_RUNTIME_ARTIFACTS <target>... [...])  # 安装导入目标的运行时文件(如 DLL、so)
install({FILES | PROGRAMS} <file>... [...])            # 安装普通文件 / 程序(脚本等)
install(DIRECTORY <dir>... [...])                      # 安装整个目录及其内容
install(SCRIPT <file> [...])                           # 安装时执行外部 CMake 脚本
install(CODE <code> [...])                             # 安装时执行内联 CMake 代码
install(EXPORT <export-name> [...])                    # 生成并安装目标导出文件,供 find_package 使用

# 以下属于较少使用(了解即可)
install(PACKAGE_INFO <package-name> [...])             # 安装通用包规范文件(CPS)
install(RUNTIME_DEPENDENCY_SET <set-name> [...])       # 安装之前收集的运行时依赖集(如 DLL 依赖)
install(SBOM <sbom-name> [...])                        # 安装软件物料清单(SBOM)

通用选项

通用选项是多个 install() 形式都能用的公共参数。你可以把它们理解成安装规则的附加属性——用来控制装到哪、什么权限、对哪些配置生效、归哪个组件管、以及一些特殊行为。

安装目录

DESTINATION <dir>:安装到那个目录。

  • 相对路径 会自动拼接到 CMAKE_INSTALL_PREFIX 后面。例如 DESTINATION bin,最终实际安装到 ++/usr/local/bin++ 或你指定的前缀下。
  • 绝对路径 会让打包失效,因为 CPack 依赖 --prefix 把文件安装到临时目录再打包。
install(TARGETS my_app DESTINATION bin)          # 最终路径:<prefix>/bin/my_app
install(FILES readme.txt DESTINATION share/doc)  # <prefix>/share/doc/readme.txt

文件权限

PERMISSIONS <permission>...

指定安装后文件的 Unix 权限(Windows 上忽略无效项)。
可多次出现,权限会累加。

指定已安装文件的权限。有效的权限包括 OWNER_READOWNER_WRITEOWNER_EXECUTEGROUP_READGROUP_WRITEGROUP_EXECUTEWORLD_READWORLD_WRITEWORLD_EXECUTESETUIDSETGID。在某些平台上无意义的权限会被忽略。

install(PROGRAMS my_script.sh DESTINATION bin
        PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
                    GROUP_READ GROUP_EXECUTE
                    WORLD_READ WORLD_EXECUTE
)
# 结果:文件拥有读写执行权限,组和其他用户有读和执行权限

只在特定构建类型时安装

CONFIGURATIONS <config>...
如果只希望 Debug 版本安装某些文件,或者不同配置安装到不同路径。
可以多次指定,配置列表会累加。

install(TARGETS my_app
        CONFIGURATIONS Debug
        DESTINATION bin/Debug
)
install(TARGETS my_app
        CONFIGURATIONS Release
        DESTINATION bin/Release
)
# Debug 时装到 bin/Debug,Release 时装到 bin/Release

分组打包

COMPONENT <component>

给安装规则贴个“组件标签”,之后可以通过 cmake --install . --component Runtime 单独安装某一组。
常用于区分 Runtime(运行时库)、Development(头文件和开发库)、Documentation 等。
install(TARGETS my_shared_lib
        LIBRARY DESTINATION lib
        COMPONENT Runtime
)
install(TARGETS my_shared_lib
        PUBLIC_HEADER DESTINATION include
        COMPONENT Development
)
# 用户可选择:cmake --install build --component Development 仅安装头文件

按需安装

EXCLUDE_FROM_ALL

加了这标记,执行完全安装(不带 --component)时该项会被跳过。只有你明确指定它所在的组件时才会安装。
install(FILES huge_data.dat DESTINATION share
        COMPONENT Data
        EXCLUDE_FROM_ALL
)
# cmake --install build        → 不会装这个文件
# cmake --install build --component Data → 才会安装

文件不存在也别报错

OPTIONAL

有时候要安装的文件可能不存在(比如可选生成的配置文件),加上这个就不会中断安装过程。
install(FILES maybe_generated.conf DESTINATION etc OPTIONAL)
# 文件不存在时:跳过安装,继续执行

综合示例

install(TARGETS my_app  
        CONFIGURATIONS Debug            # 只在 Debug 配置下安装
        RUNTIME DESTINATION bin/Debug   # 安装到 bin/Debug(相对于前缀)
        COMPONENT Runtime               # 属于 Runtime 组件
        PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ # 可执行文件权限设为 0755(所有者读写执行)
)

语法形式

TARGETS

用于安装 add_executable()、add_library() 创建的目标。它专门针对 编译生成的二进制产物(可执行文件,库文件),能自动识别操作系统的文件类型(如 Windows 的 .exe/.dll 与 Linux 的 .so),并分别处理。
install(TARGETS <target>... [...])
五大核心工件类型(<artifact-kind>)速查表
install(TARGETS ...) 可按工件类型分别控制安装位置。常用类型:
工件类型关键字包含内容默认安装目录变量变量默认值
ARCHIVE静态库 (.a/.lib)、DLL 导入库CMAKE_INSTALL_LIBDIRlib
LIBRARY共享库 (.so/.dylib),不含 Windows .dllCMAKE_INSTALL_LIBDIRlib
RUNTIME可执行文件、Windows .dllCMAKE_INSTALL_BINDIRbin
OBJECT对象库的目标文件(需显式指定 DESTINATION无(否则仅装头文件)
PUBLIC_HEADERPUBLIC_HEADER 属性指定的头文件CMAKE_INSTALL_INCLUDEDIRinclude
PRIVATE_HEADERPRIVATE_HEADER 属性指定的头文件CMAKE_INSTALL_INCLUDEDIRinclude
FILE_SET <name>目标关联的文件集(头文件集常见)CMAKE_INSTALL_INCLUDEDIRinclude

工件的示例

install(TARGETS MyTarget
  # 指定可执行文件和 Windows DLL 的安装位置
  RUNTIME DESTINATION bin
  # 指定共享库(非 Windows DLL)的安装位置
  LIBRARY DESTINATION lib
  # 指定静态库和导入库的安装位置
  ARCHIVE DESTINATION lib
  # 指定要安装的头文件集
  FILE_SET HEADERS
)
  • MyTarget 是可执行文件还是库,不是 install 命令决定的,而是在定义这个目标的时候就已经确定了。
  • 这里写三种类型,是一种跨平台兼容的写法,CMake 会根据目标的真实类型自动选用合适的部分。
  • Windows .dll 属于 RUNTIME.lib(导入库)属于 ARCHIVE
  • 若未指定某工件类型的目标位置(就是DESTINATION 后面的参数 )CMake 使用上述变量的默认值。
  • 引入include(GNUInstallDirs) 模块可以直接使用 ${CMAKE_INSTALL_BINDIR} 等变量。

设置安装前缀

安装前缀 决定了所有安装路径的根(如 bin、lib 都是相对此前缀)。

设置方法(按优先级)

  1. 调用时指定:cmake --install build --prefix /path/to/install
  2. CMake 预设:在 CMakePresets.json 中设置 installDir
  3. 配置时指定:cmake -B build -DCMAKE_INSTALL_PREFIX=/path
  4. 默认值:Unix 下通常为 /usr/local,Windows 为 C:/Program Files/${PROJECT_NAME}
  • 避免在 CMakeLists.txt 中硬编码 CMAKE_INSTALL_PREFIX,这会剥夺用户的控制权。

练习一:安装目标

install(
  TARGETS MyApp MyLib
  FILE_SET HEADERS          # 安装 HEADERS 文件集
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}/tools   # 自定义子目录
)
  • 所有目标中 EXCLUDE_FROM_ALL 的目标不会自动安装,需单独处理。
  • 可为每个目标单独添加 install(TARGETS) 命令,放在各自的 CMakeLists.txt 中,便于维护。

练习二:导出目标(Target Export)

仅拷贝文件会丢失 CMake 目标模型(传递依赖、使用要求等)。
目标导出 生成 <ExportName>.cmake 文件,重建导入目标。

基本步骤

  1. 第一个步骤:标记目标属于哪个导出集

    install(
      TARGETS MyApp MyLib      # 需要导出的 目标
      EXPORT MyProjectTargets  # 要的导出项目名集
      FILE_SET HEADERS         # 文件头
    )                          # 这个命令本身并不生成导出文件。
  2. 第二个步骤:生成并安装导出文件

    install(
      EXPORT MyProjectTargets
      DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
      NAMESPACE MyProject::
    )   #这一步才是真正的导出
  3. 将在 ${DESTINATION} 下生成 MyProjectTargets.cmake
  4. 所有导入目标都会加 MyProject:: 前缀(如 MyProject::MyLib

配置文件(Config.cmake)

通常不直接使用 <Export>.cmake,而是通过一个“包配置文件”间接包含它:

# cmake/MyProjectConfig.cmake
include(${CMAKE_CURRENT_LIST_DIR}/MyProjectTargets.cmake)

然后将该文件也安装到同一目录:

install(
  FILES cmake/MyProjectConfig.cmake
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
  • 这样 find_package(MyProject) 就能找到配置并加载目标。
  • CMAKE_CURRENT_LIST_DIR 指当前被处理的 CMake 脚本所在目录,保证路径正确。

练习三:版本文件

为防止版本不匹配导致副作用,CMake 提供轻量级版本检查文件。

使用 CMakePackageConfigHelpers 模块:

## project(Tutorial VERSION 1.0.0)

# 加载 CMake 提供的辅助模块,用于生成包版本文件
include(CMakePackageConfigHelpers)

# 在构建目录下生成一个版本兼容性文件
write_basic_package_version_file(
  # 第一个参数:要生成的版本文件路径(通常放在构建目录,后续一起安装)
  ${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake

  # 版本兼容策略:主版本号相同即可(例如 1.2.0 和 1.9.0 兼容)
  COMPATIBILITY SameMajorVersion

  # 标记该包不依赖于特定 CPU 架构(纯头文件库或脚本类包常用)
  ARCH_INDEPENDENT
)
  • 版本取自 project() 命令的 VERSION 参数。
  • 兼容性策略:

    • ExactVersion:完全匹配
    • SameMajorVersion:主版本号相同即可
    • SameMinorVersion:主、次版本号均相同
    • AnyNewerVersion:大于等于所需版本

然后将生成的版本文件一起安装:

# 安装包配置文件和版本文件
install(
  FILES
    # 开发者自己编写的包配置文件,用于加载导出目标等
    cmake/MyProjectConfig.cmake

    # 上一步生成的版本文件,提供版本检查
    ${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake

  # 安装到 find_package 的默认搜索路径之一
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)

常用细节补充

一、组件化安装(COMPONENT)

可用 COMPONENT 将安装内容分组,让用户选择性安装:

install(TARGETS MyLib
  COMPONENT runtime
  LIBRARY DESTINATION lib
)
install(TARGETS MyLib
  COMPONENT development
  FILE_SET HEADERS
)
install(FILES cmake/MyProjectConfig.cmake
  COMPONENT development
  DESTINATION lib/cmake/MyProject
)

安装时可指定组件:

cmake --install build --component runtime

二、多配置生成器注意事项

使用 Visual Studio 或 Xcode 等多配置生成器时,必须指定 --config

cmake --install build --config Release --prefix install

三、让库的包含目录自动适配安装位置

利用生成器表达式使 INTERFACE_INCLUDE_DIRECTORIES 在安装后指向正确路径:

target_include_directories(MyLib
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

安装后使用者只需 #include <mylib/header.h>,路径自动拼接前缀。

四、安装整个目录(示例:资源文件)

install(DIRECTORY resources/
  DESTINATION share/MyProject
  COMPONENT runtime
)

/ 结尾表示复制目录内容而非目录本身。

五、 调试已安装的文件列表

cmake --install build --prefix install --manifest install_manifest.txt
cat install_manifest.txt

也可以使用 --strip 去除符号。

工作流总结

一个完整的可发现包通常包含以下安装元素:

  • install(TARGETS ... EXPORT ...):安装库/可执行文件并标记导出
  • install(EXPORT ...):生成 *Targets.cmake
  • write_basic_package_version_file():生成 *ConfigVersion.cmake
  • 手动编写的 *Config.cmake 文件,内含 include() 加载目标文件
  • 通过 install(FILES ...) 安装上述配置文件
    经过这些步骤,其他项目就可以通过 find_package(MyProject REQUIRED) 可靠地发现并使用你的库。

十、查找依赖项

虽然 CMake 提供了很多高级工具来应对 C/C++ 的依赖管理难题,但只要依赖库本身打包规范、能生成标准的安装树,集成就会变得非常简单,最理想的情况下只需要一条 find_package() 命令即可。

背景

CMake 中有五个用于发现依赖项的主要命令,前四个是:

  • find_file():查找并报告指定文件的完整路径,这往往是 find 命令中最灵活的一个。
  • find_library():查找并报告静态归档文件或共享对象的完整路径,适合与 target_link_libraries() 一起使用。
  • find_path():查找并报告包含某个文件的目录的完整路径。这最常用于结合 target_include_directories() 来查找头文件。
  • find_program():查找并报告程序的可调用名称或路径。通常结合 execute_process()add_custom_command() 使用。
    这些命令应被视为“备用”命令,仅在主要查找命令不适用时使用。主要的查找命令是 find_package()。它利用全面的内置启发式方法和上游提供的打包文件,为所需的依赖项提供最佳接口。
    这些命令就不详细举例了,有需要的适合再到CMake 官网查询。

find_package()

查找外部项目(包),并加载其详细信息(如头文件路径、库文件、编译选项等)。

典型用法

最常用,推荐优先使用
find_package(<PackageName> [<version>] [REQUIRED] [QUIET] [COMPONENTS <components>...])
  • <PackageName>:必填,包名。
  • [<version>]:可选版本,如 1.2.3 或范围 1.2...<2.0
  • REQUIRED:若找不到包则停止配置并报错。
  • QUIET:可选依赖,找不到不输出信息。
  • COMPONENTS:指定需要的组件,找不到任一必需组件则整个包视为未找到。

最佳实践
将所有依赖项安装到单一安装树,并通过 CMAKE_PREFIX_PATH 告知 CMake:

# CMakePresets.json 或命令行
-DCMAKE_PREFIX_PATH=/path/to/install

CMAKE_PREFIX_PATH 是一个以分号分隔的目录列表,CMake 会将这些目录视为“安装前缀”来搜索依赖项。

搜索模式

在 CMake 中,find_package 的 搜索模式 指的是查找第三方库时所采取的三种不同策略:模块模式 (Module mode) 、FetchContent 重定向模式 和 配置模式 (Config mode)

模块模式 (Module Mode)

查找名为 Find<PackageName>.cmake 的脚本文件,通过启发式搜索(find_pathfind_library)定位头文件和库,并设置传统变量(如 <NAME>_INCLUDE_DIRS<NAME>_LIBRARIES)。
脚本可能由 CMake 官方提供,或由用户编写后放入 CMAKE_MODULE_PATH
# 基本格式(不写模式关键字,先尝试模块模式)
find_package(Foo REQUIRED)

# 显式指定模块模式
find_package(Foo MODULE REQUIRED)

优点:

  • 不要求库作者提供 CMake 支持,老旧 C/C++ 库也能用。
  • CMake 内置大量现成模块(如 FindOpenGLFindZLIB),开箱即用。
  • 简单库查找直接有效。

缺点:

  • 易过时:脚本与库自身分离,库升级后路径/文件名变化会导致查找失败。
  • 脆弱:依赖启发式搜索,库装在非标准位置时容易失败,需手动设置变量。
  • 使用不便:只返回传统变量,需手动添加头文件路径、编译选项和链接顺序。
  • 无依赖传递:不会自动处理目标库所依赖的其他库。

配置模式 (Config Mode)

查找包自带的 <PackageName>Config.cmake 或 <lowercase-name>-config.cmake 配置文件(或 .cps 文件),该文件由库作者提供,会创建导入目标(如 Foo::Foo),精确描述库的路径、编译选项、依赖关系及使用要求
# 完整格式(带 COMPONENTS,默认走配置模式)
find_package(Qt6 COMPONENTS Widgets REQUIRED)

# 强制配置模式
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets)
find_package(Qt6 NO_MODULE REQUIRED COMPONENTS Widgets)

优点:

  • 可靠:由库作者维护,随库升级同步更新,不会轻易失效。
  • 便捷:提供导入目标,只需 target_link_libraries 即可自动传递头文件、编译宏、链接选项。
  • 支持组件:可精确定制需要的子模块(COMPONENTS)。
  • 依赖透明:目标间有正确的依赖图,自动处理传递链接。

缺点:

  • 要求库提供配置文件:老旧或简单库可能没有安装 *Config.cmake,无法使用。
  • 查找路径有时需手动指定:装在自定义目录时需通过 CMAKE_PREFIX_PATH 或 <Package>_DIR 告知 CMake。

FetchContent 重定向模式

通过 FetchContent_Declare / FetchContent_MakeAvailable 在配置阶段下载依赖源码(或预编译包),将其直接引入当前项目的构建树。
随后当执行 find_package(<PackageName>) 时,CMake 会自动重定向到已下载的依赖,跳过外部搜索。
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

# 此后 find_package 会被重定向,直接使用已下载的 gtest
find_package(GTest REQUIRED)  # 实际使用的是 FetchContent 版本
target_link_libraries(my_test GTest::gtest)

优点:

  • 零环境配置:无需预先安装依赖,拉取代码即可构建,适合团队协作和 CI。
  • 版本精确锁定:可指定 Git 标签、commit,确保构建可重现。
  • 适合小型/header-only 库:下载、编译快速,管理成本低。

缺点:

  • 构建时间增加:首次需下载和编译依赖(可缓存),大型库(如 Qt)编译耗时长。
  • 可能重复下载:多项目各自使用相同依赖时无共享,增加磁盘和网络开销。
  • 不适用于巨型预编译库:直接集成编译负担大,通常仍用系统安装版本。
  • CMake 版本要求find_package 重定向需 CMake ≥ 3.24(早期版本可手动添加目标绕过)。

实际项目常混合使用:大型依赖用配置模式,测试框架等小工具用 FetchContent,少数特殊库用模块模式或自定义 Find 脚本。

基本语法格式

find_package(<PackageName>
             [version] [EXACT] [QUIET] [MODULE]
             [REQUIRED|OPTIONAL] [[COMPONENTS] <component>...]
             [OPTIONAL_COMPONENTS <component>...]
             [GLOBAL] ...)
  • EXACT:版本必须精确匹配(与范围不兼容)。
  • QUIET:静默查找,不打印信息性消息(即使包非 REQUIRED 也不提示找不到)。
  • REQUIRED / OPTIONAL:强制要求 / 可选。均可后接组件列表省略 COMPONENTS 关键字。
  • OPTIONAL_COMPONENTS:额外可选组件,找不到不影响包整体被发现。
  • GLOBAL:将导入的目标提升为全局可见(也可用变量 CMAKE_FIND_PACKAGE_TARGETS_GLOBAL)。
  • MODULE:强制仅使用模块模式,不回落配置模式。

完整签名(纯配置模式)

含 CONFIGNO_MODULE 或基本签名以外的选项时,强制进入配置模式。

find_package(<PackageName>
             [CONFIG|NO_MODULE]
             [NAMES <name>...]
             [CONFIGS <config>...]
             [HINTS <path>...]
             [PATHS <path>...]
             [PATH_SUFFIXES <suffix>...]
             [NO_DEFAULT_PATH] [NO_* ...]
             ...)
  • NAMES:提供多个包名搜索。
  • CONFIGS:指定备选的配置文件名(使用时会抑制 .cps 搜索)。
  • HINTS/PATHS:提供额外搜索路径,HINTS 适合系统自省得出的路径,PATHS 适合硬编码猜测。
  • PATH_SUFFIXES:在每个搜索条目后追加后缀。
  • NO_DEFAULT_PATH:禁用所有默认搜索路径(等价于启用所有 NO_* 选项)。

配置模式搜索路径顺序(简化)

核心作用:在多版本库共存的环境下,精确控制 CMake 到底选用哪一个库,避免误选。

查找逻辑:按照 “优先级从高到低” 的顺序,依次搜索以下路径,找到第一个匹配的即停止。

  1. 包专属根路径

    • <_PackageName>_ROOT (CMake变量/环境变量)
    • <_PACKAGENAME>_ROOT (大写变量)
    • 最优先,用于强制指定某个库的安装根目录。
  2. 用户级缓存变量

    • CMAKE_PREFIX_PATH
    • CMAKE_FRAMEWORK_PATH / CMAKE_APPBUNDLE_PATH
    • 常用:cmake -DCMAKE_PREFIX_PATH=/path/to/lib ..
  3. 环境变量

    • <PackageName>_DIR (环境变量)
    • CMAKE_PREFIX_PATH (环境变量)
    • 注意:这里的环境变量优先级低于同名 CMake 变量。
  4. HINTS 选项

    • 在 find_package 命令中通过 HINTS 直接指定的路径。
  5. 标准系统环境

    • 主要是 PATH 环境变量(会自动剔除 /bin 或 /sbin 后缀取父目录)。
  6. 用户包注册表

    • CMake 内部维护的记录,可通过 export(PACKAGE) 注册。
  7. 平台级 CMake 变量

    • CMAKE_SYSTEM_PREFIX_PATH
    • CMAKE_SYSTEM_FRAMEWORK_PATH
    • 典型值为 /usr/local 和其他标准系统目录。
  8. 系统包注册表

    • 系统级别的包注册记录。
  9. PATHS 选项

    • find_package 命令中通过 PATHS 直接指定的路径。
    • 注意:优先级最低,通常用作硬编码的兜底路径。

补充细节

  • 搜索子目录结构:在每个前缀下,CMake 会按约定搜索 <prefix>/lib/cmake/<name>/ 等结构。
  • 文件类型区分:若路径含 /cps/,只查找 .cps 文件;否则查找 .cmake 配置文件。
  • 交叉编译:可通过 CMAKE_FIND_ROOT_PATH 和 CMAKE_SYSROOT 重定向所有搜索根目录。
  • 版本排序:如果路径包含通配符 *,CMake 默认按版本降序选择较新的库。

CMake 配置模式版本选择机制

当使用 find_package(XXX 1.2.3) 指定版本时,CMake 不会随意接受任何已安装的包,而是通过版本校验文件自动判断找到的库是否满足需求。
如果版本不符合要求,CMake 会拒绝该包并继续搜索,或直接报错,避免因版本不匹配导致编译或运行时错误。

两种版本校验方式

第一种:CMake 脚本版本文件(主流)
  1. 文件位置与命名
  2. 与 <Package>Config.cmake 放在同一目录下。
  3. 常用名称:<Package>ConfigVersion.cmake 或 <config>-version.cmake
  4. 工作原理
  5. 该文件由 CMake 在找到主配置文件后自动调用。
  6. 它会读取调用者要求的版本信息(通过预定义变量传入),并与包自身的版本比较,最后必须设置以下三个结果变量之一:

    变量名含义
    PACKAGE_VERSION_EXACT包版本与请求版本完全相同
    PACKAGE_VERSION_COMPATIBLE包版本与请求版本兼容(通常指主版本号相同且包版本 ≥ 请求版本)
    PACKAGE_VERSION_UNSUITABLE此包因某些原因不适用(极少使用)
  7. 只要 EXACT 和 COMPATIBLE 都为假,查找即告失败。
  8. 如何生成
  9. 绝大多数库使用 CMake 内置函数自动生成,无需手写:

    # 在库的 CMakeLists.txt 中
    write_basic_package_version_file(
     "MyLibConfigVersion.cmake"
     VERSION 1.2.3
     COMPATIBILITY SameMajorVersion
    )
  10. COMPATIBILITY 选项指定兼容策略:

    • SameMajorVersion:主版本号相同即可(如 1.2.3 与 1.5.0 兼容)。
    • ExactVersion:必须完全一致。
    • AnyNewerVersion:包版本 ≥ 请求版本。
  11. 调用者可用的预定义变量
    版本文件可通过这些变量获取用户请求的版本信息:
  12. PACKAGE_FIND_VERSION(完整版本号,如 "1.2.3"
  13. PACKAGE_FIND_VERSION_MAJORMINORPATCH 等分量
  14. PACKAGE_FIND_VERSION_RANGE(如果使用了版本范围)
  15. 查找成功后
  16. CMake 会设置变量 <PackageName>_VERSION 及相应分量,供项目使用。
  17. 限制
  18. write_basic_package_version_file 生成的版本检查较严格:版本范围的上限是绝对截止,且要求包版本同时满足兼容性条件,不允许通过范围来放宽兼容策略。
第二种:通用包规范(CPS)文件
至少到我写这篇笔记的时候,2025年 .cps 实际情况用的不多。笔者就不详细记录了。

.cps 文件 是一个JSON 格式的文本文件,用来描述一个 C/C++ 库的元信息(例如:库名、版本、头文件路径、需要链接的库文件、编译选项、依赖的其他包),因为用 JSON 书写,任何构建系统(CMake、Meson、Bazel 等)都能解析,不再被 CMake 自己的脚本语言绑定。

总结一下 版本选择 就是 CMake 在找到配置文件后做的一次“资格审核”:根据你要求的版本号,验证该包是否足够新且接口兼容,通过则采用,不通过则丢弃。

练习(CMake官方的精简版本)

练习1示例:集成 SimpleTest 测试框架

  1. 查找包并设为必需

    # Tests/CMakeLists.txt
    find_package(SimpleTest REQUIRED)
  2. 链接导入的目标

    target_link_libraries(TestMathFunctions
      PRIVATE
     MathFunctions
     SimpleTest::SimpleTest
    )
  3. 使用包提供的函数自动发现测试
    simpletest_discover_testsSimpleTest 框架提供的 CMake 函数,它能自动从可执行文件(如 TestMathFunctions.cpp)中扫描所有用 TEST("name") 宏定义的测试用例,并为每个用例自动调用 add_test() 注册到 CTest,省去手动逐个添加测试的步骤,实现一次注册、一键运行全部测试。

    simpletest_discover_tests(TestMathFunctions)
  4. 设置 CMAKE_PREFIX_PATH 指向安装树
    在本教程中的具体效果:Tutorial 项目配置时执行 find_package(SimpleTest REQUIRED),CMake 会在 Step10/install 下自动搜索并找到:SimpleTestConfig.cmakeTransitiveDep等依赖
// CMakePresets.json 中的 cacheVariables
"CMAKE_PREFIX_PATH": "${sourceParentDir}/install"
  1. 测试文件中使用框架宏

    // TestMathFunctions.cxx
    #include <MathFunctions.h>
    #include <SimpleTest.h>
    
    TEST("add") {
      // 测试代码
    }

练习2示例:SimpleTest 依赖 TransitiveDep

当库 A 依赖库 B,而你的项目依赖 A 时,需要让 A 的包配置文件也把 B 找出来。

关键模块与命令

  • include(CMakeFindDependencyMacro):提供安全的递归查找机制。
  • find_dependency(<dep>):和 find_package 类似,并会转发上层调用的 REQUIRED/QUIET 等参数。
  1. SimpleTest 自己的 CMakeLists.txt 中查找并链接传递依赖

    find_package(TransitiveDep REQUIRED)
    
    target_link_libraries(SimpleTest
      INTERFACE
     TransitiveDep::TransitiveDep
    )
    因为是接口依赖,使用 INTERFACE,让链接 SimpleTest 的目标也自动获得 TransitiveDep。
  2. 在 SimpleTest 的包配置文件(SimpleTestConfig.cmake)中传播依赖

    include(CMakeFindDependencyMacro)
    find_dependency(TransitiveDep)

    这样,当 TutorialProject 执行 find_package(SimpleTest) 时,会递归查找 TransitiveDep,无需用户显式调用。

练习3示例:查找未打包的头文件 Unpackaged.h

对于没有提供 CMake 包配置的头文件或库,使用 find_path()find_library() 等。

  1. 查找包含目录

    find_path(UnpackagedIncludeFolder Unpackaged.h REQUIRED
      PATH_SUFFIXES Unpackaged
    )
  2. 会在已知前缀及子目录 Unpackaged 中查找 Unpackaged.h
  3. 结果保存在变量 UnpackagedIncludeFolder 中。
  4. 将找到的目录添加到目标

    target_include_directories(Tutorial
      PRIVATE
     ${UnpackagedIncludeFolder}
    )
  5. 在源文件中包含

    #include <Unpackaged.h>

总结

  1. 首选 find_package(),只在包未提供配置文件时才用 find_path/library/file/program
  2. 维护一个统一的安装前缀,通过 CMAKE_PREFIX_PATH 暴露给 CMake,简化查找。
  3. 传递依赖必须在库的 *Config.cmake 文件中使用 find_dependency() 显式声明。
  4. 参数 REQUIRED/QUIET 用于控制找不到时的行为,find_dependency 会自动传递这些参数。
  5. 在手动查找头文件时,PATH_SUFFIXES 可提高查找成功率。
  6. 推荐使用现代包管理器(如 vcpkg、Conan)管理依赖,减少手动查找工作。