11、CMake中的查找依赖:find_package()详解、Find模块与Config模块、find_library()与find_file()

依赖管理,说白了就是让CMake帮你找到别人写好的代码。我刚开始用CMake那会儿,最头疼的就是这步。明明库装好了,CMake就是找不到,气得我差点想手写Makefile。后来摸透了里面的门道,才发现其实就那么回事。

今天咱们就把CMake查找依赖的几把刷子,一个一个捋清楚。

11.1 find_package():查找依赖的入口

find_package()是CMake里最常用的查找命令。它的工作模式有两种:模块模式配置模式

基本语法长这样:

find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [NO_POLICY_SCOPE])

几个关键参数我解释一下:

  • REQUIRED:找不到就报错,直接终止构建。我一般都会加上,免得后面编译时出现一堆莫名其妙的错误。
  • QUIET:找不到也别嚷嚷,安静点。调试时建议先不加,看看CMake到底在找什么。
  • COMPONENTS:指定需要哪些组件。比如找Qt,你可能只需要QtWidgets和QtCore。
  • EXACT:版本必须精确匹配,不能向下兼容。

举个例子:

find_package(OpenCV REQUIRED COMPONENTS core imgproc highgui)
if(OpenCV_FOUND)
    include_directories(${OpenCV_INCLUDE_DIRS})
    target_link_libraries(myapp ${OpenCV_LIBS})
endif()

这里有个坑——大小写敏感。OpenCV写成opencv,有时候能行,有时候不行。我个人习惯严格按照官方文档来,包名首字母大写。

11.2 模块模式 vs 配置模式

这两种模式的区别,我画了张图,一看就明白:

find_package() 两种工作模式 find_package() 模块模式 (Module Mode) 配置模式 (Config Mode) 查找 Find<Package>.cmake 搜索路径:CMAKE_MODULE_PATH 通常由CMake官方或第三方提供 示例:FindBoost.cmake 查找 <Package>Config.cmake 搜索路径:CMAKE_PREFIX_PATH 由库的安装包自带 示例:OpenCVConfig.cmake 设置变量供项目使用 默认先尝试模块模式,找不到再尝试配置模式(除非指定 MODULE 关键字)

模块模式,就是找Find<Package>.cmake文件。这些文件通常由CMake官方提供,或者第三方库自己写的。比如FindBoost.cmakeFindPython3.cmake

配置模式,是找库自己安装时生成的<Package>Config.cmake<lower-case-package>-config.cmake。比如OpenCV安装后会生成OpenCVConfig.cmake

CMake默认先试模块模式,找不到再试配置模式。你可以用MODULE关键字强制只用模块模式。

我的经验:能用配置模式就用配置模式。因为库自己生成的Config文件,信息更准确、更完整。模块模式有时候版本判断会出问题,我踩过好几次坑。

11.3 Find模块 vs Config模块

咱们来对比一下这两种模块:

对比项 Find模块 Config模块
文件名 Find<Package>.cmake <Package>Config.cmake
提供者 CMake官方或第三方 库的开发者
搜索路径 CMAKE_MODULE_PATH CMAKE_PREFIX_PATH
可靠性 一般,可能版本滞后 高,与库版本同步
自定义程度 高,可以自己写 低,由库决定

我曾经接手过一个老项目,里面用的还是Find模块找OpenCV。结果OpenCV升级到4.x后,FindOpenCV.cmake里的一些变量名变了,编译直接挂掉。后来改成配置模式,问题就解决了。

如果你自己写了一个库,想让别人方便地用find_package()找到,那就得生成Config文件。CMake提供了install(EXPORT)configure_package_config_file()来帮忙。

# 生成导出目标
install(TARGETS mylib EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)

# 安装导出文件
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)

# 生成Config文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/MyLibConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    INSTALL_DESTINATION lib/cmake/MyLib
    PATH_VARS INCLUDE_INSTALL_DIR
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION lib/cmake/MyLib
)
注意:Config文件里的路径必须是安装后的路径,不是构建目录的路径。我见过有人直接把构建目录的绝对路径写进去,结果换台机器就废了。

11.4 find_library():精确查找库文件

有时候find_package()搞不定,或者你只想找一个单独的库文件,那就用find_library()

find_library(<VAR> name1 [path1 path2 ...])

它会搜索系统路径和用户指定的路径,找到后把完整路径存到变量里。

find_library(MY_LIB NAMES mylib mylib_d
    PATHS /usr/local/lib /opt/lib
    PATH_SUFFIXES lib64 lib
)

if(MY_LIB)
    message(STATUS "Found mylib: ${MY_LIB}")
    target_link_libraries(myapp ${MY_LIB})
else()
    message(FATAL_ERROR "mylib not found!")
endif()

几个要点:

  • NAMES:可以指定多个名字,CMake会依次尝试。比如debug版和release版名字不同。
  • PATHS:指定搜索路径。我一般把常用的第三方库路径都列上。
  • PATH_SUFFIXES:在搜索路径下再追加子目录。比如有些库把64位版本放在lib64下。
  • NO_DEFAULT_PATH:不要搜索系统默认路径。慎用,除非你知道自己在干什么。
避坑指南:我曾经在Windows上找库,明明路径对了就是找不到。后来发现是库的位数不匹配——64位的CMake找不到32位的库。所以记得检查CMAKE_SIZEOF_VOID_P

11.5 find_file():查找任意文件

find_file()find_library()用法几乎一样,只不过它找的是任意文件,不限于库文件。

find_file(<VAR> name1 [path1 path2 ...])

我常用它来找配置文件、头文件、或者资源文件。

find_file(CONFIG_FILE config.ini
    PATHS ${CMAKE_SOURCE_DIR}/config
          /etc/myapp
          $ENV{HOME}/.myapp
)

if(CONFIG_FILE)
    message(STATUS "Config file: ${CONFIG_FILE}")
    # 把配置文件安装到目标位置
    install(FILES ${CONFIG_FILE} DESTINATION etc)
endif()

这里有个小技巧:用$ENV{}可以读取环境变量。比如$ENV{HOME}就是用户主目录。

11.6 实战:组合使用

实际项目中,我经常把find_package()find_library()组合使用。比如一个库既有Config文件,又有一些额外的依赖库:

# 先尝试用find_package找主库
find_package(MyLib QUIET)

if(MyLib_FOUND)
    message(STATUS "Found MyLib via config mode")
    target_link_libraries(myapp MyLib::MyLib)
else()
    # 找不到就手动找
    find_library(MYLIB_LIB mylib
        PATHS /usr/local/lib /opt/mylib/lib
    )
    find_path(MYLIB_INCLUDE mylib.h
        PATHS /usr/local/include /opt/mylib/include
    )

    if(MYLIB_LIB AND MYLIB_INCLUDE)
        message(STATUS "Found MyLib manually")
        target_include_directories(myapp PRIVATE ${MYLIB_INCLUDE})
        target_link_libraries(myapp ${MYLIB_LIB})
    else()
        message(FATAL_ERROR "MyLib not found!")
    endif()
endif()

这样写的好处是:优先用配置模式,干净利落;不行再手动找,兜底方案。嗯,我在好几个跨平台项目里都是这么干的,效果不错。

11.7 调试技巧

找依赖出了问题,别慌。我一般按这个顺序排查:

  1. 加--debug-find参数cmake --debug-find ..,CMake会打印详细的查找过程。
  2. 检查CMAKE_PREFIX_PATH:看看这个变量有没有设置对。很多新手都栽在这。
  3. 手动搜索:用find / -name "*Config.cmake" 2>/dev/null看看文件到底在哪。
  4. 检查版本:有时候找到了,但版本不匹配。用find_packageEXACT参数测试一下。
我的习惯:在CMakeLists.txt开头加一行message(STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}"),一眼就能看到搜索路径对不对。

好了,关于CMake查找依赖的这些东西,咱们就聊到这儿。记住一句话:先找Config,再找Find,最后手动找。这个顺序能解决90%的问题。

剩下的10%,嗯,那就得具体情况具体分析了。不过有了今天这些知识,你应该能应付大部分场景了。


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