17、CMake查找包:find_package的机制,Find模块的编写,Config文件的生成与使用
说到CMake的包管理,我个人觉得find_package是嵌入式项目里最容易被低估的一个命令。很多人写CMakeLists.txt,直接写个find_package就完事了,结果换台机器就编译不过。嗯,这里面的门道其实不少。
17.1 find_package的两种模式
先搞清楚一个核心概念:find_package有两种工作模式——模块模式和配置模式。
说白了,模块模式就是CMake自己带了一堆FindXXX.cmake脚本,或者你自己写一个。配置模式则是库的作者自己生成了一个XXXConfig.cmake文件,你直接拿来用。
CMake会先尝试模块模式,找不到才走配置模式。但你可以用MODULE或CONFIG关键字强制指定。
关键区别:
- 模块模式:由使用者提供查找逻辑
- 配置模式:由库作者提供安装信息
我在项目中遇到过好几次这样的情况:团队里有人把第三方库的头文件路径写死在CMakeLists里,结果一升级库就崩。后来统一改成find_package,世界清净了。
17.2 模块模式:手写Find模块
什么时候需要自己写Find模块?
- 库没有提供CMake支持(比如一些老旧的嵌入式SDK)
- 库的安装路径不标准
- 你想自定义查找逻辑
一个典型的Find模块长这样:
# FindMySDK.cmake
# 查找MySDK库的CMake模块
# 先找头文件
find_path(MySDK_INCLUDE_DIR
NAMES mysdk.h
PATHS /usr/local/include
/opt/mysdk/include
)
# 再找库文件
find_library(MySDK_LIBRARY
NAMES mysdk libmysdk
PATHS /usr/local/lib
/opt/mysdk/lib
)
# 处理找到/没找到的情况
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MySDK
REQUIRED_VARS MySDK_INCLUDE_DIR MySDK_LIBRARY
)
# 设置导入目标
if(MySDK_FOUND AND NOT TARGET MySDK::MySDK)
add_library(MySDK::MySDK UNKNOWN IMPORTED)
set_target_properties(MySDK::MySDK PROPERTIES
IMPORTED_LOCATION "${MySDK_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${MySDK_INCLUDE_DIR}"
)
endif()
我的习惯:写Find模块时,一定要用find_package_handle_standard_args。这个宏会自动处理REQUIRED、QUIET等选项,省去很多重复代码。
17.3 配置模式:生成Config文件
如果你是自己写库,想让别人用起来方便,那就得生成Config文件。我个人觉得这是最优雅的方式——库作者一次性把信息打包好,使用者一行find_package搞定。
生成Config文件需要三步:
- 编写
XXXConfig.cmake.in模板文件 - 在库的CMakeLists中用
configure_package_config_file处理模板 - 用
install命令安装Config文件
模板文件示例:
# MyLibConfig.cmake.in
@PACKAGE_INIT@
# 包含目标导出文件
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
# 检查版本
check_required_components(MyLib)
库的CMakeLists中这样写:
# 生成Config文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/MyLibConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
INSTALL_DESTINATION lib/cmake/MyLib
)
# 安装Config文件
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
DESTINATION lib/cmake/MyLib
)
# 导出目标
install(TARGETS mylib
EXPORT MyLibTargets
DESTINATION lib
)
install(EXPORT MyLibTargets
DESTINATION lib/cmake/MyLib
)
我曾经踩过的坑:Config文件中的路径一定要用相对路径,基于CMAKE_CURRENT_LIST_DIR。我见过有人硬编码绝对路径,结果库换个目录就炸了。你想想看,这要是发布给客户用,多尴尬。
17.4 查找路径与优先级
CMake查找包的路径顺序,我整理了一张表:
| 优先级 | 路径 | 说明 |
|---|---|---|
| 1 | CMAKE_PREFIX_PATH |
用户指定的搜索前缀 |
| 2 | CMAKE_FIND_ROOT_PATH |
交叉编译时常用 |
| 3 | 系统默认路径 | /usr/lib, /usr/local/lib等 |
| 4 | PATHS选项 |
Find模块中指定的路径 |
嵌入式开发中,交叉编译场景特别多。我建议你在工具链文件中设置CMAKE_FIND_ROOT_PATH,这样查找包时就不会跑到宿主系统的路径里去。
17.5 版本管理与组件查找
Config文件可以附带版本信息。在模板中加入:
set(PACKAGE_VERSION "1.2.3")
set(PACKAGE_VERSION_COMPATIBLE 1)
set(PACKAGE_VERSION_EXACT 0)
# 版本比较逻辑
if(PACKAGE_FIND_VERSION VERSION_GREATER PACKAGE_VERSION)
set(PACKAGE_VERSION_COMPATIBLE 0)
endif()
使用者就可以这样写:
find_package(MyLib 1.2 EXACT REQUIRED)
组件查找也很实用。比如你的库有核心模块和扩展模块:
find_package(MyLib COMPONENTS Core Extension REQUIRED)
在Config文件中,用check_required_components来验证所有组件都找到了。
核心要点:
- 模块模式适合查找已有库
- 配置模式适合发布自己的库
- 交叉编译时务必设置
CMAKE_FIND_ROOT_PATH - Config文件中的路径必须用相对路径
17.6 知识体系总览
下面这张图把find_package的整个机制串起来了:
这张图把两种模式的流程和适用场景都画清楚了。我个人建议:如果你是库的使用者,优先用模块模式;如果你是库的作者,一定要生成Config文件。
避坑指南:我曾经在调试一个项目时,发现find_package总是找不到库。折腾了半天,原来是CMAKE_MODULE_PATH没设置对。记住:自定义的Find模块要放在这个路径下,CMake才会找到。
好了,关于find_package的机制就聊到这里。模块模式让你灵活查找,配置模式让库作者省心。两种模式各有千秋,关键看你的角色和场景。
公众号:蓝海资料掘金营,微信deep3321