一、find_package 到底在找什么?

说实话,我刚接触 CMake 那会儿,对 find_package 的理解就是「帮我找个库」。但后来踩过几次坑才发现,这玩意儿背后藏着两套完全不同的机制——Config 模式Module 模式

简单来说,find_package 干的事就三件:

  • 找到某个库的头文件路径
  • 找到该库的二进制文件(.lib/.dll/.so/.a)
  • 把路径和库信息塞给调用者

但怎么找?找什么格式的文件?这就是模式的区别了。

核心区别一句话:Config 模式是库作者主动提供「安装说明书」;Module 模式是使用者自己写「寻宝地图」。

二、Config 模式:库作者说了算

我个人比较喜欢 Config 模式,因为它省心。库在安装时,会生成一个 <PackageName>Config.cmake<PackageName>-config.cmake 文件。这个文件里直接写好了:

  • 头文件路径
  • 库文件路径
  • 编译选项
  • 依赖关系

举个例子,你装完 Qt5,它的 Config 文件就在 /usr/lib/cmake/Qt5/Qt5Config.cmake。你写:

find_package(Qt5 REQUIRED COMPONENTS Core Widgets)

CMake 就会去预设的路径下找这个 Config 文件。找到了,一切自动配好。

小技巧:如果你不确定 Config 文件在哪,可以设 CMAKE_FIND_DEBUG_MODE=ON,CMake 会打印出它找了哪些路径。我在排查 CI 环境问题时经常用这招。

Config 模式的查找顺序

CMake 会按这个顺序找:

  1. <PackageName>_DIR 变量指定的路径
  2. CMAKE_PREFIX_PATH 里的路径
  3. 系统默认路径(如 /usr/lib/cmake)

嗯,这里要注意:如果你自己编译了一个库,没装到系统路径,一定要设 <PackageName>_DIR。我曾经因为忘了设这个,折腾了整整一个下午。

三、Module 模式:自己动手丰衣足食

那如果库作者没提供 Config 文件呢?比如一些老旧的库,或者你自己写的内部库。这时候就得用 Module 模式了。

Module 模式会去找一个 Find<PackageName>.cmake 文件。这个文件不是库作者给的,而是你自己写的,或者 CMake 自带的。

CMake 内置了很多 Find 模块,比如:

  • FindOpenGL.cmake
  • FindBoost.cmake
  • FindPython3.cmake

这些文件放在 CMake 的安装目录下的 Modules/ 文件夹里。你直接写:

find_package(OpenGL REQUIRED)

CMake 就会自动加载内置的 FindOpenGL.cmake。

注意:如果库作者提供了 Config 文件,CMake 会优先用 Config 模式。只有找不到 Config 文件时,才会回退到 Module 模式。你可以用 CONFIGMODULE 关键字强制指定模式。

四、两种模式的对比

对比项 Config 模式 Module 模式
谁提供文件 库作者 使用者或 CMake 内置
文件命名 <Name>Config.cmake Find<Name>.cmake
灵活性 高,库作者最了解自己 中,需要手动处理各种情况
维护成本 低,一次写好到处用 高,每个项目可能都要调整
适用场景 现代库、商业库 老旧库、内部库、系统库

五、手写一个 Find 模块

光说不练假把式。我们来写一个简单的 FindMyLib.cmake。假设我们有一个叫 MyLib 的库,头文件在 /opt/mylib/include,库文件在 /opt/mylib/lib

# FindMyLib.cmake
# 查找 MyLib 库的 Find 模块

# 先检查用户有没有指定路径
if(NOT MyLib_ROOT)
  set(MyLib_ROOT "/opt/mylib")
endif()

# 找头文件
find_path(MyLib_INCLUDE_DIR
  NAMES mylib.h
  PATHS ${MyLib_ROOT}/include
  NO_DEFAULT_PATH
)

# 找库文件
find_library(MyLib_LIBRARY
  NAMES mylib
  PATHS ${MyLib_ROOT}/lib
  NO_DEFAULT_PATH
)

# 处理找到/没找到的情况
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib
  REQUIRED_VARS MyLib_INCLUDE_DIR MyLib_LIBRARY
)

# 设置导出变量
if(MyLib_FOUND)
  set(MyLib_INCLUDE_DIRS ${MyLib_INCLUDE_DIR})
  set(MyLib_LIBRARIES ${MyLib_LIBRARY})
  
  # 创建导入目标(现代 CMake 推荐做法)
  if(NOT TARGET MyLib::MyLib)
    add_library(MyLib::MyLib UNKNOWN IMPORTED)
    set_target_properties(MyLib::MyLib PROPERTIES
      IMPORTED_LOCATION "${MyLib_LIBRARY}"
      INTERFACE_INCLUDE_DIRECTORIES "${MyLib_INCLUDE_DIR}"
    )
  endif()
endif()

# 标记变量为高级,不在 cmake-gui 中默认显示
mark_as_advanced(MyLib_INCLUDE_DIR MyLib_LIBRARY)

个人经验:写 Find 模块时,一定要用 find_package_handle_standard_args 这个宏。它会自动处理版本检查、错误信息等。我第一次写 Find 模块时没用它,结果报错信息乱七八糟,用户根本看不懂。

六、知识体系总览

下面这张图把 find_package 的整个流程串起来了。你看一遍应该就能记住:

find_package 工作流程 find_package 调用 有 Config 文件? Config 模式 加载 xxxConfig.cmake Module 模式 加载 Findxxx.cmake 查找路径 CMAKE_PREFIX_PATH 查找路径 <Name>_DIR 找到没? 找到 设置变量 <Name>_FOUND = TRUE 没找到 报错或跳过 REQUIRED 则报错

七、避坑指南

最后分享几个我实际项目中踩过的坑:

  • 路径大小写敏感:Linux 上 FindOpenGL.cmakefind_package(OPENGL) 是两码事。我曾经因为大小写不匹配,找了半天没找到模块。
  • REQUIRED 慎用:如果你不确定目标机器上有没有这个库,别加 REQUIRED。否则 CMake 直接报错退出。我一般先不加 REQUIRED 做探测,再根据 <Name>_FOUND 做条件编译。
  • 版本号匹配:Config 模式会检查版本号。如果你写的 find_package(MyLib 2.0),但系统里只有 1.0,CMake 会告诉你没找到。这时候可以加 EXACT 关键字要求精确匹配。

曾经有一次:我在一个项目里同时用了 OpenCV 和 PCL,两个库都依赖 Eigen3。结果 find_package 找到了两个不同版本的 Eigen3,编译直接崩了。后来我统一设了 Eigen3_DIR 才解决。所以,依赖管理一定要小心版本冲突

好了,关于 find_package 的机制就讲到这里。Config 模式和 Module 模式各有优劣,关键看你的使用场景。写 Find 模块时记得用 find_package_handle_standard_args,能省不少事。


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