9、CMake查找依赖:find_package基础、Module模式与Config模式、FindXXX.cmake脚本
依赖管理,说白了就是让CMake知道你的项目需要哪些第三方库,以及这些库的头文件和库文件到底在哪。我刚开始用CMake那会儿,最头疼的就是这步——明明库装好了,CMake就是找不到,气得想砸键盘。
后来我才明白,find_package这玩意儿其实就两种工作模式:Module模式和Config模式。搞懂它俩的区别,依赖查找就通了七七八八。
9.1 find_package的基本用法
先看最基础的调用方式:
find_package(OpenCV REQUIRED)
find_package(Boost COMPONENTS filesystem system REQUIRED)
find_package(ZLIB QUIET)
这里有几个关键参数:
- REQUIRED:找不到就报错,编译直接停
- QUIET:找不到也别嚷嚷,安静点
- COMPONENTS:指定需要库的哪些组件
- MODULE:强制使用Module模式
- CONFIG:强制使用Config模式
我个人习惯,核心依赖一律加REQUIRED。为什么?因为依赖缺失时,早点报错比编译到一半再崩要好得多。我在项目中遇到过好几次,有人忘了加REQUIRED,结果CMake配置成功,编译到链接阶段才报找不到符号,排查起来特别痛苦。
9.2 Module模式 vs Config模式
这两种模式的区别,我画了张图帮你理解:
说白了,Module模式是CMake自己想办法帮你找库,它运行一个叫FindXXX.cmake的脚本,脚本里写死了怎么找头文件、库文件。Config模式则是库自己告诉你它在哪——库安装时会生成一个XXXConfig.cmake文件,里面包含了所有必要的信息。
CMake的查找顺序是这样的:先尝试Config模式,如果找不到对应的XXXConfig.cmake,再回退到Module模式找FindXXX.cmake。当然,你可以用MODULE或CONFIG关键字强制指定模式。
9.3 FindXXX.cmake 脚本长什么样?
你想想看,如果某个库比较老,没有提供Config文件,那我们就得自己写FindXXX.cmake。我给大家看一个我实际项目中用过的简化版:
# FindSQLite3.cmake - 查找 SQLite3 库
# 用法: find_package(SQLite3 REQUIRED)
# 查找头文件
find_path(SQLITE3_INCLUDE_DIR
NAMES sqlite3.h
PATHS /usr/include /usr/local/include
)
# 查找库文件
find_library(SQLITE3_LIBRARY
NAMES sqlite3 libsqlite3
PATHS /usr/lib /usr/local/lib
)
# 处理找到/没找到的情况
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(SQLite3
REQUIRED_VARS SQLITE3_LIBRARY SQLITE3_INCLUDE_DIR
)
# 设置导出变量
if(SQLITE3_FOUND)
set(SQLITE3_INCLUDE_DIRS ${SQLITE3_INCLUDE_DIR})
set(SQLITE3_LIBRARIES ${SQLITE3_LIBRARY})
# 如果支持,创建导入目标
if(NOT TARGET SQLite3::SQLite3)
add_library(SQLite3::SQLite3 UNKNOWN IMPORTED)
set_target_properties(SQLite3::SQLite3 PROPERTIES
IMPORTED_LOCATION "${SQLITE3_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${SQLITE3_INCLUDE_DIR}"
)
endif()
endif()
# 标记变量为高级,在CMake GUI中默认隐藏
mark_as_advanced(SQLITE3_INCLUDE_DIR SQLITE3_LIBRARY)
核心要点:
find_path找头文件目录,find_library找库文件find_package_handle_standard_args是标准的结果处理宏- 最好创建 IMPORTED 目标,这样使用者可以用
target_link_libraries直接链接 - 变量名建议用
包名_大写的格式,比如SQLITE3_LIBRARY
9.4 Config模式的优势
说实话,我现在更推荐用Config模式。为什么?因为库自己导出的配置信息最准确。比如OpenCV,它的OpenCVConfig.cmake里包含了:
- 所有模块的库文件路径
- 编译选项(比如是否用了C++11)
- 依赖的其他库信息
- Debug/Release不同版本的库
这些信息靠FindXXX.cmake脚本去猜,很容易出错。我曾经维护过一个项目,FindBoost.cmake脚本因为Boost版本升级,找库的逻辑全乱了,折腾了两天才修好。如果是Config模式,库自己更新配置,根本不用我们操心。
9.5 实战:如何让我们的库支持Config模式
如果你在开发一个库,想让别人用find_package方便地找到,那就在CMakeLists.txt里加上导出逻辑:
# 安装头文件
install(DIRECTORY include/ DESTINATION include)
# 安装库文件
install(TARGETS mylib EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
# 导出目标
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
# 生成并安装配置文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
INSTALL_DESTINATION lib/cmake/MyLib
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
DESTINATION lib/cmake/MyLib
)
对应的Config.cmake.in模板文件:
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
check_required_components(MyLib)
小技巧:用NAMESPACE MyLib::可以创建带命名空间的导入目标。这样使用者写target_link_libraries(myapp MyLib::MyLib),代码更清晰,也不容易跟其他库重名。
9.6 查找路径的优先级
CMake找依赖时,路径优先级是这样的:
| 优先级 | 路径来源 | 说明 |
|---|---|---|
| 最高 | <PackageName>_DIR |
用户手动指定的路径,最优先 |
| 高 | CMAKE_PREFIX_PATH |
常用的前缀路径列表 |
| 中 | 系统环境变量 | 比如 PATH、LD_LIBRARY_PATH |
| 低 | 默认系统路径 | /usr/lib、/usr/local/lib 等 |
注意:如果你在系统里装了多个版本的同一个库,CMake可能会找到不是你想要的版本。我曾经就踩过这个坑——系统自带的OpenCV 3.2和手动编译的OpenCV 4.5混在一起,CMake死活找到旧版本。解决办法是用OpenCV_DIR变量指定路径:
cmake .. -DOpenCV_DIR=/home/user/opencv-4.5/lib/cmake/opencv4
9.7 调试技巧
如果find_package找不到库,别慌。试试这几个调试方法:
- 加
--debug-find参数:cmake --debug-find ..,CMake会打印详细的查找过程 - 检查
CMAKE_FIND_DEBUG_MODE变量,设为ON也能开启调试 - 手动设置
<PackageName>_DIR,跳过自动查找 - 查看CMake的Find模块文档:
cmake --help-module FindXXX
嗯,这里要注意一点:调试信息虽然多,但别被吓到。你只需要关注最后几行,看它到底在哪些路径下找了,以及为什么没找到。
好了,关于find_package的基础知识就聊到这儿。记住一句话:能用Config模式就别用Module模式,能创建导入目标就别只用变量。这是我在无数次踩坑后总结出来的经验。
公众号:蓝海资料掘金营,微信deep3321