9、查找依赖包:find_package命令详解、Config模式与Module模式的区别、编写简单的FindXXX.cmake模块

好,咱们进入第九章。这一章聊的是 find_package,说白了就是 CMake 里找依赖包的那把钥匙。你写一个项目,总得用第三方库吧?OpenCV、Boost、Eigen……怎么让 CMake 知道它们在哪?就是靠 find_package

我个人习惯,每次新建项目第一件事就是写好 find_package 调用。为什么?因为后面所有 target_link_libraries 都依赖它。找不对包,后面全是红错。嗯,咱们今天就把这个命令彻底讲透。

9.1 find_package 的基本用法

先看一个最简单的例子:

find_package(OpenCV REQUIRED)

这一行干了什么?它告诉 CMake:去系统里找 OpenCV,找不到就报错(因为加了 REQUIRED)。找到之后,CMake 会设置一堆变量,比如 OpenCV_INCLUDE_DIRSOpenCV_LIBS,然后你就可以用 target_link_libraries 链接了。

但这里有个坑——find_package 其实有两种工作模式:Module 模式Config 模式。很多新手搞不清,结果包明明装了却找不到。我当年也踩过这个坑。

9.2 Config 模式 vs Module 模式

这两种模式的区别,说白了就是「谁来提供查找逻辑」。

对比项 Config 模式 Module 模式
查找依据 包自带的 XXXConfig.cmake 文件 CMake 自带的或用户写的 FindXXX.cmake 脚本
谁维护 第三方库的作者 CMake 社区或你自己
优先级 高(先尝试 Config 模式) 低(Config 找不到才用 Module)
典型例子 OpenCV、Qt、PCL BZip2、CURL、Threads

你想想看,Config 模式是「包主动告诉你我在哪」,而 Module 模式是「CMake 自己去猜你在哪」。显然 Config 更可靠。所以现代库基本都提供 XXXConfig.cmake

但有些老库或者系统库,比如 libcurl,它自己不提供 Config 文件。这时候 CMake 就靠内置的 FindCURL.cmake 模块来查找。这就是 Module 模式存在的意义。

核心区别一句话: Config 模式是库作者写的查找逻辑,Module 模式是 CMake 或用户写的查找逻辑。

9.3 查找顺序与 CMAKE_MODULE_PATH

CMake 执行 find_package 时,查找顺序是这样的:

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

那 CMake 去哪找这些文件呢?

  • Config 文件:搜索 CMAKE_PREFIX_PATH、系统路径、注册表等
  • Module 文件:搜索 CMAKE_MODULE_PATH 指定的目录

这里有个技巧:如果你自己写了一个 FindXXX.cmake,一定要把它放到 CMAKE_MODULE_PATH 指向的目录里。比如:

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules")
find_package(MyLib REQUIRED)

我曾经在一个项目里,把 FindXXX.cmake 放在了项目根目录,结果死活找不到。后来才发现 CMAKE_MODULE_PATH 默认是空的。嗯,这个坑我记了好几年。

9.4 编写一个简单的 FindXXX.cmake 模块

好,咱们动手写一个。假设有个库叫 Foo,它没有提供 Config 文件,但你知道它的头文件在 /usr/include/foo,库文件在 /usr/lib/libfoo.so。我们来写 FindFoo.cmake

# FindFoo.cmake - 查找 Foo 库的模块
# 用法: find_package(Foo REQUIRED)

# 查找头文件路径
find_path(Foo_INCLUDE_DIR
    NAMES foo.h
    PATHS /usr/include /usr/local/include
    PATH_SUFFIXES foo
)

# 查找库文件
find_library(Foo_LIBRARY
    NAMES foo libfoo
    PATHS /usr/lib /usr/local/lib
)

# 处理找到/未找到的情况
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Foo
    REQUIRED_VARS Foo_INCLUDE_DIR Foo_LIBRARY
)

# 如果找到,设置导入目标
if(Foo_FOUND AND 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()

# 隐藏内部变量
mark_as_advanced(Foo_INCLUDE_DIR Foo_LIBRARY)

这个模块做了几件事:

  • find_pathfind_library 分别找头文件和库文件
  • find_package_handle_standard_args 处理标准输出(比如设置 Foo_FOUND 变量)
  • 创建了一个导入目标 Foo::Foo,方便后续用 target_link_libraries

小提示: 尽量使用导入目标(IMPORTED target)而不是直接操作变量。这样更现代、更干净。我见过太多老项目还在手动拼 -I-L 参数,那真是上个时代的写法了。

9.5 避坑指南

FindXXX.cmake 时,有几个地方容易翻车:

  • 变量名大小写: CMake 变量名是大小写敏感的。比如 Foo_INCLUDE_DIRfoo_include_dir 是两个变量。我建议统一用 <包名>_INCLUDE_DIR 这种风格。
  • 路径后缀: 有些库的头文件在 include/foo 子目录下,记得加 PATH_SUFFIXES
  • 多库依赖: 如果一个库依赖另一个库,你需要在 FindXXX.cmake 里也调用 find_package 找依赖。比如 FindBoost 内部会找 thread、system 等组件。

警告: 不要在你的 FindXXX.cmake 里写死路径!比如 set(Foo_INCLUDE_DIR "/opt/foo/include")。这样别人机器上根本跑不通。一定要用 find_pathfind_library 动态查找。

9.6 知识体系图

下面这张图总结了 find_package 的完整流程和两种模式的关系:

find_package 执行流程 find_package(Foo) 查找 FooConfig.cmake (Config 模式) 找到!设置变量和目标 没找到,进入 Module 模式 查找 FindFoo.cmake (Module 模式) 找到!执行查找逻辑 报错:找不到 Foo 设置 Foo_FOUND 等变量 图例: 找到 未找到 Module 模式

从图上可以清楚看到:CMake 优先走 Config 模式,找不到才退回到 Module 模式。两种模式最终都会设置 Foo_FOUND 等变量,但查找逻辑的来源完全不同。

9.7 实战建议

最后,给你几个实战中的建议:

  • 优先用 Config 模式: 如果库提供了 XXXConfig.cmake,直接用。别自己写 Find 模块,那是给自己找麻烦。
  • 写 Find 模块时,一定要用 find_package_handle_standard_args 这个宏帮你处理了版本检查、错误信息等一堆细节。我早期手写这些逻辑,代码又臭又长,后来发现官方提供了这个工具,真香。
  • 测试你的 Find 模块: 在干净的 Docker 容器里跑一遍,确保没有硬编码路径。我曾经写了一个 Find 模块,本地跑得好好的,同事一拉代码就崩,最后发现我忘了加 PATH_SUFFIXES

好了,这一章的内容就到这。find_package 是 CMake 里最常用的命令之一,理解了 Config 和 Module 两种模式的区别,你就能应对绝大多数第三方库的查找场景。下一章咱们聊聊更高级的依赖管理——但那是后话了。


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