导语
在上一节中,我们学习了如何通过 install() 命令将构建产物(可执行文件、库、头文件等)安装到系统目录。然而,仅仅把文件复制到指定位置,并不能让其他 CMake 项目方便地使用你的库。其他开发者仍然需要手动指定头文件路径、库文件路径以及传递依赖,这无疑回到了“手工管理依赖”的原始时代。
Modern CMake 的解决方案是导出目标(Exporting Targets)与配置包(Config Package)。通过这套机制,你的项目可以生成一组标准的 .cmake 文件,让其他项目只需要一句 find_package(MathLib REQUIRED),就能自动获得所有头文件路径、库文件链接以及传递依赖。
本节我们将手把手完成从导出目标到创建完整配置包的全过程,涵盖 install(EXPORT ...)、configure_package_config_file、write_basic_package_version_file 以及构建树导出等核心内容。
1. 从安装到导出:为什么需要导出目标
回顾一下 6.1 节的内容,我们通常这样安装一个库:
install(TARGETS mathlib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
PUBLIC_HEADER DESTINATION include
)
这行命令告诉 CMake:“把编译好的库文件放到 lib/ 目录,头文件放到 include/ 目录。” 但它没有保留任何关于这个目标的元信息。其他项目无法知道:
mathlib的头文件在哪里?- 链接
mathlib时,是否需要同时链接它的依赖(比如线程库、第三方库)? mathlib使用了哪些编译宏定义?
在 Modern CMake 中,这些信息都附着在目标(Target)的属性上。如果我们能把目标本身“序列化”到一个 .cmake 文件中,其他项目就可以通过 find_package 导入这个目标,完整继承其所有使用要求(Usage Requirements)。这就是 install(EXPORT ...) 的使命。
2. install(EXPORT …):生成目标导出文件
2.1 基本工作流程
导出目标需要两步配合:
- 在
install(TARGETS)时,通过EXPORT参数将目标加入到一个导出组(Export Set)中; - 使用
install(EXPORT)命令将该导出组生成一个*.cmake文件并安装到指定位置。
2.2 代码示例
假设我们有一个名为 mathlib 的静态库,我们希望导出它:
add_library(mathlib STATIC src/mathlib.cpp)
target_include_directories(mathlib PUBLIC
$
$
)
# 第一步:安装目标时,将其注册到名为 "MathLibTargets" 的导出组
install(TARGETS mathlib
EXPORT MathLibTargets # 关键:指定导出组名称
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include # 保留头文件安装信息
)
# 第二步:将导出组生成为 cmake 文件并安装
install(EXPORT MathLibTargets
FILE MathLibTargets.cmake # 生成的文件名
NAMESPACE MathLib:: # 为所有目标添加命名空间前缀
DESTINATION lib/cmake/MathLib # 安装位置
)
执行安装后(例如 cmake --install build --prefix /usr/local),你会在 /usr/local/lib/cmake/MathLib/ 目录下发现 MathLibTargets.cmake 文件。这个文件包含了 mathlib 目标的完整定义,包括其头文件路径、链接要求等。
2.3 命名空间(NAMESPACE)的重要性
通过 NAMESPACE MathLib:: 参数,导出的目标在其他项目中将显示为 MathLib::mathlib。这不仅避免了名称冲突,还遵循了 Modern CMake 的最佳实践——目标名看起来像是一个“绝对路径”,清晰表明它来自外部包。
3. 创建包配置文件:Config.cmake
仅仅有 MathLibTargets.cmake 还不够。当其他项目调用 find_package(MathLib) 时,CMake 会查找名为 MathLibConfig.cmake(或 mathlib-config.cmake)的文件。这个文件是配置包的入口点,负责:
- 查找本项目所依赖的外部库(例如
Boost、OpenSSL); - 包含导出的目标文件(即
MathLibTargets.cmake); - 设置一些辅助变量(可选);
- 检查必要的组件(
check_required_components)。
3.1 手写 Config.cmake.in 模板
我们通常在源码目录中创建一个模板文件,命名为 MathLibConfig.cmake.in:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# 如果 mathlib 本身依赖其他库,在此处查找
# 例如:find_dependency(Boost REQUIRED COMPONENTS filesystem)
# 例如:find_dependency(Threads)
# 包含导出的目标定义文件
include("${CMAKE_CURRENT_LIST_DIR}/MathLibTargets.cmake")
# 验证所有请求的目标都已加载
check_required_components(MathLib)
这里有三个关键点:
@PACKAGE_INIT@:这是一个占位符,后续会被configure_package_config_file()替换为一组标准初始化代码,负责处理包的路径重定位。include(CMakeFindDependencyMacro)和find_dependency():如果库有外部依赖,必须在 Config.cmake 中重新查找,否则下游项目链接时会报错。include("${CMAKE_CURRENT_LIST_DIR}/MathLibTargets.cmake"):导入实际的目标定义。注意使用CMAKE_CURRENT_LIST_DIR,确保路径与 Config.cmake 所在目录相对,实现可重定位。
4. configure_package_config_file:生成可重定位的配置文件
为什么不能直接使用 configure_file() 处理 MathLibConfig.cmake.in?因为安装后的包可能被安装到任意前缀(/usr/local、/opt/mathlib 或 C:Program FilesMathLib)。我们需要一个与安装位置无关的配置文件。
4.1 CMakePackageConfigHelpers 模块
CMake 提供了 CMakePackageConfigHelpers 模块,其中的 configure_package_config_file() 命令专门用于此目的:
include(CMakePackageConfigHelpers)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/MathLibConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake"
INSTALL_DESTINATION lib/cmake/MathLib
)
与普通的 configure_file 不同,configure_package_config_file 会:
- 替换
@PACKAGE_INIT@为一段标准代码,定义PACKAGE_PREFIX_DIR等变量; - 处理
@PACKAGE_@形式的变量,使其基于安装前缀自动计算; - 生成路径相关的宏(如
set_and_check),确保安装后的路径正确。
4.2 安装配置文件
生成的 MathLibConfig.cmake 需要随库一起安装:
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake"
DESTINATION lib/cmake/MathLib
)
5. 创建包版本文件:ConfigVersion.cmake
当其他项目调用 find_package(MathLib 2.1 REQUIRED) 时,CMake 需要判断已安装的 MathLib 版本是否满足要求。这个判断逻辑就由 MathLibConfigVersion.cmake 实现。
5.1 write_basic_package_version_file
同样来自 CMakePackageConfigHelpers 模块:
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfigVersion.cmake"
VERSION ${PROJECT_VERSION} # 例如 "2.1.0"
COMPATIBILITY SameMajorVersion # 兼容性策略
)
其中 COMPATIBILITY 参数决定了版本匹配规则:
AnyNewerVersion:只要已安装版本 ≥ 请求版本即可(最宽松)。SameMajorVersion:主版本号必须相同,且已安装版本 ≥ 请求版本(推荐,遵循语义化版本)。SameMinorVersion:主次版本号都必须相同。ExactVersion:必须完全匹配(最严格)。
5.2 安装版本文件
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfigVersion.cmake"
DESTINATION lib/cmake/MathLib
)
安装完成后,lib/cmake/MathLib/ 目录下将包含三个核心文件:
MathLibConfig.cmake—— 包入口,负责依赖查找和目标导入。MathLibConfigVersion.cmake—— 版本兼容性检查。MathLibTargets.cmake—— 具体目标定义。
6. 构建树导出:开发时的本地使用
上述 install(EXPORT ...) 机制仅在执行安装(cmake --install)后才生效。但在日常开发中,我们可能希望在不安装的情况下,让其他项目直接引用当前项目的构建目录(Build Tree)。
6.1 export(EXPORT …) 命令
CMake 提供了与 install(EXPORT) 对应的 export(EXPORT) 命令,用于在构建目录直接生成导出文件:
export(EXPORT MathLibTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathLibTargets.cmake"
NAMESPACE MathLib::
)
这会在构建目录下生成一份与安装版几乎相同的 MathLibTargets.cmake,但路径指向构建目录中的库文件和源文件目录。
6.2 构建树配置包的完整导出
为了支持 find_package(MathLib) 直接定位到构建树,我们可以把 MathLibConfig.cmake 也复制到构建目录,或者直接导出构建树配置:
# 将配置好的 Config.cmake 也输出到构建目录
configure_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake"
COPYONLY
)
更常见的做法是在构建目录直接生成一份简化的 MathLibConfig.cmake(通过 configure_package_config_file 指定不同的安装目的路径逻辑),不过大多数开发者只需将构建目录添加到 CMAKE_PREFIX_PATH 中即可:
# 在其他项目的配置阶段
cmake -DCMAKE_PREFIX_PATH=/path/to/mathlib/build ..
此时,find_package(MathLib) 会在构建目录中找到 MathLibConfig.cmake(如果已生成)或通过 export(PACKAGE MathLib) 注册到 CMake 用户包注册表。
6.3 export(PACKAGE <name>)
还有一个不太常用但值得了解的命令:
export(PACKAGE MathLib)
这会将当前项目的构建目录写入 CMake 的用户包注册表(~/.cmake/packages/),使得其他项目在不指定 CMAKE_PREFIX_PATH 的情况下也能通过 find_package(MathLib) 找到它。不过,这在 CI/CD 环境中较少使用,更多用于本地多项目并行开发。
7. 完整实战:一个可发布的 Modern CMake 库
下面给出一个完整的示例,将本节所有内容串联起来。假设项目结构如下:
MathLib/
├── CMakeLists.txt
├── include/
│ └── mathlib/
│ └── mathlib.h
├── src/
│ └── mathlib.cpp
└── cmake/
└── MathLibConfig.cmake.in
7.1 根 CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MathLib VERSION 2.1.0 LANGUAGES CXX)
# --- 创建库 ---
add_library(mathlib STATIC src/mathlib.cpp)
add_library(MathLib::mathlib ALIAS mathlib)
target_include_directories(mathlib PUBLIC
$
$
)
target_compile_features(mathlib PUBLIC cxx_std_17)
# --- 安装规则 ---
include(GNUInstallDirs)
install(TARGETS mathlib
EXPORT MathLibTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# --- 导出目标(安装树)---
install(EXPORT MathLibTargets
FILE MathLibTargets.cmake
NAMESPACE MathLib::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathLib
)
# --- 生成并安装配置文件 ---
include(CMakePackageConfigHelpers)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/MathLibConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathLib
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MathLibConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathLib
)
# --- 导出目标(构建树,方便本地开发)---
export(EXPORT MathLibTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathLibTargets.cmake"
NAMESPACE MathLib::
)
7.2 MathLibConfig.cmake.in
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# 示例:如果 mathlib 依赖 Threads
find_dependency(Threads)
include("${CMAKE_CURRENT_LIST_DIR}/MathLibTargets.cmake")
check_required_components(MathLib)
7.3 消费方(下游项目)的使用方式
安装完成后,其他项目可以如此使用:
cmake_minimum_required(VERSION 3.15)
project(Calculator LANGUAGES CXX)
find_package(MathLib 2.1 REQUIRED)
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE MathLib::mathlib)
无需手动写 include_directories 或 link_directories,MathLib::mathlib 会自动传递所有必要的头文件路径、编译选项和依赖库。
8. 构建树导出 vs 安装树导出:对比总结
| 特性 | 构建树导出 (export) | 安装树导出 (install) |
|---|---|---|
| 生成时机 | 配置/生成阶段 | 安装阶段 |
| 文件位置 | 构建目录内 | CMAKE_INSTALL_PREFIX 下 |
| 路径指向 | 构建目录中的库和源码 | 安装目录中的库和头文件 |
| 适用场景 | 本地多项目并行开发 | 正式发布、系统安装、CI 打包 |
配合 find_package |
需 CMAKE_PREFIX_PATH 或注册表 |
标准系统查找路径即可 |
总结
本节我们系统学习了如何将 CMake 目标导出为可供其他项目消费的配置包。核心要点回顾如下:
install(TARGETS ... EXPORT)将目标注册到导出组;install(EXPORT)生成并安装目标定义文件。configure_package_config_file()生成可重定位的Config.cmake入口文件,处理@PACKAGE_INIT@和路径适配。write_basic_package_version_file()生成版本检查文件,实现find_package的版本约束匹配。export(EXPORT)支持构建树直接导出,方便开发阶段的无安装引用。- 始终使用
NAMESPACE为导出目标添加前缀,避免全局命名污染。
掌握这些内容后,你的 CMake 项目就具备了专业级的外部可用性。下一节,我们将继续探讨 CPack 打包系统,学习如何将安装好的内容进一步打包为 DEB、RPM、NSIS 等分发格式。


没有回复内容