CMake 秘籍(二)(4)https://developer.aliyun.com/article/1525091
如何操作
Boost 包含许多不同的库,这些库几乎可以独立使用。在内部,CMake 将这个库集合表示为组件集合。FindBoost.cmake
模块不仅可以搜索整个库集合的安装,还可以搜索集合中特定组件及其依赖项(如果有的话)。我们将逐步构建相应的CMakeLists.txt
:
- 我们首先声明了最低 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)
- 然后,我们使用
find_package
来搜索 Boost。对 Boost 的依赖是强制性的,因此使用了REQUIRED
参数。由于在本例中我们只需要文件系统组件,因此我们在COMPONENTS
关键字后传递该组件作为参数给find_package
:
find_package(Boost 1.54 REQUIRED COMPONENTS filesystem)
- 我们添加了一个可执行目标,用于编译示例源文件:
add_executable(path-info path-info.cpp)
- 最后,我们将目标链接到 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_INCLUDEDIR
和BOOST_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.cmake
、ConfigVersion.cmake
和Targets.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.c
和hwclient.c
组成,将构建为两个单独的可执行文件。执行时,它们将打印熟悉的“Hello, World”消息。
如何操作
这是一个 C 项目,我们将使用 C99 标准。我们将逐步构建CMakeLists.txt
文件:
- 我们声明一个 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)
- 我们查找
pkg-config
,使用 CMake 附带的 find-module。注意传递给find_package
的QUIET
参数。只有当所需的pkg-config
未找到时,CMake 才会打印消息:
find_package(PkgConfig REQUIRED QUIET)
- 当找到
pkg-config
时,我们将能够访问pkg_search_module
函数来搜索任何带有包配置.pc
文件的库或程序。在我们的例子中,我们查找 ZeroMQ 库:
pkg_search_module( ZeroMQ REQUIRED libzeromq libzmq lib0mq IMPORTED_TARGET )
- 如果找到 ZeroMQ 库,将打印状态消息:
if(TARGET PkgConfig::ZeroMQ) message(STATUS "Found ZeroMQ") endif()
- 然后我们可以添加两个可执行目标,并与 ZeroMQ 的
IMPORTED
目标链接。这将自动设置包含目录和链接库:
add_executable(hwserver hwserver.c) target_link_libraries(hwserver PkgConfig::ZeroMQ) add_executable(hwclient hwclient.c) target_link_libraries(hwclient PkgConfig::ZeroMQ)
- 现在,我们可以配置并构建示例:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build .
- 在一个终端中启动服务器,它将响应类似于以下示例的消息:
Current 0MQ version is 4.2.2
- 然后,在另一个终端启动客户端,它将打印以下内容:
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
,用于在传递的列表中查找第一个可用的模块
这些函数接受REQUIRED
和QUIET
参数,就像find_package
一样。更详细地说,我们对pkg_search_module
的调用如下:
pkg_search_module( ZeroMQ REQUIRED libzeromq libzmq lib0mq IMPORTED_TARGET )
这里,第一个参数是用于命名存储 ZeroMQ 库搜索结果的目标的前缀:PkgConfig::ZeroMQ
。注意,我们需要为系统上的库名称传递不同的选项:libzeromq
、libzmq
和lib0mq
。这是因为不同的操作系统和包管理器可能会为同一个包选择不同的名称。
pkg_check_modules
和pkg_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
文件:
- 我们声明一个 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)
- 我们将当前源目录,
CMAKE_CURRENT_SOURCE_DIR
,添加到 CMake 查找模块的路径列表中,CMAKE_MODULE_PATH
。这是我们自己的FindZeroMQ.cmake
模块所在的位置:
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
- 我们稍后将讨论
FindZeroMQ.cmake
,但现在FindZeroMQ.cmake
模块可用,我们搜索库。这是我们项目的必要依赖项。由于我们没有使用find_package
的QUIET
选项,当找到库时,将自动打印状态消息:
find_package(ZeroMQ REQUIRED)
- 我们继续添加
hwserver
可执行目标。使用ZeroMQ_INCLUDE_DIRS
和ZeroMQ_LIBRARIES
变量指定包含目录和链接库,这些变量由成功的find_package
命令设置:
add_executable(hwserver hwserver.c) target_include_directories(hwserver PRIVATE ${ZeroMQ_INCLUDE_DIRS} ) target_link_libraries(hwserver PRIVATE ${ZeroMQ_LIBRARIES} )
- 最后,我们对
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_path
和find_library
CMake 内置命令搜索 ZeroMQ 头文件和库,并使用find_package_handle_standard_args
设置相关变量,正如我们在配方 3 中所做的,检测 Python 模块和包。
- 在
FindZeroMQ.cmake
中,我们首先检查用户是否设置了ZeroMQ_ROOT
CMake 变量。此变量可用于指导检测 ZeroMQ 库到非标准安装目录。用户可能已经将ZeroMQ_ROOT
设置为环境变量,我们也检查了这一点:
if(NOT ZeroMQ_ROOT) set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}") endif()
- 然后,我们在系统上搜索
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)
- 如果成功找到头文件,则将
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)
- 然后,我们为
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()
- 我们使用
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()
- 最后,我们包含标准的
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
改编而来的。
它是如何工作的
查找模块通常遵循特定的模式:
- 检查用户是否为所需包提供了自定义位置。
- 使用
find_
家族的命令来搜索所需包的已知必需组件,即头文件、库、可执行文件等。我们已经使用find_path
来找到头文件的完整路径,并使用find_library
来找到一个库。CMake 还提供了find_file
、find_program
和find_package
。这些命令具有以下一般签名:
find_path(<VAR> NAMES name PATHS paths)
- 在这里,
将持有搜索的结果,如果成功,或者
-NOTFOUND
如果失败。NAMES
和PATHS
是 CMake 应该查找的文件的名称和搜索应该指向的路径,分别。 - 从这次初步搜索的结果中,提取版本号。在我们的例子中,ZeroMQ 头文件包含库版本,可以使用字符串操作和正则表达式提取。
- 最后,调用
find_package_handle_standard_args
命令。这将处理find_package
命令的标准REQUIRED
、QUIET
和版本参数,此外还设置ZeroMQ_FOUND
变量。
任何 CMake 命令的完整文档都可以从命令行获取。例如,cmake --help-command find_file
将输出 find_file
命令的手册页。对于 CMake 标准模块的手册页,使用 --help-module
CLI 开关。例如,cmake --help-module FindPackageHandleStandardArgs
将屏幕输出 FindPackageHandleStandardArgs.cmake
模块的手册页。
还有更多
总结一下,在发现软件包时,有四种可用的路线:
- 使用软件包供应商提供的 CMake 文件
packageConfig.cmake
、packageConfigVersion.cmake
和packageTargets.cmake
,并将其安装在与软件包本身一起的标准位置。 - 使用所需的软件包的 find-module,无论是由 CMake 还是第三方提供的。
- 采用本食谱中所示的
pkg-config
方法。 - 如果这些都不适用,编写自己的 find-module。
四种替代路线已经按相关性排名,但每种方法都有其挑战。
并非所有软件包供应商都提供 CMake 发现文件,但这变得越来越普遍。这是因为导出 CMake 目标使得第三方代码消费库和/或程序所依赖的额外依赖项变得非常容易。
Find-modules 自 CMake 诞生之初就是依赖定位的工作马。然而,它们中的大多数仍然依赖于设置由依赖方消费的变量,例如 Boost_INCLUDE_DIRS
、PYTHON_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.html
。pkg-config
在 Unix 软件包开发者中被广泛接受,但它不容易移植到非 Unix 平台。CMake 配置文件功能强大,但并非所有软件开发者都熟悉 CMake 语法。Common Package Specification 项目是一个非常新的尝试,旨在统一pkg-config
和 CMake 配置文件的软件包发现方法。您可以在项目网站上找到更多信息:mwoehlke.github.io/cps/
在第十章《编写安装程序》中,我们将讨论如何通过使用前述讨论中概述的第一条路径,即在项目旁边提供自己的 CMake 发现文件,使您自己的软件包对第三方应用程序可发现。