10. 查找依赖包:find_package(),Config模式与Module模式

依赖管理,说白了就是「你的项目需要别人写好的库」。在 CMake 里,find_package() 就是干这个活的。我刚开始用 CMake 时,总觉得这命令挺玄乎——明明指定了包名,它怎么就知道去哪找?后来踩了几次坑,才搞明白它背后其实就两种模式:Config 模式和 Module 模式。

10.1 find_package() 的基本用法

先看一个最简单的例子:

find_package(OpenCV REQUIRED)
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})

这里 REQUIRED 表示「找不到就报错」。如果不加,找不到时只会给个警告,然后跳过后续代码。我个人习惯是,核心依赖必须加 REQUIRED,可选功能才不加。

为什么会这样?因为 CMake 需要知道这个包到底存不存在。你想想看,如果 OpenCV 没装,你后面还链接它的库,那编译肯定崩。

10.2 Config 模式 vs Module 模式

CMake 找包时,会按这个顺序尝试:

  1. 先找 <PackageName>Config.cmake<lowercase-package-name>-config.cmake(Config 模式)
  2. 如果没找到,再找 Find<PackageName>.cmake(Module 模式)

说白了,Config 模式是「包自己提供的查找脚本」,Module 模式是「CMake 或用户提供的查找脚本」。

核心区别:

  • Config 模式:包作者写好了 XXXConfig.cmake,安装时一并装到系统里。比如 OpenCV、Qt、PCL 这些大库都自带。
  • Module 模式:CMake 自带了一些 FindXXX.cmake(比如 FindZLIB、FindPNG),或者你自己写一个 FindXXX.cmake 放到项目里。

我在项目中遇到过这样的情况:一个第三方库只提供了 .h.lib,没有 Config.cmake。这时候就得自己写 FindXXX.cmake,走 Module 模式。

10.3 编写一个简单的 FindXXX.cmake

假设我们有一个叫 MyUtils 的库,头文件在 /usr/local/include,库文件在 /usr/local/lib。我们来写一个 FindMyUtils.cmake

# FindMyUtils.cmake
# 查找 MyUtils 库的头文件和库文件

# 先找头文件
find_path(MyUtils_INCLUDE_DIR
    NAMES myutils.h
    PATHS /usr/local/include
          /opt/local/include
)

# 再找库文件
find_library(MyUtils_LIBRARY
    NAMES myutils libmyutils
    PATHS /usr/local/lib
          /opt/local/lib
)

# 检查是否都找到了
if(MyUtils_INCLUDE_DIR AND MyUtils_LIBRARY)
    set(MyUtils_FOUND TRUE)
    set(MyUtils_INCLUDE_DIRS ${MyUtils_INCLUDE_DIR})
    set(MyUtils_LIBRARIES ${MyUtils_LIBRARY})
    
    # 给调用方提供导入目标(现代 CMake 推荐做法)
    if(NOT TARGET MyUtils::MyUtils)
        add_library(MyUtils::MyUtils UNKNOWN IMPORTED)
        set_target_properties(MyUtils::MyUtils PROPERTIES
            IMPORTED_LOCATION "${MyUtils_LIBRARY}"
            INTERFACE_INCLUDE_DIRECTORIES "${MyUtils_INCLUDE_DIR}"
        )
    endif()
else()
    set(MyUtils_FOUND FALSE)
endif()

# 标准输出
mark_as_advanced(MyUtils_INCLUDE_DIR MyUtils_LIBRARY)

嗯,这里要注意几个点:

  • find_pathfind_library 是 CMake 内置的查找命令,它们会缓存结果,第二次运行会快很多。
  • 我习惯同时设置 MyUtils_INCLUDE_DIRSMyUtils_LIBRARIES,这样兼容老项目用变量链接的方式。
  • 导入目标 MyUtils::MyUtils 是现代 CMake 的推荐做法,调用方可以直接 target_link_libraries,不用手动设置包含目录。

小技巧: 如果你想让用户能通过 MyUtils_ROOT 环境变量指定查找路径,可以在 find_pathfind_libraryPATHS 里加上 $ENV{MyUtils_ROOT}。这样用户不用改 CMakeLists.txt,直接设环境变量就行。

10.4 两种模式的查找路径对比

模式 查找的文件 典型路径 谁提供
Config 模式 XXXConfig.cmake /usr/lib/cmake/XXX/ 库作者
Module 模式 FindXXX.cmake CMake 安装目录的 Modules/ 或项目内 CMake 或用户

我曾经踩过一个坑:某个库同时提供了 Config.cmakeFindXXX.cmake,但 FindXXX.cmake 是旧版的,变量名不一样。CMake 默认优先走 Config 模式,所以没问题。但如果我手贱把 FindXXX.cmake 放到了项目里,CMake 会优先用项目里的 FindXXX.cmake(因为 CMAKE_MODULE_PATH 优先级更高),结果就用了旧版查找逻辑。嗯,从那以后我养成了习惯:明确指定用哪种模式。

10.5 强制指定模式

你可以用 MODULECONFIG 关键字强制 CMake 使用特定模式:

# 强制走 Module 模式
find_package(MyUtils MODULE REQUIRED)

# 强制走 Config 模式
find_package(MyUtils CONFIG REQUIRED)

我个人建议:如果库本身提供了 Config.cmake,优先用 Config 模式。因为库作者最清楚自己的库该怎么配置。只有那些没有 Config 的库,才自己写 FindXXX.cmake

注意事项: 如果你写 FindXXX.cmake,一定要设置 XXX_FOUND 变量。CMake 就是靠这个变量判断包是否找到的。忘了设?那 if(XXX_FOUND) 永远不成立,调用方会一脸懵。

10.6 知识体系图

下面这张图帮你理清 find_package() 的完整流程:

find_package() 查找流程 find_package(XXX) 有 Config.cmake? (XXXConfig.cmake) Config 模式 直接加载 有 FindXXX.cmake? (Module 模式) Module 模式 执行 FindXXX.cmake 报错/警告 (取决于 REQUIRED) 设置 XXX_FOUND 等变量 成功路径 失败路径 判断节点

10.7 实战建议

FindXXX.cmake 时,我总结了几个经验:

  • 变量命名要规范XXX_INCLUDE_DIRXXX_LIBRARYXXX_FOUND 是约定俗成的,别自己发明新名字。
  • 提供导入目标:现代 CMake 项目更倾向于用 target_link_libraries 而不是直接操作变量。所以尽量创建 XXX::XXX 这样的导入目标。
  • 考虑跨平台:Windows 上库文件可能叫 .lib,Linux 上叫 .so.a。用 find_library 时,CMake 会自动处理这些差异。
  • 缓存变量:用 mark_as_advanced 把查找路径标记为高级变量,这样普通用户不会在 CMake GUI 里被一堆内部变量吓到。

调试小技巧: 如果 find_package 找不到包,可以加 --debug-find 参数运行 CMake:cmake --debug-find ..。它会打印出每一步查找了哪些路径,非常直观。我曾经靠这个参数发现 CMake 在找 .cmake 文件时,路径里多了一个斜杠,导致路径拼接错误。

好了,关于 find_package() 的两种模式就聊到这。记住:Config 模式是「库主动告诉你它在哪」,Module 模式是「你去帮 CMake 找到它」。写 FindXXX.cmake 时,保持简单、规范,你的队友会感谢你的。


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