12、查找包:find_package命令、Find模块、Config模式、pkg-config集成、编写自己的Find模块
说实话,CMake 里最让我头疼过的功能,就是查找第三方库。早期我写项目时,经常因为库找不到、版本不对、路径写死,导致同事一拉代码就编译失败。后来我才彻底搞懂 find_package 的套路——说白了,它就是个「找东西」的机制,但找法有好几种。
这一章,我们就把它彻底讲透。
12.1 find_package 的基本用法
find_package 是 CMake 里最常用的命令之一。它的核心任务就一个:找到某个库的头文件和库文件,并设置好相关的变量。
基本语法长这样:
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[CONFIG|NO_MODULE]
[NAMES name1 [name2 ...]]
[PATHS path1 [path2 ...]]
)
我个人习惯,只要项目依赖的库是必需的,一定会加上 REQUIRED。这样如果找不到,CMake 会直接报错,而不是默默跳过。你想想看,如果库没找到但编译继续,最后链接时蹦出一堆 undefined reference,那才叫崩溃。
QUIET 可以抑制找不到时的警告信息。适合在尝试多个备选库时使用。
12.2 两种模式:Module 模式 vs Config 模式
find_package 有两种工作模式:Module 模式 和 Config 模式。理解它们的区别,是掌握查找包的关键。
| 特性 | Module 模式 | Config 模式 |
|---|---|---|
| 查找依据 | Find<Package>.cmake 脚本 | <Package>Config.cmake 或 <lower-case-package>-config.cmake |
| 谁提供 | CMake 自带或用户编写 | 库的开发者提供 |
| 适用场景 | 库本身没有 CMake 支持 | 库已原生支持 CMake |
| 版本检查 | 有限支持 | 原生支持 |
| 优先级 | 默认优先尝试 | 指定 CONFIG 或 NO_MODULE 时使用 |
CMake 会先尝试 Module 模式,如果找不到对应的 Find 模块,再尝试 Config 模式。你也可以用 MODULE 强制只用 Module 模式,或用 CONFIG 强制只用 Config 模式。
我记得有一次,项目里依赖了一个很老的库,它没有提供 Config 文件。我只好自己写了个 Find 模块。嗯,那时候我才真正体会到 Module 模式的价值。
12.3 Find 模块的工作原理
Find 模块本质上就是一个 CMake 脚本文件,命名规则是 Find<PackageName>.cmake。CMake 会在 CMAKE_MODULE_PATH 指定的路径下搜索这些文件。
一个典型的 Find 模块会做这几件事:
- 设置搜索路径(系统路径、用户指定路径、环境变量等)
- 查找头文件(用
find_path) - 查找库文件(用
find_library) - 检查版本(如果有要求)
- 设置结果变量(如
<Package>_FOUND、<Package>_INCLUDE_DIRS、<Package>_LIBRARIES) - 调用
find_package_handle_standard_args提供标准输出
举个例子,CMake 自带的 FindZLIB.cmake 就是很好的学习范本。它会设置 ZLIB_INCLUDE_DIRS 和 ZLIB_LIBRARIES,用起来非常方便。
12.4 Config 模式详解
Config 模式是更现代的方式。库的作者在安装时,会生成一个 <Package>Config.cmake 文件,里面包含了库的路径、版本、依赖关系等信息。
Config 文件通常由 install(EXPORT) 或 configure_package_config_file() 生成。它最大的好处是:库自己知道怎么被使用。
比如,你用 find_package(OpenCV REQUIRED),OpenCV 的 Config 文件会自动设置好所有 include 路径和库列表,甚至包括依赖的第三方库。你只需要写一行:
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})
我个人非常推荐库的开发者使用 Config 模式。它让使用者的体验好太多了。你想想看,如果每个库都要用户自己去配路径,那得多痛苦。
<Package>_DIR 变量指向 Config 文件所在目录。
12.5 pkg-config 集成
有些库既没有 Find 模块,也没有 Config 文件,但它们提供了 .pc 文件(pkg-config 格式)。CMake 可以通过 pkg_check_modules 来利用这些信息。
首先,你需要找到 pkg-config 工具:
find_package(PkgConfig REQUIRED)
然后,用它查找库:
pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libavutil)
这会设置 FFMPEG_INCLUDE_DIRS、FFMPEG_LIBRARY_DIRS、FFMPEG_LIBRARIES 等变量。之后你就可以像使用普通 CMake 目标一样使用它们了。
我曾经在一个嵌入式项目里用过这个技巧。那个平台的 SDK 只提供了 .pc 文件,没有 CMake 支持。用 pkg-config 集成,几分钟就搞定了。
12.6 编写自己的 Find 模块
当官方没有提供 Find 模块,库本身也没有 Config 文件时,你就得自己动手了。别怕,其实套路很固定。
下面是一个完整的 Find 模块示例,假设我们要找的是一个叫 Foo 的库:
# FindFoo.cmake
# 查找 Foo 库的头文件和库文件
# 先检查是否已经找到
if(Foo_FOUND)
return()
endif()
# 查找头文件
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
VERSION_VAR Foo_VERSION
)
# 如果找到,设置标准变量
if(Foo_FOUND)
set(Foo_INCLUDE_DIRS ${Foo_INCLUDE_DIR})
set(Foo_LIBRARIES ${Foo_LIBRARY})
# 创建导入目标(推荐做法)
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()
# 标记为高级变量
mark_as_advanced(Foo_INCLUDE_DIR Foo_LIBRARY)
这里有几个要点:
- 使用导入目标:我强烈建议创建
Foo::Foo这样的导入目标。这样使用者可以用target_link_libraries直接链接,CMake 会自动处理 include 路径。 - 调用标准处理函数:
find_package_handle_standard_args会统一处理FOUND变量、版本检查、错误信息等。 - 标记高级变量:用
mark_as_advanced隐藏内部变量,让 CMake GUI 界面更清爽。
把写好的 FindFoo.cmake 放到项目的 cmake/ 目录下,然后在主 CMakeLists.txt 里添加:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(Foo REQUIRED)
搞定。
12.7 知识体系总览
下面这张图总结了本章的核心逻辑,帮你理清思路:
从图中可以看到,CMake 会先尝试 Module 模式,如果找不到对应的 Find 模块,再降级到 Config 模式。两种模式都失败时,如果加了 REQUIRED 就会报错终止。
12.8 避坑指南
最后,分享几个我踩过的坑:
- 路径问题:我曾经把 Find 模块放错了目录,结果 CMake 一直找不到。记得检查
CMAKE_MODULE_PATH是否设置正确。 - 变量命名:Find 模块里的变量名必须严格遵循
<Package>_<VARIABLE>的格式,否则find_package_handle_standard_args会认不出来。 - 缓存变量:
find_path和find_library的结果会被缓存。如果你改了搜索路径,记得清理 CMake 缓存再重新配置。 - 版本比较:Config 模式支持精确的版本比较,但 Module 模式不一定。如果你的项目对版本有严格要求,优先用 Config 模式。
嗯,关于查找包的内容就这些了。掌握了 find_package 的两种模式和编写 Find 模块的技巧,你就能轻松应对各种第三方库的集成需求了。