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_DIRSOpenCV_LIBSOpenCV_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.cmakeXXXConfig.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_pathfind_library而不是硬编码路径。这样用户可以通过设置XXX_ROOT或者CMAKE_PREFIX_PATH来覆盖查找路径。我曾经见过有人直接写死路径,结果换台机器就编译不过,太痛苦了。

17.4 知识体系流程图

下面这张图帮你理清find_package的整个流程:

find_package() 工作流程 find_package(XXX) 有FindXXX.cmake? (Module模式) 执行FindXXX.cmake 尝试Config模式 设置 XXX_FOUND = TRUE 查找XXXConfig.cmake 使用包:target_link_libraries

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的时候,我一般遵循这几个原则:

  1. 先查环境变量:让用户可以通过XXX_ROOTXXX_DIR指定路径。
  2. 提供版本检测:如果库有版本头文件,尽量提取版本号。这样用户可以用find_package(XXX 2.0 EXACT)来精确匹配。
  3. 导出标准变量:按照CMake惯例,设置XXX_INCLUDE_DIRSXXX_LIBRARIESXXX_FOUND这些变量。
  4. 打印友好信息:用message(STATUS)输出找到的路径,方便调试。

嗯,其实写FindXXX.cmake没那么玄乎。你只要记住:它本质上就是一个CMake脚本,用find_pathfind_library去搜文件,然后把结果存到变量里。剩下的就是一些边界处理和错误检查。

好了,这一章的内容就到这。你只要把find_package的两种模式搞清楚,再动手写一个FindXXX.cmake练练手,以后遇到任何第三方库都不慌了。


公众号:蓝海资料掘金营,微信deep3321