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模式

这两种模式的区别,我画了张图帮你理解:

find_package 两种模式对比 Module 模式 查找路径: CMAKE_MODULE_PATH 目录 CMake 内置模块目录 查找目标: FindXXX.cmake 脚本 典型场景: 系统级库、老旧库 没有提供 .cmake 配置 Config 模式 查找路径: _DIR 变量 系统注册表 / 默认安装路径 查找目标: XXXConfig.cmake 或 xxx-config.cmake 典型场景: 现代库(OpenCV、PCL、VTK) 库自己导出的配置

说白了,Module模式是CMake自己想办法帮你找库,它运行一个叫FindXXX.cmake的脚本,脚本里写死了怎么找头文件、库文件。Config模式则是库自己告诉你它在哪——库安装时会生成一个XXXConfig.cmake文件,里面包含了所有必要的信息。

CMake的查找顺序是这样的:先尝试Config模式,如果找不到对应的XXXConfig.cmake,再回退到Module模式找FindXXX.cmake。当然,你可以用MODULECONFIG关键字强制指定模式。

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 常用的前缀路径列表
系统环境变量 比如 PATHLD_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