22. 6.2 导出目标与配置包

导语

在上一节中,我们学习了如何通过 install() 命令将构建产物(可执行文件、库、头文件等)安装到系统目录。然而,仅仅把文件复制到指定位置,并不能让其他 CMake 项目方便地使用你的库。其他开发者仍然需要手动指定头文件路径、库文件路径以及传递依赖,这无疑回到了“手工管理依赖”的原始时代。

Modern CMake 的解决方案是导出目标(Exporting Targets)配置包(Config Package)。通过这套机制,你的项目可以生成一组标准的 .cmake 文件,让其他项目只需要一句 find_package(MathLib REQUIRED),就能自动获得所有头文件路径、库文件链接以及传递依赖。

本节我们将手把手完成从导出目标到创建完整配置包的全过程,涵盖 install(EXPORT ...)configure_package_config_filewrite_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 基本工作流程

导出目标需要两步配合:

  1. install(TARGETS) 时,通过 EXPORT 参数将目标加入到一个导出组(Export Set)中;
  2. 使用 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)的文件。这个文件是配置包的入口点,负责:

  • 查找本项目所依赖的外部库(例如 BoostOpenSSL);
  • 包含导出的目标文件(即 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/mathlibC: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/ 目录下将包含三个核心文件:

  1. MathLibConfig.cmake —— 包入口,负责依赖查找和目标导入。
  2. MathLibConfigVersion.cmake —— 版本兼容性检查。
  3. 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_directorieslink_directoriesMathLib::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 等分发格式。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……