CMake Error at CMakeLists.txt (find_package)幕后真凶

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
个人主页: rainInSunny  |  个人专栏: C++那些事儿Learn OpenGL In Qt

写在前面

  本文从CMake中find_package()报错入手,首先给出了如何解决这类报错,然后深入探讨了find_package()的用法,揭示了find_package()背后帮我们做的事情,阐明了find_package()Module模式和Config模式的区别,同时针对在这两种模式下如何为自己写的三方库适配find_package()给出了详细说明。

find_package报错解决

  find_package报错描述的是依赖的三库文件没有找到,要解决这个问题也很简单,告诉find_package()要找的三方库在哪就行。先分析出错的原因:

  1. 可能是没有安装find_package寻找的三方库。
  2. 安装了三方库,但是是没设置路径或者路径设置错误。

  基本就是上面两种原因,那破案就简单了。针对第一种情况没什么好说的,先安装三方库再说。针对第二种情况,由于find_package支持Module模式和Config模式,下面举例分析。

Module模式

  以FontConfig为例,编译安装好后目录结构如下。一般在share/cmake目录能否找到提供find_package()支持的构建配置文件,Find<package>.cmake,这里名称为FindFontConfig.cmake。如果报错找的三方库安装目录有这样名称的文件,那么只需要将这个文件的路径,例子中也就是FindFontConfig.cmake路径添加到CMAKE_MODULE_PATH中,就像这样set(CMAKE_MODULE_PATH path/to/fontconfig;${CMAKE_MODULE_PATH}")

在这里插入图片描述

在这里插入图片描述

Config模式

  以Qt5为例,官方的安装目录结构如下。一般在share/cmake目录能否找到提供find_package()支持的构建配置文件,<package>Config.cmake,这里名称为Qt5Config.cmake,注意也可能通过-连接,即<package>-config.cmake。如果找到三方库中有这里文件,那么只需要将该路径添设置到package_DIR变量中,就像这样set(Qt5_DIR path/to/Qt5)

在这里插入图片描述

在这里插入图片描述

  核心思路就是通过三方库提供的配置文件名称来确定是Module模式还是Config模式,然后添加到CMake对应的预设变量中,帮助find_package找到三方库的构建配置即可。如果Module模式和Config模式同时存在,CMake会优先使用Module模式。

find_package()用法

  上面提到了find_package()有Module和Config两种模式,虽然问题解决了,但是如果自己开发的三方库需要给使用者提供这样便捷的能力,就需要更加理解find_package(),下面就仔细聊聊find_package(),其实本质目的就是帮助工程找到依赖三方库的头文件、动态库/静态库文件,因为在编译链接过程中需要这些文件。

Module模式

  假设你写出了一个震惊世界的消息库ZeroMQ,显然你希望大家能够很容易的在CMake中使用,就像这样find_package(ZeroMQ REQUIRED),为此你必须提供一个名称为Find<package>.cmake的文件来支持find_package(),这里文件的名称为FindZeroMQ.cmake。在其它三方库安装目录看到类似Find<package>.cmake,可以知道这个三方库支持find_package()的Module模式。接下来让我们一步步准备好FindZeroMQ.cmake。

  1. 首先FindZeroMQ.cmake中需要检查ZeroMQ_ROOT变量是否设置。此变量可用于ZeroMQ库的检测,并引导到自定义安装目录。用户可能设置了ZeroMQ_ROOT作为环境变量,类似<package>_ROOT一般用于指向package的安装路径,该路径下包含了三方库的头文件、库文件等。
if(NOT ZeroMQ_ROOT)
    set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}") //如果用户没有定义ZeroMQ_ROOT,则设置为环境变量中的ZeroMQ_ROOT
endif()
  1. 一般来说,当依赖一个三方库时最好显示指定三方库安装路径。如果ZeroMQ_ROOT有定义,则赋值给_ZeroMQ_ROOT。如果上面既没有定义ZeroMQ_ROOT,又没有在环境变量中设置ZeroMQ_ROOT的值,则尝试通过find_path去找三方库的典型头文件,并指定这个路径为_ZeroMQ_ROOT的值。_ZeroMQ_ROOT是最后实际查找ZeroMQ库的路径。通常会将三方库的路径添加到在${CMAKE_MODULE_PATH}中
if(NOT ZeroMQ_ROOT)
    find_path(_ZeroMQ_ROOT NAMES include/zmq.h)
else()
    set(_ZeroMQ_ROOT "${ZeroMQ_ROOT}")
endif()

find_path(ZeroMQ_INCLUDE_DIRS NAMES zmq.h HINTS ${_ZeroMQ_ROOT}/include) //ZeroMQ_INCLUDE_DIRS设置为头文件路径
  1. 如果成功找到头文件,则将ZeroMQ_INCLUDE_DIRS设置为其位置。然后继续通过使用字符串操作和正则表达式,寻找相应版本的ZeroMQ库,如果没有多版本区分的需求,不需要这个步骤,后续说明将省略版本号区分。
set(_ZeroMQ_H ${ZeroMQ_INCLUDE_DIRS}/zmq.h)

function(_zmqver_EXTRACT _ZeroMQ_VER_COMPONENT _ZeroMQ_VER_OUTPUT)
set(CMAKE_MATCH_1 "0")
set(_ZeroMQ_expr "^[ \\t]*#define[ \\t]+${_ZeroMQ_VER_COMPONENT}[ \\t]+([0-9]+)$")
file(STRINGS "${_ZeroMQ_H}" _ZeroMQ_ver REGEX "${_ZeroMQ_expr}")
string(REGEX MATCH "${_ZeroMQ_expr}" ZeroMQ_ver "${_ZeroMQ_ver}")
set(${_ZeroMQ_VER_OUTPUT} "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()

_zmqver_EXTRACT("ZMQ_VERSION_MAJOR" ZeroMQ_VERSION_MAJOR)
_zmqver_EXTRACT("ZMQ_VERSION_MINOR" ZeroMQ_VERSION_MINOR)
_zmqver_EXTRACT("ZMQ_VERSION_PATCH" ZeroMQ_VERSION_PATCH)
  1. 接着使用find_library命令搜索ZeroMQ库。因为库的命名有所不同,这里我们需要区分Unix的平台和Windows平台。
if(NOT ${CMAKE_C_PLATFORM_ID} STREQUAL "Windows")
    find_library(ZeroMQ_LIBRARIES
        NAMES
            libzmq
        HINTS
            ${_ZeroMQ_ROOT}/lib
            ${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
        )
else()
    find_library(ZeroMQ_LIBRARIES
        NAMES
            zmq
        HINTS
            ${_ZeroMQ_ROOT}/lib
        )
endif()

//如果需要区分release和debug
if(NOT ${CMAKE_C_PLATFORM_ID} STREQUAL "Windows")
    find_library(ZeroMQ_LIBRARY_RELEASE
        NAMES
            libzmq
        HINTS
            ${_ZeroMQ_ROOT}/lib
            ${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
        )
    find_library(ZeroMQ_LIBRARY_DEBUG
        NAMES
            libzmqd
        HINTS
            ${_ZeroMQ_ROOT}/lib
            ${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
        )
else()
    find_library(ZeroMQ_LIBRARY_RELEASE
        NAMES
            zmq
        HINTS
            ${_ZeroMQ_ROOT}/lib
        )
    find_library(ZeroMQ_LIBRARY_DEBUG
        NAMES
            zmqd
        HINTS
            ${_ZeroMQ_ROOT}/lib
        )
endif()
  1. 最后包含了标准FindPackageHandleStandardArgs.cmake,并调用相应的CMake命令。如果找到所有需要的变量,这里是ZeroMQ_INCLUDE_DIRSZeroMQ_LIBRARIES,则将ZeroMQ_FOUND变量设置为TRUE。完成这些步骤后,用户就能通过find_package(ZeroMQ REQUIRED)来使用ZeroMQ库,通过ZeroMQ_FOUND判断find_package是否成功,如果成功${ZeroMQ_INCLUDE_DIRS}${ZeroMQ_LIBRARIES}变量中就保存了ZeroMQ库的头文件路径和库文件路径供用户在target_include_directories和target_link_libraries中使用。
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ZeroMQ
    FOUND_VAR
        ZeroMQ_FOUND
    REQUIRED_VARS
        ZeroMQ_INCLUDE_DIRS
        ZeroMQ_LIBRARIES
    )

//如果需要区分release和debug
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ZeroMQ
    FOUND_VAR
        ZeroMQ_FOUND
    REQUIRED_VARS
        ZeroMQ_INCLUDE_DIRS
        ZeroMQ_LIBRARY_RELEASE
        ZeroMQ_LIBRARY_DEBUG
    )

Config模式

  Config模式相对于Module模式更为复杂些,需要配合目标安装过程,如果使用的三方库是二进制文件并不是源码编译的,建议通过Module模式为三方库提供find_package支持。这里假设我们通过自己编写的源码编译出了message这样一个target,我们需要通过Config模式为message提供find_package支持。接下来一步步实现这个目标

  1. 在安装目标过程中添加EXPORT关键字,这样CMake将为目标生成一个导出的目标文件。这里的${INSTALL_LIBDIR}和${INSTALL_BINDIR}分别为库文件和可执行文件安装目录。
install(
    TARGETS
        message
    EXPORT
        messageTargets
    ARCHIVE
        DESTINATION ${INSTALL_LIBDIR}
        COMPONENT lib
    RUNTIME
        DESTINATION ${INSTALL_BINDIR}
        COMPONENT bin
    LIBRARY
        DESTINATION ${INSTALL_LIBDIR}
        COMPONENT lib
    PUBLIC_HEADER
        DESTINATION ${INSTALL_INCLUDEDIR}/message
        COMPONENT dev
    )
  1. 自动生成的导出目标文件称为messageTargets.cmake,需要显式地指定它的安装规则。这里的${INSTALL_CMAKEDIR}是CMake配置文件路径,一般设置为库安装路径下的share目录。
install(
    EXPORT
        messageTargets
    NAMESPACE
        "message::"
    DESTINATION
        ${INSTALL_CMAKEDIR}
    COMPONENT
        dev
    )
  1. 需要生成正确的CMake配置文件,这些将确保下游项目能够找到消息库导出的目标。为此需要包括CMakePackageConfigHelpers.cmake标准模块,然后用模块提供的函数生成版本配置文件messageConfigVersion.cmake和配置文件messageConfig.cmake。注意这里的messageConfig.cmake是CMake通过模版文件messageConfig.cmake.in生成的,这个模版文件需要在message库的源码中提供,这里目录为${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in,模版的内容很简单,如下。
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
    )

configure_package_config_file(
    ${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
    INSTALL_DESTINATION ${INSTALL_CMAKEDIR}
    )
// messageConfig.cmake.in
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/messageTargets.cmake")
  1. 最后安装配置文件。完成后用户在CMake中设定<package>_DIR变量指向这两个文件的安装路径,这里是message_DIR,就能通过find_package(message REQUIRED)引用message库了。
install(
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
    DESTINATION
        ${INSTALL_CMAKEDIR}
    )

欢迎留言讨论,创作不易,感谢点赞、关注和收藏

  • 22
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值