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_DIRS、OpenCV_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 时,查找顺序是这样的:
- 先找
<PackageName>Config.cmake或<lower-case-package-name>-config.cmake(Config 模式) - 如果没找到,再找
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_path和find_library分别找头文件和库文件 - 用
find_package_handle_standard_args处理标准输出(比如设置Foo_FOUND变量) - 创建了一个导入目标
Foo::Foo,方便后续用target_link_libraries
小提示: 尽量使用导入目标(IMPORTED target)而不是直接操作变量。这样更现代、更干净。我见过太多老项目还在手动拼 -I 和 -L 参数,那真是上个时代的写法了。
9.5 避坑指南
写 FindXXX.cmake 时,有几个地方容易翻车:
- 变量名大小写: CMake 变量名是大小写敏感的。比如
Foo_INCLUDE_DIR和foo_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_path 和 find_library 动态查找。
9.6 知识体系图
下面这张图总结了 find_package 的完整流程和两种模式的关系:
从图上可以清楚看到: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