11、编写Config文件:configure_package_config_file、write_basic_package_version_file、安装Config文件
好,咱们今天聊一个实战中绕不开的话题——Config文件。
你想想看,你写了一个库,费了半天劲,编译、测试都过了。别人想用,总不能让人家去翻你的源码找头文件路径、库文件路径吧?这时候,Config文件就派上用场了。它就像是你库的「身份证」加「使用说明书」,告诉CMake:我的头文件在哪、库文件在哪、依赖什么、版本是多少。
我个人习惯,只要是一个稍微正经点的库,我都会写Config文件。哪怕只是内部项目,也值得花这十分钟。为什么?因为省事。你省的是别人(包括未来的你自己)的时间。
11.1 为什么要写Config文件?
说白了,Config文件就是让find_package(MyLib)能正常工作。CMake的find_package有两种模式:Module模式和Config模式。Module模式找的是FindXXX.cmake脚本,那是别人帮你写的。Config模式找的是你自己提供的XXXConfig.cmake文件,这才是正主。
我在项目中遇到过好几次,团队里有人把库装好了,但find_package就是找不到。一查,Config文件没装,或者装错位置了。嗯,这种坑踩过一次就记住了。
11.2 configure_package_config_file:生成Config文件
CMake提供了一个专门用来生成Config文件的命令——configure_package_config_file。它和普通的configure_file有什么区别?区别大了。
configure_package_config_file会自动处理路径的「相对化」。什么意思?你想想看,你的库安装后,头文件可能在/usr/local/include,库文件在/usr/local/lib。但用户可能把库装到别的地方,比如/opt/mylib。如果你在Config文件里写死了绝对路径,那就完蛋了——换个安装位置就找不到。
configure_package_config_file会帮你把路径写成相对于Config文件本身的路径。这样,只要Config文件和头文件、库文件的相对位置不变,不管装到哪,都能找到。
用法很简单,先写一个模板文件,比如MyLibConfig.cmake.in:
# MyLibConfig.cmake.in
@PACKAGE_INIT@
# 设置头文件路径
set_and_check(MyLib_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")
# 设置库文件路径
set_and_check(MyLib_LIBRARY_DIR "@PACKAGE_LIB_INSTALL_DIR@")
# 导入目标
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
# 检查版本
check_required_components(MyLib)
然后在CMakeLists.txt里调用:
include(CMakePackageConfigHelpers)
configure_package_config_file(
"${PROJECT_SOURCE_DIR}/cmake/MyLibConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
INSTALL_DESTINATION "lib/cmake/MyLib"
PATH_VARS
INCLUDE_INSTALL_DIR
LIB_INSTALL_DIR
)
注意那个@PACKAGE_INIT@,它会被展开成一些初始化代码,包括set_and_check宏的定义。set_and_check和普通的set不一样,它会检查路径是否存在。如果路径不存在,直接报错。这个设计很贴心——你想想看,如果用户安装不完整,早点报错总比运行时崩溃好。
cmake/目录下,和源码分开。这样结构清晰,别人一看就知道这是CMake相关的文件。
11.3 write_basic_package_version_file:版本文件
光有Config文件还不够,你还需要一个版本文件。为什么?因为用户可能写find_package(MyLib 2.0 EXACT),如果没有版本文件,CMake没法判断版本是否匹配。
write_basic_package_version_file就是干这个的。它生成一个MyLibConfigVersion.cmake文件,里面包含了版本号和兼容性检查逻辑。
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
VERSION "${PROJECT_VERSION}"
COMPATIBILITY AnyNewerVersion
)
这里COMPATIBILITY参数有几种选择:
| 模式 | 含义 | 适用场景 |
|---|---|---|
AnyNewerVersion |
只要请求的版本 ≤ 实际版本,就认为兼容 | API向后兼容的库 |
SameMajorVersion |
主版本号必须相同,次版本号可以更高 | 主版本间不兼容的库 |
SameMinorVersion |
主版本和次版本都必须相同 | 小版本间都可能不兼容 |
ExactVersion |
版本必须完全一致 | 严格匹配的场景 |
我曾经在一个项目里用了AnyNewerVersion,结果后来改了API,忘了更新版本号。下游项目直接崩了。嗯,从那以后我学乖了——API有破坏性变更时,一定要升主版本号,并且用SameMajorVersion。
11.4 安装Config文件
文件生成好了,接下来就是安装。安装Config文件和安装普通文件没什么区别,用install命令就行。但要注意安装位置。
CMake有一个约定俗成的路径:lib/cmake/<PackageName>/。比如你的库叫MyLib,Config文件就装到lib/cmake/MyLib/下。这样find_package会自动找到。
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
DESTINATION "lib/cmake/MyLib"
)
如果你用了CMake的install(TARGETS ... EXPORT ...),还会生成一个MyLibTargets.cmake文件。这个文件也要一起装:
install(
EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION "lib/cmake/MyLib"
)
这样,用户就可以用MyLib::MyLib这种带命名空间的方式来引用你的库目标了。我个人非常推荐这种做法——命名空间可以避免目标名冲突,而且语义更清晰。
11.5 完整的Config文件结构
下面是一个完整的例子,把上面说的串起来:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyLib VERSION 1.2.3)
# 添加库
add_library(MyLib src/myclass.cpp)
target_include_directories(MyLib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 安装目标
install(TARGETS MyLib
EXPORT MyLibTargets
INCLUDES DESTINATION include
)
# 生成Config文件
include(CMakePackageConfigHelpers)
set(INCLUDE_INSTALL_DIR "include")
set(LIB_INSTALL_DIR "lib")
configure_package_config_file(
"${PROJECT_SOURCE_DIR}/cmake/MyLibConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
INSTALL_DESTINATION "lib/cmake/MyLib"
PATH_VARS
INCLUDE_INSTALL_DIR
LIB_INSTALL_DIR
)
# 生成版本文件
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
VERSION "${PROJECT_VERSION}"
COMPATIBILITY SameMajorVersion
)
# 安装Config文件
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
DESTINATION "lib/cmake/MyLib"
)
# 安装目标导出文件
install(
EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION "lib/cmake/MyLib"
)
对应的模板文件MyLibConfig.cmake.in:
@PACKAGE_INIT@
set_and_check(MyLib_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")
set_and_check(MyLib_LIBRARY_DIR "@PACKAGE_LIB_INSTALL_DIR@")
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
check_required_components(MyLib)
11.6 知识体系总览
下面这张图帮你理清整个流程:
11.7 避坑指南
最后,分享几个我踩过的坑:
- 路径写死:不要在Config模板里写绝对路径。用
@PACKAGE_...@占位符,让configure_package_config_file帮你处理。 - 忘了装版本文件:没有版本文件,
find_package带版本号参数时会失败。我刚开始就犯过这个错,查了半天才发现。 - INSTALL_DESTINATION写错:这个路径必须和实际安装路径一致。如果写错了,Config文件里的相对路径会算错。
- 忘了调用check_required_components:这个宏会检查所有必需的组件是否都找到了。如果不调用,用户少装了一个组件也不会报错,运行时才崩溃。
嗯,Config文件这块其实不难,但细节挺多。你只要按照上面的流程走一遍,基本不会出问题。记住一个原则:你的库,你自己负责让它好用。写Config文件就是对自己和用户负责。