现在位置: 首页 > CMake 教程 > 正文

CMake 高级特性

掌握 CMake 基础之后,高级特性可以帮助你更灵活地管理复杂项目的构建配置。

本文涵盖自定义模块、多配置构建、高级包查找、自定义构建步骤、跨平台交叉编译以及目标属性配置六大主题。

整体内容概览:

主题 核心能力 典型应用场景
自定义模块和脚本 封装可复用的 CMake 逻辑 统一编译器选项、自定义查找逻辑、团队共享构建规则
构建配置和目标 管理多配置和多目标 Debug/Release 切换、构建多种输出产物
高级查找和配置 灵活定位外部依赖 查找特定组件、通过配置文件传递构建信息
自定义构建步骤 扩展构建流程 代码生成、资源处理、构建后自动化
跨平台和交叉编译 面向多平台构建 嵌入式开发、为不同操作系统编译
目标属性和配置 精细控制编译链接 针对特定目标设置警告级别、链接路径

自定义 CMake 模块和脚本

当多个 CMakeLists.txt 中需要重复同样的逻辑时,将这些逻辑封装成自定义模块可以大幅减少代码冗余。

自定义模块和脚本的本质是将常用的 CMake 函数、宏和配置提取到独立文件中,供多个项目复用。

自定义 CMake 模块

自定义模块是一个以 .cmake 为扩展名的文件,其中定义了可被 include 加载的 CMake 函数和宏。

创建步骤:

步骤 操作 说明
1 创建 cmake/ 目录 在项目根目录下创建,用于集中存放自定义模块
2 创建模块文件 例如 cmake/MyModule.cmake,编写自定义函数
3 扩展模块搜索路径 在 CMakeLists.txt 中通过 CMAKE_MODULE_PATH 注册该目录
4 加载模块 使用 include() 引入模块并调用其中的函数

自定义模块示例——MyModule.cmake:

实例

# 文件路径:cmake/MyModule.cmake
# 自定义模块:提供项目通用的辅助函数

# 函数:为所有目标统一设置编译器警告选项
# 参数 ARG_TARGET:需要设置警告的目标名称
function(runoob_set_warnings ARG_TARGET)
    # 根据编译器类型设置不同的警告标志
    if(MSVC)
        # MSVC 编译器:启用 /W4 警告级别
        target_compile_options(${ARG_TARGET} PRIVATE /W4)
    else()
        # GCC / Clang 编译器:启用常用警告
        target_compile_options(${ARG_TARGET} PRIVATE -Wall -Wextra -Wpedantic)
    endif()
endfunction()

# 函数:打印目标的基本构建信息
function(runoob_print_target_info ARG_TARGET)
    message(STATUS "目标名称:${ARG_TARGET}")
    message(STATUS "源文件列表:$<TARGET_PROPERTY:${ARG_TARGET},SOURCES>")
endfunction()

在 CMakeLists.txt 中加载并使用模块:

实例

# 文件路径:CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)

# 将 cmake/ 目录添加到模块搜索路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

# 加载自定义模块
include(MyModule)

# 添加可执行文件目标
add_executable(MyApp main.cpp)

# 调用模块中的自定义函数,为 MyApp 设置警告选项
runoob_set_warnings(MyApp)

# 调用模块中的自定义函数,打印目标信息
runoob_print_target_info(MyApp)

CMAKE_MODULE_PATH 是 CMake 查找模块文件时的搜索路径列表。CMake 会先在内置模块路径中查找,找不到时再到 CMAKE_MODULE_PATH 指定的路径中查找。

使用自定义 CMake 脚本

自定义脚本与模块类似,但通常用于执行配置操作而非定义可复用的函数。

脚本可以直接用 include() 加载,CMake 会按顺序执行其中的每一条指令。

创建脚本文件 config.cmake:

实例

# 文件路径:config.cmake
# 自定义脚本:集中管理项目的配置选项

# 设置默认构建类型为 Release
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "构建类型" FORCE)
    message(STATUS "未指定构建类型,默认使用 Release")
endif()

# 根据构建类型设置不同的编译选项
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    message(STATUS "启用 Debug 模式:关闭优化,开启调试符号")
else()
    message(STATUS "启用 Release 模式:开启 O2 优化")
endif()

在 CMakeLists.txt 中调用脚本:

# 使用绝对路径加载脚本
include(${CMAKE_SOURCE_DIR}/config.cmake)

构建配置和目标

CMake 支持在同一个项目中管理多种构建配置(如 Debug 和 Release),并定义多个构建目标。

多配置生成器

不同的 CMake 生成器对配置的处理方式不同。

像 Visual Studio 和 Xcode 这样的多配置生成器允许在同一个构建目录中切换 Debug/Release,而 Unix Makefiles 等单配置生成器需要在配置阶段就确定构建类型。

生成器类型 典型生成器 配置指定方式
单配置 Unix Makefiles、Ninja 配置时通过 -DCMAKE_BUILD_TYPE=Release 指定
多配置 Visual Studio、Xcode 构建时通过 --config Release 切换

在 CMakeLists.txt 中设置默认配置:

# 仅对单配置生成器生效,多配置生成器会忽略此变量
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type")

CMAKE_BUILD_TYPE 只对单配置生成器有效。如果使用 Visual Studio,请通过 cmake --build . --config Release 或在 IDE 中切换配置。

构建目标

一个 CMake 项目中可以定义多个构建目标,每个目标独立配置编译选项和链接依赖。

这让你可以在一次构建中产出多个可执行文件或多个库。

实例

# 定义两个可执行文件目标,各自有不同的源文件
add_executable(MyApp src/main.cpp)
add_executable(MyTool src/tool.cpp)

# 为不同目标设置不同的编译宏,控制条件编译
set_target_properties(MyApp PROPERTIES COMPILE_DEFINITIONS "APP_MODE")
set_target_properties(MyTool PROPERTIES COMPILE_DEFINITIONS "TOOL_MODE")

# 为不同目标链接不同的库
target_link_libraries(MyApp PRIVATE MyLib)
target_link_libraries(MyTool PRIVATE MyLib CLI11::CLI11)

高级查找和配置

除了基本的 find_package 用法外,CMake 还支持指定组件、精确控制版本和通过配置文件模板传递构建信息。

find_package 高级用法

COMPONENTS 关键字允许你只查找库中的特定子模块,而非整个包。

这对于像 Boost 这样的大型库非常重要——按需引入可以避免链接不需要的组件。

实例

# 只查找 Boost 的 filesystem 和 system 两个组件(而非整个 Boost)
# filesystem 内部依赖 system,所以两个都要声明
find_package(Boost REQUIRED COMPONENTS filesystem system)

# 检查是否找到了所需组件
if(Boost_FOUND)
    message(STATUS "Boost 版本:${Boost_VERSION}")
    message(STATUS "Boost 头文件路径:${Boost_INCLUDE_DIRS}")

    # 分别链接每个需要的组件
    target_link_libraries(MyApp PRIVATE
        Boost::filesystem
        Boost::system
    )
endif()

指定搜索路径(适用于非标准安装位置):

# 方式一:通过变量指定根目录
cmake .. -DBOOST_ROOT=/path/to/custom/boost

# 方式二:在 CMakeLists.txt 中预先设置
set(BOOST_ROOT "/path/to/custom/boost")
find_package(Boost REQUIRED COMPONENTS filesystem)

配置文件与构建选项

configure_file() 指令可以将 CMake 变量的值注入到代码文件中,实现在编译时传递配置信息。

这在需要将版本号、构建时间等信息编译进程序时非常实用。

创建配置模板文件 config.h.in:

实例

// 文件路径:config.h.in
// CMake 配置模板文件,通过 configure_file() 生成 config.h

// 项目版本号(由 CMake 变量 @PROJECT_VERSION@ 替换)
#define RUNOOB_VERSION "@PROJECT_VERSION@"

// 构建类型(Debug 或 Release)
#define RUNOOB_BUILD_TYPE "@CMAKE_BUILD_TYPE@"

// 编译时间戳
#define RUNOOB_BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"

在 CMakeLists.txt 中配置并生成头文件:

实例

# 获取当前时间戳并存入变量
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")

# 将 config.h.in 中的 @变量@ 替换为 CMake 变量的值
# 输出到 build 目录下的 config.h
configure_file(config.h.in config.h)

# 将 build 目录添加到头文件搜索路径,使源文件能找到 config.h
include_directories(${CMAKE_BINARY_DIR})

在源文件中使用生成的配置:

// 在 main.cpp 中包含生成的配置文件
#include "config.h"

std::cout << "RUNOOB Version: " << RUNOOB_VERSION << std::endl;
std::cout << "Build Type: " << RUNOOB_BUILD_TYPE << std::endl;

生成自定义构建步骤

除了标准的编译和链接,CMake 允许你在构建过程中插入自定义操作,例如代码生成、资源复制和构建后通知。

自定义命令

add_custom_command() 用于定义一个自定义命令,它会在某个文件需要更新时执行。

这个指令的核心概念是依赖驱动——只有当 OUTPUT 文件不存在或 DEPENDS 文件更新时,命令才会执行。

实例

# 自定义命令:从模板文件生成代码
# 仅当 generated_file.cpp 不存在或 input_template.txt 更新时才执行
add_custom_command(
    # 输出文件(CMake 通过检查此文件决定是否重新执行命令)
    OUTPUT ${CMAKE_BINARY_DIR}/generated_file.cpp

    # 要执行的命令(此处用 cmake -E 执行内置的文件生成操作)
    COMMAND ${CMAKE_COMMAND}
        -E echo "// 自动生成的源文件" > ${CMAKE_BINARY_DIR}/generated_file.cpp

    # 依赖文件(此文件变更时命令重新执行)
    DEPENDS ${CMAKE_SOURCE_DIR}/input_template.txt

    # 执行时输出的提示信息
    COMMENT "正在从模板生成源文件..."
)

构建阶段触发命令——在链接完成后执行:

实例

# 在 MyApp 构建完成后,复制可执行文件到部署目录
add_custom_command(
    TARGET MyApp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:MyApp>
        ${CMAKE_SOURCE_DIR}/deploy/
    COMMENT "将 MyApp 复制到部署目录"
)

自定义目标

add_custom_target() 定义一个不产生输出文件的构建目标,通常用于将多个自定义命令组织在一起。

除非使用 ALL 关键字,否则自定义目标不会在默认构建中执行,需要显式指定目标名。

实例

# 自定义目标:关联上面的自定义命令
# ALL 表示在默认构建(make)时自动执行
add_custom_target(generate_code ALL
    # 依赖前面定义的 OUTPUT 文件
    DEPENDS ${CMAKE_BINARY_DIR}/generated_file.cpp
)

# 自定义目标:执行辅助任务(不会在默认构建时执行)
add_custom_target(deploy
    COMMAND ${CMAKE_COMMAND} -E echo "部署到服务器..."
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_BINARY_DIR}/bin
        /opt/runoob/deploy/
    COMMENT "执行部署操作"
)

# 在构建时单独触发此目标
# $ cmake --build . --target deploy

add_custom_command 和 add_custom_target 的区别:前者定义一个「如何生成文件」的规则,由依赖关系触发;后者定义一个「做什么事」的动作目标,需要显式调用或在 ALL 时自动执行。


跨平台和交叉编译

CMake 的核心优势之一就是跨平台支持——同一份 CMakeLists.txt 可以在不同操作系统和架构上生成对应的构建文件。

跨平台构建

CMake 会自动检测当前平台的编译器和系统环境,生成匹配的构建文件。

你也可以通过变量显式指定目标平台信息。

变量 作用 示例值
CMAKE_SYSTEM_NAME 目标操作系统 Linux、Windows、Darwin、Android
CMAKE_SYSTEM_PROCESSOR 目标处理器架构 x86_64、arm、aarch64
CMAKE_C_COMPILER C 编译器路径 /usr/bin/arm-linux-gnueabihf-gcc
CMAKE_CXX_COMPILER C++ 编译器路径 /usr/bin/arm-linux-gnueabihf-g++

交叉编译

交叉编译是指在当前平台(如 x86_64 Linux)上编译出运行在另一种架构(如 ARM)上的程序。

CMake 通过工具链文件(Toolchain File)来指定交叉编译所需的编译器、系统信息和链接器配置。

创建工具链文件 toolchain.cmake:

实例

# 文件路径:toolchain.cmake
# ARM Linux 交叉编译工具链配置文件

# 目标系统为 Linux
set(CMAKE_SYSTEM_NAME Linux)

# 目标处理器架构为 ARM
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定交叉编译器(必填:路径需根据实际工具链位置调整)
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)

# 指定目标系统的根文件系统路径(可选,用于查找库和头文件)
set(CMAKE_FIND_ROOT_PATH /path/to/arm-sysroot)

# 查找程序时只在目标系统路径中搜索(不在主机路径搜索)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# 查找库时只在目标系统路径中搜索
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)

# 查找头文件时只在目标系统路径中搜索
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

使用工具链文件进行交叉编译:

# 配置时通过 -DCMAKE_TOOLCHAIN_FILE 指定工具链文件
mkdir build_arm && cd build_arm
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake ..

# 使用生成的 Makefile 编译(将调用 ARM 交叉编译器)
make

工具链文件必须在第一次运行 cmake 时指定。如果已有 CMakeCache.txt,需要删除 build 目录后重新配置才能切换工具链。


目标属性和配置

CMake 允许以目标为单位精细控制编译选项、链接选项和各类属性,这是现代 CMake 推荐的做法。

目标属性

set_target_properties() 用于设置目标级别的属性,这些属性影响编译器和链接器的行为。

属性 作用 示例
COMPILE_OPTIONS 设置编译选项 -Wall、-O2
COMPILE_DEFINITIONS 设置预处理器宏 DEBUG、VERSION=2
LINK_FLAGS 设置链接器标志 -L/path/to/lib
OUTPUT_NAME 修改输出文件名 目标名是 MyApp,输出为 my_app

实例

# 创建可执行文件目标
add_executable(MyApp main.cpp)

# 为目标设置编译选项和链接标志
set_target_properties(MyApp PROPERTIES
    COMPILE_OPTIONS "-Wall;-Wextra"
    COMPILE_DEFINITIONS "RUNOOB_DEBUG"
    LINK_FLAGS "-L/usr/local/lib"
    OUTPUT_NAME "my_app"
)

# 上面的 set_target_properties 等同于以下三条指令的效果:
# target_compile_options(MyApp PRIVATE -Wall -Wextra)
# target_compile_definitions(MyApp PRIVATE RUNOOB_DEBUG)
# target_link_options(MyApp PRIVATE -L/usr/local/lib)

在现代 CMake 中,推荐使用 target_compile_options()、target_compile_definitions() 和 target_link_options() 替代 set_target_properties(),因为它们支持 PUBLIC/PRIVATE/INTERFACE 可见性控制,依赖关系更明确。

自定义编译和链接选项

使用 target 系列指令可以更精确地控制选项的传播范围。

实例

# 为 MyApp 设置编译选项
# PRIVATE:只有 MyApp 自身编译时使用这些选项
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic)

# 为 MyApp 设置预处理器定义
# 等同于在源文件中写 #define RUNOOB_VERSION "1.0"
target_compile_definitions(MyApp PRIVATE RUNOOB_VERSION="1.0")

# 为 MyApp 设置链接选项
target_link_options(MyApp PRIVATE -L/usr/local/lib)

# 如果是库,使用 PUBLIC 可以让链接此库的目标也继承这些选项
# 例如:使用 MyLib 的任何目标都会自动启用 C++17
target_compile_features(MyLib PUBLIC cxx_std_17)

注意事项

CMAKE_BUILD_TYPE 仅对单配置生成器有效。如果项目需要同时支持 Makefile 和 Visual Studio,不要依赖 CMAKE_BUILD_TYPE 的值。使用生成器表达式 $<$<CONFIG:Debug>:DEBUG_VALUE> 来根据实际配置动态切换。

优先使用 target_xxx 系列指令。set_target_properties 虽然功能强大,但缺少可见性控制。在需要 PUBLIC/PRIVATE/INTERFACE 语义的场景下,使用 target_compile_options 等专门的指令是更好的选择。

工具链文件必须在首次 cmake 时指定。交叉编译时,如果在已有 CMakeCache.txt 的目录中重新指定工具链,可能会产生不完整的配置。始终在干净的 build 目录中配置交叉编译。

自定义模块中的函数名建议加上前缀。避免与 CMake 内置命令或其他模块产生命名冲突。例如使用项目名作为前缀:myproject_set_warnings()。