引言:从“交钥匙”到“出图纸”
在上一节(6.1)中,我们学会了如何把编译好的可执行文件、库、头文件和配置文件,整整齐齐地“搬进”系统的安装目录。这就像是建筑工程的交付仪式:钥匙已经交到业主手里,房间打扫干净了。但问题来了——如果另一位开发商(另一个 CMake 项目)想在隔壁盖一栋楼,想直接利用你这栋楼的地基和钢结构(你的库),他该怎么找到你?
如果仅仅是把 .a 或 .so 文件Copy过去,对方项目还得手动写 include_directories、link_libraries,甚至要搞清楚你的依赖链里还藏着哪些第三方库。这显然违背了 Modern CMake “基于目标(Target)”的哲学。
正确的做法是:在交付建筑实体的同时,交付一套完整的“建筑图纸和使用说明书”。这套文件能让对方的 CMake 通过一句 find_package(MyLib REQUIRED) 就自动找回你的目标、头文件路径、依赖库和编译选项。这一节,我们就要学会制作这套图纸——导出目标(Export Targets)与配置包(Config Package)。
install(EXPORT …):生成目标的“身份档案”
当你用 install(TARGETS) 把库文件安放到系统目录时,CMake 其实只搬运了“实体”(二进制文件)。但目标本身携带的诸多属性——比如头文件搜索路径、链接依赖、宏定义——这些“灵魂信息”还停留在你的构建树里。你需要一份导出文件,把这些目标的完整定义誊写成 CMake 能读懂的脚本。
这份导出文件的生成,靠的就是 install(EXPORT ...) 指令。
基本写法
# 假设你定义了一个库目标
add_library(mylib SHARED src/mylib.cpp)
target_include_directories(mylib PUBLIC
<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
<INSTALL_INTERFACE:include> # 安装后使用的路径
)
# 1. 先为目标注册到导出集(EXPORT)
install(TARGETS mylib
EXPORT MyLibTargets # 注册到名为 MyLibTargets 的导出集
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
# 2. 安装导出集本身:生成 MyLibTargets.cmake
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake # 生成的文件名
NAMESPACE MyLib:: # 给所有目标加命名空间前缀
DESTINATION lib/cmake/MyLib # 安装到哪里
)
上面这段代码会在安装目录的 lib/cmake/MyLib/MyLibTargets.cmake 处生成一个脚本。里面大致是这样的内容:
# 伪代码示意
add_library(MyLib::mylib SHARED IMPORTED)
set_target_properties(MyLib::mylib PROPERTIES
IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/libmylib.so"
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
这意味着,只要消费者项目 include() 了这个文件,MyLib::mylib 这个目标就“复活”了,它携带了所有 INTERFACE 属性,消费者只需要 target_link_libraries(consumer PRIVATE MyLib::mylib) 即可。
构建树导出 vs 安装树导出:两把钥匙开两把锁
CMake 其实提供了两种导出场景。很多初学者在这里混淆,导致本地开发能跑,一安装到系统就报错。
场景一:构建树导出(Build Tree Export)—— 开发期自用
假设你的库项目和消费项目在同一个大仓库里,或者你正在开发库本身,想在不执行 make install 的情况下,让外面的项目直接链接你构建目录里的产物。这时你需要构建树导出:
export(EXPORT MyLibTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MyLibTargets.cmake"
NAMESPACE MyLib::
)
这会在你的 build/ 目录下生成 MyLibTargets.cmake。外部项目可以通过 -DMyLib_DIR=/path/to/build 或者直接在 CMAKE_PREFIX_PATH 里加入构建目录来找到它。
注意:构建树导出使用的是绝对路径,因为库还没被安装到标准位置。它只适合你本地开发调试,不适合发布。
场景二:安装树导出(Install Tree Export)—— 正式发布
上一节讲的 install(EXPORT ...) 就是安装树导出。它生成的是相对路径(基于 _IMPORT_PREFIX),因此无论用户把库安装到 /usr/local 还是 /home/user/custom,只要消费者用 find_package 找到了配置目录,路径都能自动解析正确。
最佳实践:两者都要做
一个优秀的 CMake 项目通常会同时提供两种导出:
- 构建树导出:方便自己或同事在超级构建(Superbuild)中直接引用。
- 安装树导出:方便最终用户通过包管理器或手动安装后使用。
# 构建树导出(可选,但强烈推荐)
export(EXPORT MyLibTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MyLibTargets.cmake"
NAMESPACE MyLib::
)
# 安装树导出(必须,面向最终用户)
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
创建包配置文件 Config.cmake:给 find_package 的“使用说明书”
光有 MyLibTargets.cmake 还不够。当用户写下 find_package(MyLib REQUIRED) 时,CMake 会按照搜索顺序去找 MyLibConfig.cmake(或 mylib-config.cmake)。这个文件就是包配置文件,相当于图书馆的索引卡片:它告诉 CMake 去哪里加载目标导出文件,以及你的包有哪些公开的依赖需要提前找到。
最简单的手写 Config.cmake
如果你的库没有任何外部依赖,理论上可以直接手写一个极简版:
# MyLibConfig.cmake
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
把上面这个文件也安装到 lib/cmake/MyLib/ 就行。但这太理想了。现实中,如果你的库本身依赖了 Boost::filesystem 或 Eigen3,并且这些依赖是 PUBLIC 的,那么消费者在链接你的库之前,必须先找到这些依赖。否则目标 MyLib::mylib 身上的 INTERFACE_LINK_LIBRARIES 指向的是未定义的目标,CMake 会直接报错。
处理依赖:find_dependency 宏
CMake 提供了 CMakeFindDependencyMacro 模块,里面封装了 find_dependency 宏。它和 find_package 类似,但如果找不到依赖,会给出更友好的错误信息,并且能把 QUIET 和 REQUIRED 等参数正确透传。
# MyLibConfig.cmake.in(模板文件,注意后缀 .in)
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# 列出本库对外公开的所有依赖
find_dependency(Boost 1.70 REQUIRED COMPONENTS filesystem)
find_dependency(Eigen3 3.3 REQUIRED)
# 加载导出的目标
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
# 检查必须的组件是否都存在(可选)
check_required_components(MyLib)
这里有几个关键点:
@PACKAGE_INIT@:这是一个占位符,后面会用configure_package_config_file替换为一组标准初始化宏。暂时可理解为“启用可重定位支持”的开关。find_dependency:只写PUBLIC或INTERFACE依赖。如果是PRIVATE依赖(比如只在你库的 .cpp 里用了,头文件没暴露),不需要也不应该写在这里。check_required_components():如果用户使用了find_package(MyLib REQUIRED COMPONENTS xxx),这个宏负责检查xxx是否被满足。
configure_package_config_file:让配置包“走到哪都能用”
如果你直接用传统的 configure_file(MyLibConfig.cmake.in MyLibConfig.cmake) 来处理上面的模板,会有一个隐患:路径被写死为构建机的绝对路径。比如 @CMAKE_INSTALL_PREFIX@ 在打包机上是 /home/ci/workspace/install,但用户安装到了 /opt/mylib,这就彻底失效了。
CMake 的 CMakePackageConfigHelpers 模块提供了一个更聪明的工具:configure_package_config_file。它会生成可重定位(Relocatable)的配置文件,无论包被安装到哪个前缀目录,都能自动推导出正确路径。
完整用法
include(CMakePackageConfigHelpers)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/MyLibConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
INSTALL_DESTINATION lib/cmake/MyLib
)
它会读取你的 .in 模板,做两件事:
- 把
@PACKAGE_INIT@替换为一组宏定义,其中最重要的是设置PACKAGE_PREFIX_DIR变量指向安装前缀(即MyLibConfig.cmake文件所在位置的 ../../../ 相对推算结果)。 - 处理模板里使用
@PACKAGE_XXX_DIR@形式的变量,自动计算基于安装前缀的相对路径。
模板中可使用的特殊变量
在 .in 模板里,除了常规的 @VAR@,还可以使用 @PACKAGE_CMAKE_INSTALL_INCLUDEDIR@ 等由 GNUInstallDirs 定义的路径变量,这些都会被转换成相对于安装前缀的路径。
write_basic_package_version_file:版本兼容性检查
现代 CMake 项目几乎都支持版本约束,比如消费者写:
find_package(MyLib 2.5 REQUIRED)
这就要求你提供一个 ConfigVersion.cmake 文件(也叫 MyLibConfigVersion.cmake),CMake 通过它来快速判断已安装的版本是否符合消费者的要求,而无需启动复杂的探测逻辑。
生成方法
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
VERSION ${PROJECT_VERSION} # 例如 2.5.1
COMPATIBILITY SameMajorVersion # 兼容性策略
)
COMPATIBILITY 参数非常关键,它决定了什么样的版本被视为“兼容”。常见策略有:
- AnyNewerVersion:只要已安装版本 ≥ 要求的版本,就兼容。最宽松。
- SameMajorVersion:主版本号必须一致。例如要求 2.5,安装 2.8 可以通过,但 3.0 不行。这是最常用的语义化版本策略。
- SameMinorVersion:主次版本都必须一致。要求 2.5,安装 2.6 都不行。
- ExactVersion:必须完全一致,连补丁号都不能差。极少使用。
生成后,别忘了安装它:
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
DESTINATION lib/cmake/MyLib
)
完整实战:打造支持 find_package 的 C++ 数学库
让我们把前面所有碎片拼起来,看一个完整示例。假设项目叫 MathUtils,版本 1.2.0,它依赖 Eigen3(PUBLIC 依赖,因为头文件暴露了 Eigen 类型)。
目录结构
MathUtils/
├── CMakeLists.txt
├── cmake/
│ └── MathUtilsConfig.cmake.in
├── include/
│ └── mathutils/
│ └── vector_ops.h
└── src/
└── vector_ops.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MathUtils VERSION 1.2.0 LANGUAGES CXX)
# 1. 查找本库自己的依赖(构建时)
find_package(Eigen3 3.3 REQUIRED NO_MODULE)
# 2. 创建库
add_library(mathutils src/vector_ops.cpp)
target_include_directories(mathutils PUBLIC
<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
<INSTALL_INTERFACE:include>
)
target_link_libraries(mathutils PUBLIC Eigen3::Eigen)
target_compile_features(mathutils PUBLIC cxx_std_17)
# 3. 安装规则
include(GNUInstallDirs)
install(TARGETS mathutils
EXPORT MathUtilsTargets
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}
)
# 4. 导出目标(构建树 + 安装树)
export(EXPORT MathUtilsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathUtilsTargets.cmake"
NAMESPACE MathUtils::
)
install(EXPORT MathUtilsTargets
FILE MathUtilsTargets.cmake
NAMESPACE MathUtils::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathUtils
)
# 5. 生成配置文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/MathUtilsConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfig.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathUtils
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
# 6. 安装配置文件
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathUtils
)
cmake/MathUtilsConfig.cmake.in 模板
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# 公开依赖必须在这里再找一遍
find_dependency(Eigen3 3.3)
# 加载导出的目标
include("${CMAKE_CURRENT_LIST_DIR}/MathUtilsTargets.cmake")
# 如果用户用了 REQUIRED COMPONENTS,这里做最终检查
check_required_components(MathUtils)
消费者的使用方式
安装完成后(比如 cmake --install build --prefix /opt/mathutils),其他项目可以无缝使用:
cmake_minimum_required(VERSION 3.15)
project(Consumer)
find_package(MathUtils 1.2 REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE MathUtils::mathutils)
CMake 会自动处理:
- 找到
/opt/mathutils/lib/cmake/MathUtils/MathUtilsConfigVersion.cmake,确认 1.2 ≤ 1.2.0 且主版本兼容。 - 执行
MathUtilsConfig.cmake,里面调用find_dependency(Eigen3),先确保 Eigen3 存在。 - 包含
MathUtilsTargets.cmake,恢复MathUtils::mathutils目标。 target_link_libraries不仅链接了libmathutils.so,还自动继承了 Eigen3 的头文件路径和 C++17 标准设定。
常见问题排雷
Q1: 构建树导出能用,为什么安装后 find_package 报错找不到目标?
通常是因为 install(EXPORT ...) 时指定的 DESTINATION 和 configure_package_config_file 里的 INSTALL_DESTINATION 不一致。CMake 通过后者计算 PACKAGE_PREFIX_DIR,如果对不上号,路径推算就会出错。
Q2: 我的库有 PRIVATE 依赖(比如只用在 .cpp 里的 zlib),需要写进 Config.cmake.in 吗?
不需要。 find_dependency 只应该列出消费者也必须链接的 PUBLIC 或 INTERFACE 依赖。PRIVATE 依赖只影响你的库本身编译,不影响消费者的接口。但要注意:如果你生成的是静态库,链接器可能需要解析所有符号,那时 PRIVATE 依赖的库文件也必须能被找到,但路径问题通常由安装时的 RPATH 或消费者环境解决,而不是通过 Config.cmake 暴露。
Q3: NAMESPACE 一定要加吗?
技术上不是强制的,但强烈推荐。加命名空间(如 MyLib::target)可以避免目标名冲突,也明确告诉消费者“这是一个 IMPORTED 目标,不是你的本地目标”。不加命名空间,万一消费者自己也定义了一个叫 mathutils 的目标,就会产生难以调试的冲突。
总结:制作配置包的三件套
要让一个 CMake 项目能够被他人通过 find_package 优雅地使用,你需要准备好以下三件套:
- 目标导出文件(
*Targets.cmake):由install(EXPORT ...)生成,内含目标的完整属性定义。 - 包配置文件(
*Config.cmake):由configure_package_config_file从模板生成,负责查找公开依赖并加载目标导出文件。 - 包版本文件(
*ConfigVersion.cmake):由write_basic_package_version_file生成,负责版本兼容性检查。
同时,提供构建树导出(export(EXPORT ...))可以极大提升开发效率,让超级构建或本地多项目联调无需频繁执行安装。
掌握了这套流程,你的库就不再是一堆孤零零的二进制文件,而是一个具备“自描述能力”的现代 CMake 包。下一节(6.3),我们将继续深入,学习如何用 CPack 把这些内容打包成 .deb、.rpm 或安装程序,真正做到一键分发。


没有回复内容