一、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 会按这个顺序找:
<PackageName>_DIR变量指定的路径- CMAKE_PREFIX_PATH 里的路径
- 系统默认路径(如 /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 模式。你可以用 CONFIG 或 MODULE 关键字强制指定模式。
四、两种模式的对比
| 对比项 | 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 的整个流程串起来了。你看一遍应该就能记住:
七、避坑指南
最后分享几个我实际项目中踩过的坑:
- 路径大小写敏感:Linux 上
FindOpenGL.cmake和find_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