17、查找包:find_package()命令,Config模式与Module模式,编写FindXXX.cmake模块
好,咱们今天聊一个实战性很强的话题——find_package()。说白了,就是让CMake帮你找到别人写好的库。比如你想用OpenCV、Boost、Eigen这些第三方库,总不能每次都手动去翻路径吧?find_package就是干这个的。
我个人习惯,写一个新项目的第一步,就是先把依赖的包用find_package找好。省得后面编译报一堆“找不到头文件”的错,心态容易崩。
17.1 find_package() 的基本用法
先看一个最简单的例子:
find_package(OpenCV REQUIRED)
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})
target_include_directories(my_app PRIVATE ${OpenCV_INCLUDE_DIRS})
这里REQUIRED的意思是——找不到这个包,就直接报错退出。如果你觉得这个包可有可无,可以不加REQUIRED,CMake会继续往下走,但你要自己处理变量为空的情况。
嗯,这里要注意:find_package找到包之后,会设置一堆变量。比如OpenCV会设置OpenCV_INCLUDE_DIRS、OpenCV_LIBS、OpenCV_VERSION等等。不同包提供的变量名不太一样,你得去看文档或者查FindXXX.cmake里的注释。
17.2 Config模式 vs Module模式
find_package有两种工作模式:Config模式和Module模式。我刚开始学的时候,经常搞混这两个东西。其实区别很简单:
- Module模式:CMake自带一个FindXXX.cmake脚本,或者你自己写了一个。它通过搜索路径找到这个脚本,然后执行它来定位库。
- Config模式:库本身安装的时候,会生成一个XXXConfig.cmake文件。CMake直接找这个文件,里面已经写好了所有路径和变量。
说白了,Module模式是“我去帮你找”,Config模式是“你告诉我你在哪”。
CMake会先尝试Module模式,如果找不到对应的FindXXX.cmake,再切换到Config模式。你也可以强制指定模式:
# 强制使用Module模式
find_package(XXX MODULE)
# 强制使用Config模式
find_package(XXX CONFIG)
核心区别总结:
| 特性 | Module模式 | Config模式 |
|---|---|---|
| 脚本来源 | CMake自带或用户提供 | 库安装时自带 |
| 文件名 | FindXXX.cmake | XXXConfig.cmake 或 xxx-config.cmake |
| 适用场景 | 老旧库、没有提供Config的库 | 现代库、自己写的库 |
| 灵活性 | 高,可以自定义搜索逻辑 | 低,依赖库的安装结构 |
17.3 编写FindXXX.cmake模块
好,重点来了。如果你用的库比较老,或者它压根没提供Config文件,你就得自己写一个FindXXX.cmake。我在项目中遇到过好几次这种情况——比如一个内部的老库,文档都丢了,只能自己写查找脚本。
一个典型的FindXXX.cmake长这样:
# FindXXX.cmake - 查找XXX库的CMake模块
# 先检查用户有没有手动指定路径
if(NOT XXX_ROOT)
set(XXX_ROOT "" CACHE PATH "XXX库的根目录")
endif()
# 查找头文件
find_path(XXX_INCLUDE_DIR
NAMES xxx.h
PATHS ${XXX_ROOT}/include
/usr/include
/usr/local/include
DOC "XXX库的头文件路径"
)
# 查找库文件
find_library(XXX_LIBRARY
NAMES xxx libxxx
PATHS ${XXX_ROOT}/lib
/usr/lib
/usr/local/lib
DOC "XXX库的库文件路径"
)
# 处理找到的结果
if(XXX_INCLUDE_DIR AND XXX_LIBRARY)
set(XXX_FOUND TRUE)
set(XXX_INCLUDE_DIRS ${XXX_INCLUDE_DIR})
set(XXX_LIBRARIES ${XXX_LIBRARY})
# 可选:检查版本
if(EXISTS "${XXX_INCLUDE_DIR}/xxx_version.h")
file(STRINGS "${XXX_INCLUDE_DIR}/xxx_version.h" _version_str
REGEX "#define XXX_VERSION")
string(REGEX REPLACE ".*XXX_VERSION \"([0-9.]+)\".*" "\\1"
XXX_VERSION "${_version_str}")
endif()
# 打印友好信息
message(STATUS "Found XXX: ${XXX_LIBRARY} (version: ${XXX_VERSION})")
else()
set(XXX_FOUND FALSE)
if(XXX_FIND_REQUIRED)
message(FATAL_ERROR "Could not find XXX library!")
endif()
endif()
# 导出变量供上层使用
mark_as_advanced(XXX_INCLUDE_DIR XXX_LIBRARY)
小技巧:写FindXXX.cmake的时候,记得用find_path和find_library而不是硬编码路径。这样用户可以通过设置XXX_ROOT或者CMAKE_PREFIX_PATH来覆盖查找路径。我曾经见过有人直接写死路径,结果换台机器就编译不过,太痛苦了。
17.4 知识体系流程图
下面这张图帮你理清find_package的整个流程:
17.5 避坑指南
我曾经踩过的坑:
- 路径优先级问题:find_package会按顺序搜索多个路径。如果你系统里装了多个版本的同一个库,它可能找到不是你想要的版本。我建议在调用find_package之前,先设置
CMAKE_PREFIX_PATH或者XXX_ROOT来锁定路径。 - 大小写敏感:find_package的参数是大小写敏感的。比如
find_package(OpenCV)和find_package(opencv)可能找到不同的东西。我习惯保持和官方文档一致的大小写。 - REQUIRED的副作用:加了REQUIRED之后,如果找不到包,CMake会直接报错退出。这在CI环境里是好事,但在本地调试时可能有点烦。我有时候会先不加REQUIRED,看看它到底找到了什么路径。
17.6 实战建议
写FindXXX.cmake的时候,我一般遵循这几个原则:
- 先查环境变量:让用户可以通过
XXX_ROOT或XXX_DIR指定路径。 - 提供版本检测:如果库有版本头文件,尽量提取版本号。这样用户可以用
find_package(XXX 2.0 EXACT)来精确匹配。 - 导出标准变量:按照CMake惯例,设置
XXX_INCLUDE_DIRS、XXX_LIBRARIES、XXX_FOUND这些变量。 - 打印友好信息:用
message(STATUS)输出找到的路径,方便调试。
嗯,其实写FindXXX.cmake没那么玄乎。你只要记住:它本质上就是一个CMake脚本,用find_path和find_library去搜文件,然后把结果存到变量里。剩下的就是一些边界处理和错误检查。
好了,这一章的内容就到这。你只要把find_package的两种模式搞清楚,再动手写一个FindXXX.cmake练练手,以后遇到任何第三方库都不慌了。