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会先尝试模块模式,找不到才走配置模式。但你可以用MODULECONFIG关键字强制指定。

关键区别:

  • 模块模式:由使用者提供查找逻辑
  • 配置模式:由库作者提供安装信息

我在项目中遇到过好几次这样的情况:团队里有人把第三方库的头文件路径写死在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。这个宏会自动处理REQUIREDQUIET等选项,省去很多重复代码。

17.3 配置模式:生成Config文件

如果你是自己写库,想让别人用起来方便,那就得生成Config文件。我个人觉得这是最优雅的方式——库作者一次性把信息打包好,使用者一行find_package搞定。

生成Config文件需要三步:

  1. 编写XXXConfig.cmake.in模板文件
  2. 在库的CMakeLists中用configure_package_config_file处理模板
  3. 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的整个机制串起来了:

find_package 机制总览 find_package 模块模式 (MODULE) 配置模式 (CONFIG) 查找 FindXXX.cmake 脚本 使用者自定义查找逻辑 适合老旧库、非标准路径 查找 XXXConfig.cmake 文件 库作者提供安装信息 支持版本、组件、导入目标 输出:XXX_FOUND、导入目标、变量

这张图把两种模式的流程和适用场景都画清楚了。我个人建议:如果你是库的使用者,优先用模块模式;如果你是库的作者,一定要生成Config文件。

避坑指南:我曾经在调试一个项目时,发现find_package总是找不到库。折腾了半天,原来是CMAKE_MODULE_PATH没设置对。记住:自定义的Find模块要放在这个路径下,CMake才会找到。

好了,关于find_package的机制就聊到这里。模块模式让你灵活查找,配置模式让库作者省心。两种模式各有千秋,关键看你的角色和场景。


公众号:蓝海资料掘金营,微信deep3321