16、查找包:find_package()机制详解、Config模式与Module模式、FindXXX.cmake模块编写、<Package>Config.cmake文件生成

说到 CMake 里的包管理,find_package() 绝对是个绕不开的核心。我刚开始用 CMake 那会儿,总觉得这命令有点玄学——明明装了库,怎么就找不到?后来踩了坑才明白,其实它背后就两套机制:Module 模式Config 模式。搞懂了这两兄弟,包管理这块基本就通了。

16.1 find_package() 的两种模式

你想想看,当你写下 find_package(OpenCV REQUIRED) 时,CMake 到底在干什么?它其实在按顺序做两件事:

  1. 先尝试 Module 模式——去 CMAKE_MODULE_PATH 里找 FindOpenCV.cmake 这个脚本。
  2. 如果没找到,自动切换到 Config 模式——去标准路径下找 OpenCVConfig.cmakeopencv-config.cmake

我个人习惯把 Module 模式理解为「手动挡」——你得自己写查找逻辑;Config 模式则是「自动挡」——库的作者已经帮你配好了。不过要注意,find_package() 默认先走 Module 模式,只有明确指定 CONFIG 关键字才会直接跳过。

关键区别:

  • Module 模式:执行 Find<Package>.cmake 脚本,通常由使用者提供。
  • Config 模式:直接读取 <Package>Config.cmake 文件,由库的安装过程生成。

16.2 Module 模式:手写 FindXXX.cmake

什么时候需要自己写 FindXXX.cmake?说白了,就是库本身不提供 CMake 支持,或者你想自定义查找逻辑。我在项目中遇到过好几次,比如某个老旧的三方库只提供了 .lib.dll,没有 Config.cmake,那就只能自己动手了。

一个典型的 FindFoo.cmake 长这样:

# FindFoo.cmake - 查找 Foo 库
# 设置标准变量:Foo_FOUND, Foo_INCLUDE_DIRS, Foo_LIBRARIES

find_path(Foo_INCLUDE_DIR
  NAMES foo.h
  PATHS /usr/local/include /opt/local/include
)

find_library(Foo_LIBRARY
  NAMES foo libfoo
  PATHS /usr/local/lib /opt/local/lib
)

# 处理 REQUIRED 和 QUIET 参数
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Foo
  REQUIRED_VARS Foo_INCLUDE_DIR Foo_LIBRARY
)

if(Foo_FOUND)
  set(Foo_INCLUDE_DIRS ${Foo_INCLUDE_DIR})
  set(Foo_LIBRARIES ${Foo_LIBRARY})
  # 标记为导入目标,方便现代 CMake 使用
  if(NOT TARGET Foo::Foo)
    add_library(Foo::Foo UNKNOWN IMPORTED)
    set_target_properties(Foo::Foo PROPERTIES
      IMPORTED_LOCATION "${Foo_LIBRARY}"
      INTERFACE_INCLUDE_DIRECTORIES "${Foo_INCLUDE_DIR}"
    )
  endif()
endif()

我的经验:写 Find 模块时,一定要用 find_package_handle_standard_args 这个宏。它自动处理 REQUIREDQUIET 这些参数,省去很多样板代码。我曾经手写判断逻辑,结果漏了 QUIET 支持,被同事吐槽了好久。

16.3 Config 模式:生成 <Package>Config.cmake

Config 模式才是现代 CMake 推荐的方式。库的作者在安装时,通过 install(EXPORT)configure_package_config_file() 生成 FooConfig.cmake。这样使用者只需要 find_package(Foo) 就能拿到所有信息。

生成 Config 文件的标准流程分三步:

  1. 创建导出目标——用 install(TARGETS ... EXPORT FooTargets) 把目标标记为可导出。
  2. 安装导出文件——install(EXPORT FooTargets DESTINATION lib/cmake/Foo)
  3. 编写 Config 模板——用 configure_package_config_file() 生成 FooConfig.cmake

模板文件 FooConfig.cmake.in 示例:

@PACKAGE_INIT@

# 包含导出的目标文件
include("${CMAKE_CURRENT_LIST_DIR}/FooTargets.cmake")

# 检查依赖
include(CMakeFindDependencyMacro)
find_dependency(Bar REQUIRED)

# 设置版本兼容性
set_and_check(Foo_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")
set_and_check(Foo_LIBRARY_DIR "@PACKAGE_LIB_INSTALL_DIR@")

# 标记为找到
set(Foo_FOUND TRUE)

注意:模板里一定要用 @PACKAGE_INIT@ 这个宏,它会自动生成 set_and_check 等辅助函数。我见过有人自己手写路径检查,结果路径写死导致跨平台失败——用 CMake 提供的工具函数才是正道。

16.4 两种模式的选择策略

什么时候用 Module,什么时候用 Config?我个人的判断标准很简单:

场景 推荐模式 原因
库本身提供 CMake 支持 Config 模式 直接使用,无需额外维护
老旧库或系统库 Module 模式 需要自定义查找路径和逻辑
自己开发的库 Config 模式 现代 CMake 标准做法,方便他人使用
需要版本检查 Config 模式 原生支持版本比较,Module 模式需要手写

16.5 核心流程:一张图看懂

下面这张 SVG 图展示了 find_package() 的完整决策流程。嗯,我画图时特意把两种模式的分支标清楚了,你一看就明白。

find_package() 决策流程 find_package(<Package>) Module 模式 查找 Find<Pkg>.cmake 找到? 执行脚本 Config 模式 查找 <Pkg>Config.cmake 找到? 读取配置 报错:找不到包

16.6 避坑指南

最后分享几个我踩过的坑:

  • 路径问题:我曾经把 FindXXX.cmake 放在项目根目录,结果 CMake 死活找不到。后来才发现必须设置 CMAKE_MODULE_PATH,或者把文件放到 cmake/Modules/ 下。
  • 版本兼容:Config 模式支持 find_package(Foo 2.0 EXACT),但 Module 模式需要自己实现版本检查。我建议能用 Config 就用 Config。
  • 导入目标 vs 变量:现代 CMake 推荐用导入目标(如 Foo::Foo)而不是设置 Foo_INCLUDE_DIRS 变量。前者能自动传递依赖,后者容易漏掉。
  • 跨平台路径:写 Find 模块时,PATHS 里不要写死 Windows 路径。用 ENV{PROGRAMFILES} 或者 CMAKE_SYSTEM_PREFIX_PATH 更稳妥。

一句话总结:如果你在开发库,请务必生成 <Package>Config.cmake;如果你在使用库,优先用 Config 模式。Module 模式只作为兜底方案。


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