CMake 秘籍(二)(5)

本文涉及的产品
文档翻译,文档翻译 1千页
语种识别,语种识别 100万字符
图片翻译,图片翻译 100张
简介: CMake 秘籍(二)

CMake 秘籍(二)(4)https://developer.aliyun.com/article/1525091

如何操作

Boost 包含许多不同的库,这些库几乎可以独立使用。在内部,CMake 将这个库集合表示为组件集合。FindBoost.cmake模块不仅可以搜索整个库集合的安装,还可以搜索集合中特定组件及其依赖项(如果有的话)。我们将逐步构建相应的CMakeLists.txt

  1. 我们首先声明了最低 CMake 版本、项目名称、语言,并强制使用 C++11 标准:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-08 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 然后,我们使用find_package来搜索 Boost。对 Boost 的依赖是强制性的,因此使用了REQUIRED参数。由于在本例中我们只需要文件系统组件,因此我们在COMPONENTS关键字后传递该组件作为参数给find_package
find_package(Boost 1.54 REQUIRED COMPONENTS filesystem)
  1. 我们添加了一个可执行目标,用于编译示例源文件:
add_executable(path-info path-info.cpp)
  1. 最后,我们将目标链接到 Boost 库组件。由于依赖关系被声明为PUBLIC,依赖于我们目标的其他目标将自动获取该依赖关系:
target_link_libraries(path-info
  PUBLIC
    Boost::filesystem
  )

工作原理

FindBoost.cmake模块,在本例中使用,将尝试在标准系统安装目录中定位 Boost 库。由于我们链接到导入的Boost::filesystem目标,CMake 将自动设置包含目录并调整编译和链接标志。如果 Boost 库安装在非标准位置,可以在配置时使用BOOST_ROOT变量传递 Boost 安装的根目录,以指示 CMake 也在非标准路径中搜索:

$ cmake -D BOOST_ROOT=/custom/boost/

或者,可以同时传递BOOST_INCLUDEDIRBOOST_LIBRARYDIR变量,以指定包含头文件和库的目录:

$ cmake -D BOOST_INCLUDEDIR=/custom/boost/include -D BOOST_LIBRARYDIR=/custom/boost/lib

检测外部库:I. 使用 pkg-config

本例的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-09找到,并包含一个 C 语言示例。本例适用于 CMake 3.6(及以上)版本,并在 GNU/Linux、macOS 和 Windows(使用 MSYS Makefiles)上进行了测试。在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-09,我们还提供了一个与 CMake 3.5 兼容的示例。

到目前为止,我们已经讨论了两种检测外部依赖的方法:

  • 使用 CMake 附带的 find-modules。这通常是可靠且经过良好测试的。然而,并非所有包在 CMake 的官方发布版中都有一个 find-module。
  • 使用包供应商提供的Config.cmakeConfigVersion.cmakeTargets.cmake文件,这些文件与包本身一起安装在标准位置。

如果某个依赖项既不提供 find-module 也不提供 vendor-packaged CMake 文件,我们该怎么办?在这种情况下,我们有两个选择:

  • 依赖pkg-config实用程序来发现系统上的包。这依赖于包供应商在.pc配置文件中分发有关其包的元数据。
  • 为依赖项编写我们自己的 find-package 模块。

在本食谱中,我们将展示如何从 CMake 内部利用pkg-config来定位 ZeroMQ 消息库。下一个食谱,检测外部库:II. 编写 find-module,将展示如何为 ZeroMQ 编写自己的基本 find-module。

准备工作

我们将构建的代码是 ZeroMQ 手册中的一个示例,网址为zguide.zeromq.org/page:all。它由两个源文件hwserver.chwclient.c组成,将构建为两个单独的可执行文件。执行时,它们将打印熟悉的“Hello, World”消息。

如何操作

这是一个 C 项目,我们将使用 C99 标准。我们将逐步构建CMakeLists.txt文件:

  1. 我们声明一个 C 项目并强制执行 C99 标准:
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
project(recipe-09 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_C_STANDARD_REQUIRED ON)
  1. 我们查找pkg-config,使用 CMake 附带的 find-module。注意传递给find_packageQUIET参数。只有当所需的pkg-config未找到时,CMake 才会打印消息:
find_package(PkgConfig REQUIRED QUIET)
  1. 当找到pkg-config时,我们将能够访问pkg_search_module函数来搜索任何带有包配置.pc文件的库或程序。在我们的例子中,我们查找 ZeroMQ 库:
pkg_search_module(
  ZeroMQ
  REQUIRED
    libzeromq libzmq lib0mq
  IMPORTED_TARGET
  )
  1. 如果找到 ZeroMQ 库,将打印状态消息:
if(TARGET PkgConfig::ZeroMQ)
  message(STATUS "Found ZeroMQ")
endif()
  1. 然后我们可以添加两个可执行目标,并与 ZeroMQ 的IMPORTED目标链接。这将自动设置包含目录和链接库:
add_executable(hwserver hwserver.c)
target_link_libraries(hwserver PkgConfig::ZeroMQ)
add_executable(hwclient hwclient.c)
target_link_libraries(hwclient PkgConfig::ZeroMQ)
  1. 现在,我们可以配置并构建示例:
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
  1. 在一个终端中启动服务器,它将响应类似于以下示例的消息:
Current 0MQ version is 4.2.2
  1. 然后,在另一个终端启动客户端,它将打印以下内容:
Connecting to hello world server…
Sending Hello 0…
Received World 0
Sending Hello 1…
Received World 1
Sending Hello 2…
...

工作原理

一旦找到pkg-config,CMake 将提供两个函数来封装这个程序提供的功能:

  • pkg_check_modules,用于在传递的列表中查找所有模块(库和/或程序)
  • pkg_search_module,用于在传递的列表中查找第一个可用的模块

这些函数接受REQUIREDQUIET参数,就像find_package一样。更详细地说,我们对pkg_search_module的调用如下:

pkg_search_module(
  ZeroMQ
  REQUIRED
    libzeromq libzmq lib0mq
  IMPORTED_TARGET
  )

这里,第一个参数是用于命名存储 ZeroMQ 库搜索结果的目标的前缀:PkgConfig::ZeroMQ。注意,我们需要为系统上的库名称传递不同的选项:libzeromqlibzmqlib0mq。这是因为不同的操作系统和包管理器可能会为同一个包选择不同的名称。

pkg_check_modulespkg_search_module函数在 CMake 3.6 中获得了IMPORTED_TARGET选项和定义导入目标的功能。在此之前的 CMake 版本中,只会为稍后使用定义变量ZeroMQ_INCLUDE_DIRS(包含目录)和ZeroMQ_LIBRARIES(链接库)。

检测外部库:II. 编写查找模块

本配方的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-03/recipe-10获取,并包含一个 C 示例。本配方适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

本配方补充了之前的配方,检测外部库:I. 使用 pkg-config。我们将展示如何编写一个基本的查找模块来定位系统上的 ZeroMQ 消息库,以便在非 Unix 操作系统上进行库检测。我们将重用相同的服务器-客户端示例代码。

如何操作

这是一个 C 项目,我们将使用 C99 标准。我们将逐步构建CMakeLists.txt文件:

  1. 我们声明一个 C 项目并强制执行 C99 标准:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-10 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_C_STANDARD_REQUIRED ON)
  1. 我们将当前源目录,CMAKE_CURRENT_SOURCE_DIR,添加到 CMake 查找模块的路径列表中,CMAKE_MODULE_PATH。这是我们自己的FindZeroMQ.cmake模块所在的位置:
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
  1. 我们稍后将讨论FindZeroMQ.cmake,但现在FindZeroMQ.cmake模块可用,我们搜索库。这是我们项目的必要依赖项。由于我们没有使用find_packageQUIET选项,当找到库时,将自动打印状态消息:
find_package(ZeroMQ REQUIRED)
  1. 我们继续添加hwserver可执行目标。使用ZeroMQ_INCLUDE_DIRSZeroMQ_LIBRARIES变量指定包含目录和链接库,这些变量由成功的find_package命令设置:
add_executable(hwserver hwserver.c)
target_include_directories(hwserver
  PRIVATE
    ${ZeroMQ_INCLUDE_DIRS}
  )
target_link_libraries(hwserver
  PRIVATE
    ${ZeroMQ_LIBRARIES}
  )
  1. 最后,我们对hwclient可执行目标也做同样的事情:
add_executable(hwclient hwclient.c)
target_include_directories(hwclient
  PRIVATE
    ${ZeroMQ_INCLUDE_DIRS}
  )
target_link_libraries(hwclient
  PRIVATE
    ${ZeroMQ_LIBRARIES}
  )

本配方的主要CMakeLists.txt与之前配方中使用的不同之处在于使用了FindZeroMQ.cmake模块。该模块使用find_pathfind_libraryCMake 内置命令搜索 ZeroMQ 头文件和库,并使用find_package_handle_standard_args设置相关变量,正如我们在配方 3 中所做的,检测 Python 模块和包

  1. FindZeroMQ.cmake中,我们首先检查用户是否设置了ZeroMQ_ROOT CMake 变量。此变量可用于指导检测 ZeroMQ 库到非标准安装目录。用户可能已经将ZeroMQ_ROOT设置为环境变量,我们也检查了这一点:
if(NOT ZeroMQ_ROOT)
  set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}")
endif()
  1. 然后,我们在系统上搜索zmq.h头文件的位置。这是基于_ZeroMQ_ROOT变量,并使用 CMake 的find_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)
  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_package_handle_standard_args命令准备ZeroMQ_VERSION变量:
if(ZeroMQ_FIND_VERSION_COUNT GREATER 2)
  set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}.${ZeroMQ_VERSION_PATCH}")
else()
  set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}")
endif()
  1. 我们使用find_library命令来搜索ZeroMQ库。在这里,我们需要在 Unix 基础和 Windows 平台之间做出区分,因为库的命名约定不同:
if(NOT ${CMAKE_C_PLATFORM_ID} STREQUAL "Windows")
  find_library(ZeroMQ_LIBRARIES 
      NAMES 
        zmq 
      HINTS 
        ${_ZeroMQ_ROOT}/lib
        ${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
      )
else()
  find_library(ZeroMQ_LIBRARIES
      NAMES
        libzmq
        "libzmq-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
        "libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
        libzmq_d
        "libzmq-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
        "libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
      HINTS
        ${_ZeroMQ_ROOT}/lib
      )
endif()
  1. 最后,我们包含标准的FindPackageHandleStandardArgs.cmake模块并调用相应的 CMake 命令。如果找到所有必需的变量并且版本匹配,则将ZeroMQ_FOUND变量设置为TRUE
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ZeroMQ
  FOUND_VAR
    ZeroMQ_FOUND
  REQUIRED_VARS
ZeroMQ_INCLUDE_DIRS
    ZeroMQ_LIBRARIES
  VERSION_VAR
    ZeroMQ_VERSION
  )

我们刚才描述的FindZeroMQ.cmake模块是从github.com/zeromq/azmq/blob/master/config/FindZeroMQ.cmake改编而来的。

它是如何工作的

查找模块通常遵循特定的模式:

  1. 检查用户是否为所需包提供了自定义位置。
  2. 使用find_家族的命令来搜索所需包的已知必需组件,即头文件、库、可执行文件等。我们已经使用find_path来找到头文件的完整路径,并使用find_library来找到一个库。CMake 还提供了find_filefind_programfind_package。这些命令具有以下一般签名:
find_path(<VAR> NAMES name PATHS paths)
  1. 在这里,将持有搜索的结果,如果成功,或者-NOTFOUND如果失败。NAMESPATHS是 CMake 应该查找的文件的名称和搜索应该指向的路径,分别。
  2. 从这次初步搜索的结果中,提取版本号。在我们的例子中,ZeroMQ 头文件包含库版本,可以使用字符串操作和正则表达式提取。
  3. 最后,调用find_package_handle_standard_args命令。这将处理find_package命令的标准REQUIREDQUIET和版本参数,此外还设置ZeroMQ_FOUND变量。

任何 CMake 命令的完整文档都可以从命令行获取。例如,cmake --help-command find_file 将输出 find_file 命令的手册页。对于 CMake 标准模块的手册页,使用 --help-module CLI 开关。例如,cmake --help-module FindPackageHandleStandardArgs 将屏幕输出 FindPackageHandleStandardArgs.cmake 模块的手册页。

还有更多

总结一下,在发现软件包时,有四种可用的路线:

  1. 使用软件包供应商提供的 CMake 文件 packageConfig.cmakepackageConfigVersion.cmakepackageTargets.cmake,并将其安装在与软件包本身一起的标准位置。
  2. 使用所需的软件包的 find-module,无论是由 CMake 还是第三方提供的。
  3. 采用本食谱中所示的 pkg-config 方法。
  4. 如果这些都不适用,编写自己的 find-module。

四种替代路线已经按相关性排名,但每种方法都有其挑战。

并非所有软件包供应商都提供 CMake 发现文件,但这变得越来越普遍。这是因为导出 CMake 目标使得第三方代码消费库和/或程序所依赖的额外依赖项变得非常容易。

Find-modules 自 CMake 诞生之初就是依赖定位的工作马。然而,它们中的大多数仍然依赖于设置由依赖方消费的变量,例如 Boost_INCLUDE_DIRSPYTHON_INTERPRETER 等。这种方法使得为第三方重新分发自己的软件包并确保依赖项得到一致满足变得困难。

使用 pkg-config 的方法可以很好地工作,因为它已经成为基于 Unix 的系统的事实标准。因此,它不是一个完全跨平台的方法。此外,正如 CMake 文档所述,在某些情况下,用户可能会意外地覆盖软件包检测,导致 pkg-config 提供错误的信息。

最后的选择是编写自己的 find-module CMake 脚本,正如我们在本食谱中所做的那样。这是可行的,并且依赖于我们简要讨论过的 FindPackageHandleStandardArgs.cmake 模块。然而,编写一个完全全面的 find-module 远非易事;有许多难以发现的边缘情况,我们在寻找 Unix 和 Windows 平台上的 ZeroMQ 库文件时展示了这样一个例子。

这些关注点和困难对于所有软件开发者来说都非常熟悉,这一点在 CMake 邮件列表上的热烈讨论中得到了证明:cmake.org/pipermail/cmake/2018-May/067556.htmlpkg-config在 Unix 软件包开发者中被广泛接受,但它不容易移植到非 Unix 平台。CMake 配置文件功能强大,但并非所有软件开发者都熟悉 CMake 语法。Common Package Specification 项目是一个非常新的尝试,旨在统一pkg-config和 CMake 配置文件的软件包发现方法。您可以在项目网站上找到更多信息:mwoehlke.github.io/cps/

在第十章《编写安装程序》中,我们将讨论如何通过使用前述讨论中概述的第一条路径,即在项目旁边提供自己的 CMake 发现文件,使您自己的软件包对第三方应用程序可发现。

相关文章
|
2月前
|
编译器 Shell 开发工具
CMake 秘籍(八)(5)
CMake 秘籍(八)
21 2
|
2月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
18 1
|
2月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
21 1
|
2月前
|
Linux 编译器 C++
CMake 秘籍(七)(2)
CMake 秘籍(七)
26 1
|
2月前
|
编译器 Linux C++
CMake 秘籍(六)(5)
CMake 秘籍(六)
19 1
|
2月前
|
测试技术 C++
CMake 秘籍(四)(5)
CMake 秘籍(四)
15 0
|
2月前
|
并行计算 编译器 Linux
CMake 秘籍(二)(3)
CMake 秘籍(二)
15 0
|
2月前
|
Linux C++ iOS开发
CMake 秘籍(七)(1)
CMake 秘籍(七)
17 0
|
2月前
|
Linux 测试技术 iOS开发
CMake 秘籍(四)(4)
CMake 秘籍(四)
16 0
|
2月前
|
编译器 Linux C++
CMake 秘籍(二)(1)
CMake 秘籍(二)
20 0

相关实验场景

更多