现代 CMake 目标系统
"现代 CMake 目标系统"这个概念,可以从两个紧密相关但又截然不同的层面来理解。
首先也是最核心的,它指的是 交叉编译中"目标平台" 的概念,这也是现代 CMake 支持跨平台和嵌入式开发的关键。其次,它还指 CMake 构建系统中的核心抽象------"目标",这是现代 CMake 组织构建的基石。
交叉编译的"目标系统"
当你在一种架构上(如 x86 Linux)构建,但希望代码在另一种架构上(如 ARM 嵌入式设备)运行时,这就叫交叉编译。这里的"目标系统"就是程序实际要运行的平台。
这是现代 CMake 的杀手锏功能,它通过工具链文件(Toolchain File)来告知 CMake 目标平台的详细信息。
核心配置变量
在工具链文件中,你需要通过以下变量明确指定目标系统,实现交叉编译:
CMAKE_SYSTEM_NAME:(必选) 目标系统的操作系统名称。常见值为Linux、Windows、Android或iOS。如果目标是没有操作系统的嵌入式设备,则设置为Generic。CMAKE_SYSTEM_PROCESSOR:(可选) 目标系统的处理器架构,例如arm、aarch64、x86_64等。CMAKE_SYSTEM_VERSION:(可选) 目标操作系统的版本号。CMAKE_C_COMPILER/CMAKE_CXX_COMPILER:(必选) 指定针对目标平台的 C/C++ 交叉编译器路径。
查找依赖的行为控制
交叉编译时,find_* 系列命令(如 find_library)需要区分该搜索主机(Host)还是目标机(Target)的目录。以下变量用于控制这些行为:
CMAKE_FIND_ROOT_PATH:设置目标环境根目录的路径列表,find_*命令会优先搜索这些路径。CMAKE_FIND_ROOT_PATH_MODE_PROGRAM:控制find_program的行为。通常设置为NEVER,表示只在主机系统搜索可执行程序。CMAKE_FIND_ROOT_PATH_MODE_LIBRARY:控制find_library的行为。通常设置为ONLY,表示只在目标环境搜索库。CMAKE_FIND_ROOT_PATH_MODE_INCLUDE:控制find_path和find_file的行为。通常设置为ONLY,表示只在目标环境搜索头文件。
一个简单的工具链文件示例
这是一个为 Windows 平台使用 MinGW 交叉编译的示例:
cmake
# 设置目标系统名称
set(CMAKE_SYSTEM_NAME Windows)
# 指定交叉编译器
set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
# 设置目标环境的根路径
set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc /home/user/mingw-install)
# 调整查找行为
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 时通过 -DCMAKE_TOOLCHAIN_FILE=path/to/your/toolchain.cmake 参数指定即可。
构建系统的"逻辑目标"
在现代 CMake 中,"目标(Target)"是构建系统的基本单元。它不仅指最终生成的产物,也包含构建过程中用到的中间对象。CMake 通过定义目标之间的关系来决定构建顺序和规则。
常见的目标类型
| 目标类型 | 说明 | 创建示例 |
|---|---|---|
| 可执行文件 | 最终的程序文件 | add_executable(my_app main.cpp) |
| 静态库 | 链接时使用的 .a 或 .lib 文件 | add_library(my_lib STATIC lib.cpp) |
| 共享库 | 运行时动态加载的 .so、.dylib 或 .dll 文件 | add_library(my_shared SHARED shared.cpp) |
| 对象库 | 只编译不链接,将 .o 文件打包成一个集合,供其他目标复用 | add_library(my_objs OBJECT obj.cpp) |
| 接口库 | 纯头文件库,只传递使用要求,不生成构建产物 | add_library(my_headers INTERFACE) |
核心概念:使用要求与传递性
这是现代 CMake 的精髓。目标可以通过 target_* 命令设置其使用要求,并通过 target_link_libraries 将依赖关系及使用要求传递下去。
PRIVATE:依赖仅用于目标自身,不会传递给链接它的目标。INTERFACE:依赖不用于目标自身,只要求链接它的目标使用。PUBLIC:依赖既用于目标自身,也要求链接它的目标使用。
示例:
cmake
# 创建库目标
add_library(utils STATIC utils.cpp)
target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 创建可执行文件目标
add_executable(my_program main.cpp)
# 链接 utils 库,my_program 会自动获得 utils 的 PUBLIC 包含目录
target_link_libraries(my_program PRIVATE utils)
在这个例子中,my_program 链接了 utils,CMake 会自动处理 utils 的 PUBLIC 包含目录,使其对 my_program 的编译生效,这大大简化了依赖管理。
一个特殊的属性:SYSTEM
Make 中还定义了一个名为 SYSTEM 的目标属性,它用来将某个目标标记为"系统目标"。当编译一个链接了系统目标的目标时,系统目标的所有 INTERFACE_INCLUDE_DIRECTORIES 都会自动被编译器视为系统包含目录(通常对应编译器的 -isystem 标志)。
这样做的好处是:编译器会抑制这些系统头文件产生的警告信息,从而让你的构建输出更干净,聚焦于自己代码中的问题。
总结
- 如果关心的是"在哪里运行",那么"目标系统"指的就是交叉编译中的目标平台(Target Platform),需要通过工具链文件来明确指定操作系统和架构。
- 如果关心的是"如何构建",那么"目标系统"指的就是基于 逻辑目标(Target) 的现代 CMake 构建范式。通过
target_*命令和PUBLIC/PRIVATE/INTERFACE关键字,可以清晰地管理依赖和传递编译选项。
现代 CMake 的声明式构建系统
现代 CMake 目标系统中的这五个核心概念
- 目标的三种可见性(PRIVATE/PUBLIC/INTERFACE)详解
- 传递依赖管理
- 导入目标(IMPORTED targets)
- 别名目标(ALIAS targets)
- 接口库(INTERFACE libraries
目标的三种可见性(PRIVATE/PUBLIC/INTERFACE)详解
这是现代 CMake 最核心的设计,用于控制依赖关系的传播方向。
概念对比
| 可见性 | 用于当前目标? | 传递给依赖者? | 典型使用场景 |
|---|---|---|---|
| PRIVATE | ✅ 是 | ❌ 否 实 | 现细节,如内部使用的库、内部编译选项 |
| INTERFACE | ❌ 否 | ✅ 是 | 纯头文件库的要求、跨目标的编译接口 |
| PUBLIC | ✅ 是 | ✅ 是 公 | 开 API 需要的东西,如公共头文件目录 |
实际示例
cmake
# 假设有一个数学库
add_library(math STATIC math.cpp)
target_include_directories(math
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/internal # 仅 math 内部需要
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # 公开 API 头文件
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/common # 只要求使用者添加的
)
# 使用 PRIVATE 链接 OpenMP(内部优化,不暴露给外部)
target_link_libraries(math
PRIVATE OpenMP::OpenMP_CXX
PUBLIC ${CMAKE_DL_LIBS}
)
# 应用程序使用这个库
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE math) # my_app 自动获得 math 的 PUBLIC/INTERFACE 属性
关键理解:target_link_libraries(my_app PRIVATE math) 中的 PRIVATE 是指 math 对 my_app 是私有的(即 my_app 不会把自己的依赖传递给它的链接者),这和 math 内部的 PUBLIC/PRIVATE 是不同层次的概念。
传递依赖管理
传递依赖是现代 CMake 自动化处理依赖链的核心能力。
传播规则
cmake
# 依赖链:app → libB → libA
add_library(libA STATIC a.cpp)
target_compile_definitions(libA PUBLIC FEATURE_A=1) # 传播给 libB 和 app
target_compile_definitions(libA PRIVATE INTERNAL_A=1) # 仅 libA
add_library(libB STATIC b.cpp)
target_link_libraries(libB PUBLIC libA) # libA 的要求传递给 libB 的使用者
target_compile_definitions(libB PRIVATE FEATURE_B=1)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE libB) # app 自动获得 libA 的 PUBLIC 要求
传递路径追踪
cmake
# 查看完整传递依赖树
cmake --graphviz=deps.dot .
dot -Tpng deps.dot -o deps.png
# 或使用命令行
cmake --show-progress . && cmake --graphviz=graph.dot .
避免传递污染
cmake
# 如果 libB 只为实现使用 libA,不暴露其 API
target_link_libraries(libB PRIVATE libA) # libA 不会传递给 app
导入目标(IMPORTED targets)
导入目标用于表示已经预先构建好的外部依赖,不在此项目中构建。
基本用法
cmake
# 创建导入目标
add_library(fmt::fmt UNKNOWN IMPORTED)
# 设置位置属性
set_target_properties(fmt::fmt PROPERTIES
IMPORTED_LOCATION "${FMT_LIBRARY_PATH}"
INTERFACE_INCLUDE_DIRECTORIES "${FMT_INCLUDE_DIR}"
)
# 或者使用更现代的方式
add_library(zlib::zlib SHARED IMPORTED)
set_target_properties(zlib::zlib PROPERTIES
IMPORTED_LOCATION "${ZLIB_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}"
)
不同类型的导入目标
cmake
# 1. 静态库
add_library(my_static STATIC IMPORTED)
set_property(TARGET my_static PROPERTY IMPORTED_LOCATION "/path/to/libstatic.a")
# 2. 共享库
add_library(my_shared SHARED IMPORTED)
set_property(TARGET my_shared PROPERTY IMPORTED_LOCATION "/path/to/libshared.so")
# 3. 特定配置的库(Debug/Release)
add_library(my_configd_lib UNKNOWN IMPORTED)
set_target_properties(my_configd_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "/path/to/debug/lib"
IMPORTED_LOCATION_RELEASE "/path/to/release/lib"
MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release # 映射配置
)
# 4. 模块库
add_library(my_module MODULE IMPORTED)
set_property(TARGET my_module PROPERTY IMPORTED_LOCATION "/path/to/module.so")
使用 Find 模块的现代方式
cmake
# 传统方式(不推荐)
find_package(OpenSSL REQUIRED)
target_include_directories(my_app PRIVATE ${OPENSSL_INCLUDE_DIR})
target_link_libraries(my_app PRIVATE ${OPENSSL_LIBRARIES})
# 现代方式(推荐)- FindPNG.cmake 提供导入目标
find_package(PNG REQUIRED)
target_link_libraries(my_app PRIVATE PNG::PNG) # 使用导入目标
别名目标(ALIAS targets)
别名目标为现有目标创建一个替代名称,主要用于简化使用和避免命名冲突。
基本用法
cmake
# 为库创建别名
add_library(my_project_core STATIC core.cpp)
add_library(my_project::core ALIAS my_project_core) # 创建命名空间别名
# 使用别名
target_link_libraries(my_app PRIVATE my_project::core) # 更清晰的命名空间
实际应用场景
cmake
# 场景1:版本管理
add_library(math_v2 STATIC math_v2.cpp)
add_library(math::v2 ALIAS math_v2)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE math::v2) # 清晰表明版本
# 场景2:接口库别名
add_library(my_headers INTERFACE)
target_include_directories(my_headers INTERFACE include/)
add_library(my_project::headers ALIAS my_headers)
# 场景3:条件选择
if(USE_OPENMP)
add_library(parallel_backend ALIAS OpenMP::OpenMP_CXX)
else()
add_library(parallel_backend ALIAS Threads::Threads)
endif()
target_link_libraries(my_app PRIVATE parallel_backend)
限制条件
cmake
# ❌ 不能为导入目标创建别名(除非是 INTERFACE/UNKNOWN 类型)
add_library(external STATIC IMPORTED)
add_library(external::external ALIAS external) # 错误!
# ✅ 可以为非 GLOBAL 导入目标创建别名
add_library(external INTERFACE IMPORTED GLOBAL)
add_library(external::external ALIAS external) # 可以
# ❌ 别名不能用于修改属性
add_library(core STATIC core.cpp)
add_library(my::core ALIAS core)
set_target_properties(my::core PROPERTIES CXX_STANDARD 17) # 错误!应直接修改 core
# ❌ 不能给别名创建别名
add_library(my::core_v2 ALIAS my::core) # 错误!
接口库(INTERFACE libraries)
接口库是只包含使用要求(头文件、编译定义、链接库)但不生成构建产物的特殊目标。
基本概念
cmake
# 创建纯头文件库
add_library(sdl_headers INTERFACE)
target_include_directories(sdl_headers INTERFACE /usr/include/SDL2)
target_compile_definitions(sdl_headers INTERFACE SDL_BUILD=1)
# 使用接口库
add_executable(game main.cpp)
target_link_libraries(game PRIVATE sdl_headers) # 自动获得包含路径和宏定义
高级应用模式
模式1:特性集合(Feature Collections
cmake
# 组合多个编译特性
add_library(cxx17_features INTERFACE)
target_compile_features(cxx17_features INTERFACE cxx_std_17)
target_compile_definitions(cxx17_features INTERFACE USE_CXX17=1)
add_library(warning_flags INTERFACE)
target_compile_options(warning_flags INTERFACE
-Wall -Wextra -Wpedantic -Werror
)
# 组合使用
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE
cxx17_features
warning_flags
pthread # 普通库也可以混合
)
模式2:配置预设(Configuration Presets)
cmake
# Debug 配置
add_library(debug_config INTERFACE)
target_compile_definitions(debug_config INTERFACE DEBUG_MODE=1 _GLIBCXX_DEBUG)
target_compile_options(debug_config INTERFACE -g -O0 -fsanitize=address)
# Release 配置
add_library(release_config INTERFACE)
target_compile_definitions(release_config INTERFACE NDEBUG=1)
target_compile_options(release_config INTERFACE -O3 -DNDEBUG -march=native)
# 条件使用
add_executable(my_app main.cpp)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_link_libraries(my_app PRIVATE debug_config)
else()
target_link_libraries(my_app PRIVATE release_config)
endif()
模式3:平台抽象层
cmake
# 平台检测接口库
add_library(platform_detection INTERFACE)
if(WIN32)
target_compile_definitions(platform_detection INTERFACE _WIN32_WINNT=0x0601)
target_link_libraries(platform_detection INTERFACE ws2_32)
elseif(APPLE)
target_compile_definitions(platform_detection INTERFACE __APPLE__)
target_link_libraries(platform_detection INTERFACE "-framework CoreFoundation")
elseif(LINUX)
target_compile_definitions(platform_detection INTERFACE _GNU_SOURCE)
target_link_libraries(platform_detection INTERFACE dl pthread)
endif()
# 业务代码无需关心平台细节
add_executable(cross_platform_app main.cpp)
target_link_libraries(cross_platform_app PRIVATE platform_detection)
模式4:包装第三方库
cmake
# 找不到导入目标时,手动创建接口库包装
find_path(EIGEN3_INCLUDE_DIR NAMES Eigen/Core)
if(EIGEN3_INCLUDE_DIR)
add_library(Eigen3::Eigen INTERFACE IMPORTED)
set_target_properties(Eigen3::Eigen PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${EIGEN3_INCLUDE_DIR}"
)
endif()
# 统一使用接口库
target_link_libraries(my_math_app PRIVATE Eigen3::Eigen)
实用技巧
cmake
# 检查目标是否存在
if(TARGET my_interface_lib)
target_link_libraries(app PRIVATE my_interface_lib)
endif()
# 获取接口库的属性
get_target_property(INCLUDES my_interface_lib INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "Interface includes: ${INCLUDES}")
# 接口库也可以有源文件(但会被忽略)
add_library(header_only INTERFACE header.hpp) # header.hpp 不会被编译
综合示例:完整的现代 CMake 项目结构
cmake
cmake_minimum_required(VERSION 3.20)
project(ModernProject VERSION 1.0.0 LANGUAGES CXX)
# 1. 接口库:全局编译选项
add_library(project_options INTERFACE)
target_compile_features(project_options INTERFACE cxx_std_17)
target_compile_options(project_options INTERFACE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Werror>
)
# 2. 接口库:平台抽象
add_library(platform_abstraction INTERFACE)
if(WIN32)
target_compile_definitions(platform_abstraction INTERFACE _CRT_SECURE_NO_WARNINGS)
target_link_libraries(platform_abstraction INTERFACE ws2_32)
elseif(UNIX)
target_link_libraries(platform_abstraction INTERFACE pthread)
endif()
# 3. 静态库(核心逻辑)
add_library(core STATIC core/algorithm.cpp core/utils.cpp)
target_include_directories(core PUBLIC core/include)
target_link_libraries(core PUBLIC project_options platform_abstraction)
# 4. 接口库:测试配置
add_library(test_config INTERFACE)
target_link_libraries(test_config INTERFACE core)
target_compile_definitions(test_config INTERFACE UNIT_TEST=1)
# 5. 别名目标(方便使用)
add_library(project::core ALIAS core)
add_library(project::test_config ALIAS test_config)
# 6. 可执行文件
add_executable(main_app src/main.cpp)
target_link_libraries(main_app PRIVATE project::core)
# 7. 测试可执行文件
add_executable(unit_tests tests/test_main.cpp tests/core_test.cpp)
target_link_libraries(unit_tests PRIVATE project::test_config)
# 8. 导入第三方库(假设已找到)
find_package(fmt CONFIG REQUIRED)
target_link_libraries(main_app PRIVATE fmt::fmt)