CMake 秘籍(三)(5)

简介: CMake 秘籍(三)

CMake 秘籍(三)(4)https://developer.aliyun.com/article/1524626

在构建时为特定目标运行自定义命令

本配方的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-05/recipe-05找到,并包含一个 Fortran 示例。该配方适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows(使用 MSYS Makefiles)上进行了测试。

本配方将展示如何使用add_custom_command的第二个签名来执行无输出的自定义操作。这对于在特定目标构建或链接之前或之后执行某些操作非常有用。由于自定义命令仅在目标本身需要构建时执行,我们实现了对它们执行的目标级控制。我们将通过一个示例来演示这一点,在该示例中,我们在目标构建之前打印其链接行,然后在编译后的可执行文件之后测量其静态大小分配。

准备工作

在本配方中,我们将使用以下示例 Fortran 代码(example.f90):

program example
  implicit none
  real(8) :: array(20000000)
  real(8) :: r
  integer :: i
  do i = 1, size(array)
    call random_number(r)
    array(i) = r
  end do
  print *, sum(array)
end program

这段代码是 Fortran 的事实对后续讨论影响不大,但我们选择 Fortran 是因为那里有很多遗留的 Fortran 代码,其中静态大小分配是一个问题。

在这段代码中,我们定义了一个包含 20,000,000 个双精度浮点的数组,我们期望这个数组占用 160MB 内存。我们在这里所做的并不是推荐的编程实践,因为在一般情况下,无论代码中是否使用,都会消耗内存。更好的方法是在需要时动态分配数组,并在使用后立即释放。

示例代码用随机数填充数组并计算它们的总和 - 这是为了确保数组确实被使用,编译器不会优化分配。我们将使用一个 Python 脚本(static-size.py)来测量示例二进制文件的静态分配大小,该脚本围绕 size 命令:

import subprocess
import sys
# for simplicity we do not check number of
# arguments and whether the file really exists
file_path = sys.argv[-1]
try:
    output = subprocess.check_output(['size', file_path]).decode('utf-8')
except FileNotFoundError:
    print('command "size" is not available on this platform')
    sys.exit(0)
size = 0.0
for line in output.split('\n'):
    if file_path in line:
        # we are interested in the 4th number on this line
        size = int(line.split()[3])
print('{0:.3f} MB'.format(size/1.0e6))

为了打印链接行,我们将使用第二个 Python 辅助脚本(echo-file.py)来打印文件内容:

import sys
# for simplicity we do not verify the number and
# type of arguments
file_path = sys.argv[-1]
try:
    with open(file_path, 'r') as f:
        print(f.read())
except FileNotFoundError:
    print('ERROR: file {0} not found'.format(file_path))

如何实现

让我们看一下我们的 CMakeLists.txt

  1. 我们首先声明一个 Fortran 项目:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-05 LANGUAGES Fortran)
  1. 这个例子依赖于 Python 解释器,以便我们可以以可移植的方式执行辅助脚本:
find_package(PythonInterp REQUIRED)
  1. 在这个例子中,我们默认使用 "Release" 构建类型,以便 CMake 添加优化标志,以便我们稍后有东西可以打印:
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
  1. 现在,我们定义可执行目标:
add_executable(example "")
target_sources(example
  PRIVATE
    example.f90
  )
  1. 然后,我们定义一个自定义命令,在链接 example 目标之前打印链接行:
add_custom_command(
  TARGET
    example
  PRE_LINK
COMMAND
    ${PYTHON_EXECUTABLE}
      ${CMAKE_CURRENT_SOURCE_DIR}/echo-file.py
      ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/example.dir/link.txt
  COMMENT
    "link line:"
  VERBATIM
  )
  1. 最后,我们定义一个自定义命令,在成功构建后打印可执行文件的静态大小:
add_custom_command(
  TARGET
    example
  POST_BUILD
  COMMAND
    ${PYTHON_EXECUTABLE}
      ${CMAKE_CURRENT_SOURCE_DIR}/static-size.py
      $<TARGET_FILE:example>
  COMMENT
    "static size of executable:"
  VERBATIM
  )
  1. 让我们来测试一下。观察打印出的链接行和可执行文件的静态大小:
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
Scanning dependencies of target example
[ 50%] Building Fortran object CMakeFiles/example.dir/example.f90.o
[100%] Linking Fortran executable example
link line:
/usr/bin/f95 -O3 -DNDEBUG -O3 CMakeFiles/example.dir/example.f90.o -o example 
static size of executable:
160.003 MB
[100%] Built target example

工作原理

一旦声明了库或可执行目标,就可以通过使用 add_custom_command 将附加命令附加到目标上。正如我们所见,这些命令将在特定时间执行,与它们所附加的目标的执行上下文相关。CMake 理解以下选项,用于自定义命令的执行顺序:

  • PRE_BUILD:用于在执行与目标相关的任何其他规则之前执行的命令。但是,这只支持 Visual Studio 7 或更高版本。
  • PRE_LINK:使用此选项,命令将在目标编译后但在链接器或归档器调用之前执行。使用 PRE_BUILD 与 Visual Studio 7 或更高版本以外的生成器将被解释为 PRE_LINK
  • POST_BUILD:如前所述,命令将在执行给定目标的所有规则之后运行。

在这个例子中,我们向可执行目标添加了两个自定义命令。PRE_LINK 命令将屏幕上打印出 ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/example.dir/link.txt 的内容。该文件包含链接命令,在我们的例子中,链接行结果如下:

link line:
/usr/bin/f95 -O3 -DNDEBUG -O3 CMakeFiles/example.dir/example.f90.o -o example

我们为此使用了一个 Python 包装器,以不依赖于可能不具有可移植性的 shell 命令。

在第二步中,POST_BUILD自定义命令调用了 Python 辅助脚本static-size.py,其参数为生成器表达式$。CMake 将在生成时间,即构建系统生成时,将生成器表达式扩展为目标文件路径。Python 脚本static-size.py反过来使用size命令来获取可执行文件的静态分配大小,将其转换为 MB,并打印结果。在我们的例子中,我们得到了预期的 160 MB:

static size of executable:
160.003 MB

探究编译和链接

本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-05/recipe-06找到,并提供了一个 C++示例。该食谱适用于 CMake 版本 3.9(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。代码仓库还包含了一个与 CMake 3.5 兼容的示例。

在构建系统生成过程中最常见的操作之一是评估我们试图在哪种系统上构建项目。这意味着尝试找出哪些功能有效,哪些无效,并相应地调整项目的编译,无论是通过发出依赖项未满足的信号,还是在我们的代码库中启用适当的变通方法。接下来的几个食谱将展示如何使用 CMake 执行这些操作。特别是,我们将考虑以下内容:

  1. 如何确保特定的代码片段能够成功编译成可执行文件。
  2. 如何确保编译器理解所需的标志。
  3. 如何确保特定的代码片段能够成功编译成运行的可执行文件

准备就绪

本食谱将展示如何使用相应的CheckSourceCompiles.cmake标准模块中的check__source_compiles函数,以评估给定的编译器是否能够将预定义的代码片段编译成可执行文件。该命令可以帮助您确定:

  • 您的编译器支持所需的功能。
  • 链接器工作正常并理解特定的标志。
  • 使用find_package找到的包含目录和库是可用的。

在本食谱中,我们将展示如何检测 OpenMP 4.5 标准中的任务循环功能,以便在 C++可执行文件中使用。我们将使用一个示例 C++源文件来探测编译器是否支持这样的功能。CMake 提供了一个额外的命令try_compile来探测编译。本食谱将展示如何使用这两种方法。

您可以使用 CMake 命令行界面来获取特定模块(cmake --help-module )和命令(cmake --help-command )的文档。在我们的例子中,cmake --help-module CheckCXXSourceCompiles将输出check_cxx_source_compiles函数的文档到屏幕,而cmake --help-command try_compile将做同样的事情,为try_compile命令。

如何操作

我们将同时使用try_compilecheck_cxx_source_compiles,并比较这两个命令的工作方式:

  1. 我们首先创建一个 C++11 项目:
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(recipe-06 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 我们找到编译器的 OpenMP 支持:
find_package(OpenMP)
if(OpenMP_FOUND)
  # ... <- the steps below will be placed here
else()
  message(STATUS "OpenMP not found: no test for taskloop is run")
endif()
  1. 如果找到了 OpenMP,我们继续前进并探测所需功能是否可用。为此,我们设置一个临时目录。这将由try_compile用于生成其中间文件。我们将这个放在前一步引入的 if 子句中:
set(_scratch_dir ${CMAKE_CURRENT_BINARY_DIR}/omp_try_compile)
  1. 我们调用try_compile来生成一个小项目,尝试编译源文件taskloop.cpp。成功或失败将被保存到omp_taskloop_test_1变量中。我们需要为这个小样本编译设置适当的编译器标志、包含目录和链接库。由于我们使用的是导入的目标 OpenMP::OpenMP_CXX,这只需通过设置LINK_LIBRARIES选项为try_compile来简单完成。如果编译成功,那么任务循环功能是可用的,我们向用户打印一条消息:
try_compile(
  omp_taskloop_test_1
  ${_scratch_dir}
  SOURCES
    ${CMAKE_CURRENT_SOURCE_DIR}/taskloop.cpp
  LINK_LIBRARIES
    OpenMP::OpenMP_CXX
  ) 
message(STATUS "Result of try_compile: ${omp_taskloop_test_1}")
  1. 为了使用check_cxx_source_compiles函数,我们需要包含CheckCXXSourceCompiles.cmake模块文件。这是随 CMake 一起分发的,与 C(CheckCSourceCompiles.cmake)和 Fortran(CheckFortranSourceCompiles.cmake)的类似文件一起:
include(CheckCXXSourceCompiles)
  1. 我们通过使用file(READ ...)命令读取其内容,将我们尝试编译和链接的源文件的内容复制到 CMake 变量中:
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/taskloop.cpp _snippet)
  1. 我们设置CMAKE_REQUIRED_LIBRARIES。这是为了在下一步中正确调用编译器所必需的。注意使用了导入的 OpenMP::OpenMP_CXX目标,这将同时设置适当的编译器标志和包含目录:
set(CMAKE_REQUIRED_LIBRARIES OpenMP::OpenMP_CXX)
  1. 我们调用check_cxx_source_compiles函数并传入我们的代码片段。检查的结果将被保存到omp_taskloop_test_2变量中:
check_cxx_source_compiles("${_snippet}" omp_taskloop_test_2)
  1. 在调用check_cxx_source_compiles之前,我们取消设置之前定义的变量,并向用户打印一条消息:
unset(CMAKE_REQUIRED_LIBRARIES)
message(STATUS "Result of check_cxx_source_compiles: ${omp_taskloop_test_2}"
  1. 最后,我们测试这个配方:
$ mkdir -p build
$ cd build
$ cmake ..
-- ...
-- Found OpenMP_CXX: -fopenmp (found version "4.5") 
-- Found OpenMP: TRUE (found version "4.5") 
-- Result of try_compile: TRUE
-- Performing Test omp_taskloop_test_2
-- Performing Test omp_taskloop_test_2 - Success
-- Result of check_cxx_source_compiles: 1

工作原理

try_compilecheck_cxx_source_compiles都将编译并链接一个源文件到一个可执行文件。如果这些操作成功,那么输出变量,对于前者是omp_task_loop_test_1,对于后者是omp_task_loop_test_2,将被设置为TRUE。这两个命令完成任务的方式略有不同,然而。check__source_compiles系列命令是try_compile命令的一个简化包装。因此,它提供了一个最小化的接口:

  1. 要编译的代码片段必须作为 CMake 变量传递。大多数情况下,这意味着必须使用 file(READ ...) 读取文件,正如我们在示例中所做的那样。然后,该片段将保存到构建目录的 CMakeFiles/CMakeTmp 子目录中的文件中。
  2. 通过在调用函数之前设置以下 CMake 变量来微调编译和链接:
  • CMAKE_REQUIRED_FLAGS 用于设置编译器标志
  • CMAKE_REQUIRED_DEFINITIONS 用于设置预处理器宏
  • CMAKE_REQUIRED_INCLUDES 用于设置包含目录列表
  • CMAKE_REQUIRED_LIBRARIES 用于设置链接到可执行文件的库列表
  1. 在调用 check__compiles_function 后,必须手动取消设置这些变量,以确保同一变量的后续使用不会包含虚假内容。

在 CMake 3.9 中引入了 OpenMP 导入目标,但当前的方案也可以通过手动设置所需的标志和库,使其与早期版本的 CMake 兼容,方法如下:set(CMAKE_REQUIRED_FLAGS ${OpenMP_CXX_FLAGS})set(CMAKE_REQUIRED_LIBRARIES ${OpenMP_CXX_LIBRARIES})

对于 Fortran,CMake 假定样本片段采用固定格式,但这并不总是正确的。为了克服假阴性,需要为 check_fortran_source_compiles 设置 -ffree-form 编译器标志。这可以通过 set(CMAKE_REQUIRED_FLAGS "-ffree-form") 实现。

这种最小接口反映了测试编译是通过在 CMake 调用中直接生成和执行构建和链接命令来进行的。

try_compile 命令提供了更完整的接口和两种不同的操作模式:

  1. 第一种方式接受一个完整的 CMake 项目作为输入,并根据其 CMakeLists.txt 配置、构建和链接它。这种操作模式提供了更多的灵活性,因为要编译的项目可以任意复杂。
  2. 第二种方式,我们使用的方式,提供了一个源文件以及用于包含目录、链接库和编译器标志的配置选项。

try_compile 因此基于调用 CMake 的项目,要么是已经存在 CMakeLists.txt 的项目(在第一种操作模式下),要么是根据传递给 try_compile 的参数动态生成的项目。

还有更多

本方案中概述的检查类型并不总是万无一失的,可能会产生假阳性和假阴性。例如,你可以尝试注释掉包含 CMAKE_REQUIRED_LIBRARIES 的行,示例仍将报告“成功”。这是因为编译器将忽略 OpenMP 指令。

当你怀疑返回了错误的结果时,应该怎么办?CMakeOutput.logCMakeError.log文件位于构建目录的CMakeFiles子目录中,它们提供了出错线索。它们报告了 CMake 运行的操作的标准输出和标准错误。如果你怀疑有误报,应该检查前者,通过搜索设置为保存编译检查结果的变量。如果你怀疑有漏报,应该检查后者。

调试try_compile需要小心。CMake 会删除该命令生成的所有文件,即使检查不成功。幸运的是,--debug-trycompile将阻止 CMake 进行清理。如果你的代码中有多个try_compile调用,你将只能一次调试一个:

  1. 运行一次 CMake,不带--debug-trycompile。所有try_compile命令都将运行,并且它们的执行目录和文件将被清理。
  2. 从 CMake 缓存中删除保存检查结果的变量。缓存保存在CMakeCache.txt文件中。要清除变量的内容,可以使用-UCLI 开关,后跟变量的名称,该名称将被解释为全局表达式,因此可以使用*?
$ cmake -U <variable-name>
  1. 再次运行 CMake,使用--debug-trycompile选项。只有清除缓存的检查会被重新运行。这次执行目录和文件不会被清理。

try_compile提供了更多的灵活性和更清晰的接口,特别是当要编译的代码不是简短的代码片段时。我们建议在需要测试编译的代码是简短、自包含且不需要广泛配置的情况下,使用check__source_compiles。在所有其他情况下,try_compile被认为是更优越的替代方案。

探测编译器标志

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

设置编译器标志至关重要,以确保代码正确编译。不同的编译器供应商为相似的任务实现不同的标志。即使是同一供应商的不同编译器版本,也可能在可用的标志上略有差异。有时,会引入新的标志,这些标志对于调试或优化目的极为方便。在本节中,我们将展示如何检查所选编译器是否支持某些标志。

准备工作

消毒器(参考github.com/google/sanitizers)已经成为静态和动态代码分析的极其有用的工具。只需使用适当的标志重新编译代码并链接必要的库,您就可以调查和调试与内存错误(地址消毒器)、未初始化读取(内存消毒器)、线程安全(线程消毒器)和未定义行为(未定义行为消毒器)相关的问题。与类似的分析工具相比,消毒器通常引入的性能开销要小得多,并且往往提供更详细的问题检测信息。缺点是您的代码,可能还有部分工具链,需要使用额外的标志重新编译。

在本教程中,我们将设置一个项目以使用激活的不同消毒器编译代码,并展示如何检查正确的编译器标志是否可用。

如何操作

消毒器已经有一段时间与 Clang 编译器一起可用,并且后来也被引入到 GCC 工具集中。它们是为 C 和 C++程序设计的,但最近的 Fortran 版本将理解相同的标志并生成正确检测的库和可执行文件。然而,本教程将重点介绍一个 C++示例。

  1. 通常,我们首先声明一个 C++11 项目:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-07 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 我们声明一个列表CXX_BASIC_FLAGS,包含构建项目时始终使用的编译器标志,-g3-O1
list(APPEND CXX_BASIC_FLAGS "-g3" "-O1")
  1. 我们包含 CMake 模块CheckCXXCompilerFlag.cmake。类似的模块也可用于 C(CheckCCompilerFlag.cmake)和 Fortran(CheckFortranCompilerFlag.cmake,自 CMake 3.3 起):
include(CheckCXXCompilerFlag)
  1. 我们声明一个ASAN_FLAGS变量,它包含激活地址消毒器所需的标志,并设置CMAKE_REQUIRED_FLAGS变量,该变量由check_cxx_compiler_flag函数内部使用:
set(ASAN_FLAGS "-fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_REQUIRED_FLAGS ${ASAN_FLAGS})
  1. 我们调用check_cxx_compiler_flag以确保编译器理解ASAN_FLAGS变量中的标志。调用函数后,我们取消设置CMAKE_REQUIRED_FLAGS
check_cxx_compiler_flag(${ASAN_FLAGS} asan_works)
unset(CMAKE_REQUIRED_FLAGS)
  1. 如果编译器理解这些选项,我们将变量转换为列表,方法是替换空格为分号:
if(asan_works)
  string(REPLACE " " ";" _asan_flags ${ASAN_FLAGS})
  1. 我们为我们的代码示例添加一个带有地址消毒器的可执行目标:
add_executable(asan-example asan-example.cpp)
  1. 我们将可执行文件的编译器标志设置为包含基本和地址消毒器标志:
target_compile_options(asan-example
    PUBLIC
      ${CXX_BASIC_FLAGS}
      ${_asan_flags}
    )
  1. 最后,我们将地址消毒器标志也添加到链接器使用的标志集中。这关闭了if(asan_works)块:
target_link_libraries(asan-example PUBLIC ${_asan_flags})
endif()

完整的教程源代码还展示了如何为线程、内存和未定义行为消毒器编译和链接示例可执行文件。这些在这里没有详细讨论,因为我们使用相同的模式来检查编译器标志。

一个用于在您的系统上查找消毒器支持的自定义 CMake 模块可在 GitHub 上获得:github.com/arsenm/sanitizers-cmake

它是如何工作的

check__compiler_flag函数只是check__source_compiles函数的包装器,我们在上一节中讨论过。这些包装器为常见用例提供了一个快捷方式,即不重要检查特定的代码片段是否编译,而是检查编译器是否理解一组标志。

对于 sanitizer 的编译器标志来说,它们还需要传递给链接器。为了使用check__compiler_flag函数实现这一点,我们需要在调用之前设置CMAKE_REQUIRED_FLAGS变量。否则,作为第一个参数传递的标志只会在调用编译器时使用,导致错误的否定结果。

在本节中还有一个要点需要注意,那就是使用字符串变量和列表来设置编译器标志。如果在target_compile_optionstarget_link_libraries函数中使用字符串变量,将会导致编译器和/或链接器错误。CMake 会将这些选项用引号括起来,导致解析错误。这就解释了为什么需要以列表的形式表达这些选项,并进行后续的字符串操作,将字符串变量中的空格替换为分号。我们再次提醒,CMake 中的列表是分号分隔的字符串。

另请参阅

我们将在第七章,项目结构化,第三部分,编写测试和设置编译器标志的函数中重新审视并概括测试和设置编译器标志的模式。

探测执行

本节的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-05/recipe-08找到,并提供了一个 C/C++示例。本节适用于 CMake 版本 3.6(及以上),并在 GNU/Linux 和 macOS 上进行了测试。代码仓库还包含了一个与 CMake 3.5 兼容的示例。

到目前为止,我们已经展示了如何检查给定的源代码片段是否能被选定的编译器编译,以及如何确保所需的编译器和链接器标志可用。本节将展示如何检查代码片段是否可以在当前系统上编译、链接和运行。

准备工作

本节的代码示例是对第三章,检测外部库和程序,第九部分,*检测外部库:I. 使用pkg-config*的轻微变体。在那里,我们展示了如何在系统上找到 ZeroMQ 库并将其链接到 C 程序中。在本节中,我们将检查使用 GNU/Linux 系统 UUID 库的小型 C 程序是否可以实际运行,然后再生成实际的 C++程序。

如何操作

我们希望检查 GNU/Linux 上的 UUID 系统库是否可以链接,然后再开始构建我们自己的 C++项目。这可以通过以下一系列步骤实现:

  1. 我们首先声明一个混合 C 和 C++11 程序。这是必要的,因为我们要编译和运行的测试代码片段是用 C 语言编写的:
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
project(recipe-08 LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 我们需要在我们的系统上找到 UUID 库。这可以通过使用 pkg-config 来实现。我们要求搜索返回一个 CMake 导入目标,使用 IMPORTED_TARGET 参数:
find_package(PkgConfig REQUIRED QUIET)
pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET)
if(TARGET PkgConfig::UUID)
  message(STATUS "Found libuuid")
endif()
  1. 接下来,我们包含 CheckCSourceRuns.cmake 模块。对于 C++ 有一个类似的 CheckCXXSourceRuns.cmake 模块。然而,对于 Fortran 语言,在 CMake 3.11 中没有这样的模块:
include(CheckCSourceRuns)
  1. 我们声明一个包含要编译和运行的 C 代码片段的 _test_uuid 变量:
set(_test_uuid
  "
#include <uuid/uuid.h>
int main(int argc, char * argv[]) {
  uuid_t uuid;
  uuid_generate(uuid);
  return 0;
}
  ")
  1. 我们声明 CMAKE_REQUIRED_LIBRARIES 变量以微调对 check_c_source_runs 函数的调用。接下来,我们使用测试代码片段作为第一个参数和对 _runs 变量作为第二个参数调用 check_c_source_runs,以保存执行的检查结果。我们还取消设置 CMAKE_REQUIRED_LIBRARIES 变量:
set(CMAKE_REQUIRED_LIBRARIES PkgConfig::UUID)
check_c_source_runs("${_test_uuid}" _runs)
unset(CMAKE_REQUIRED_LIBRARIES)
  1. 如果检查未成功,可能是因为代码片段未编译或未运行,我们以致命错误停止配置:
if(NOT _runs)
  message(FATAL_ERROR "Cannot run a simple C executable using libuuid!")
endif()
  1. 否则,我们继续添加 C++ 可执行文件作为目标并链接到 UUID:
add_executable(use-uuid use-uuid.cpp)
target_link_libraries(use-uuid
  PUBLIC
    PkgConfig::UUID
  )

工作原理

check__source_runs 函数对于 C 和 C++ 的操作原理与 check__source_compiles 相同,但在实际运行生成的可执行文件时增加了额外步骤。与 check__source_compiles 一样,check__source_runs 的执行可以通过以下变量进行指导:

  • CMAKE_REQUIRED_FLAGS 用于设置编译器标志
  • CMAKE_REQUIRED_DEFINITIONS 用于设置预处理器宏
  • CMAKE_REQUIRED_INCLUDES 用于设置包含目录列表
  • CMAKE_REQUIRED_LIBRARIES 用于设置链接到可执行文件的库列表

由于我们使用了由 pkg_search_module 生成的导入目标,因此只需将 CMAKE_REQUIRES_LIBRARIES 设置为 PkgConfig::UUID,即可正确设置包含目录。

正如 check__source_compilestry_compile 的包装器,check__source_runs 是 CMake 中另一个更强大的命令 try_run 的包装器。因此,可以通过适当地包装 try_run 来编写一个提供与 C 和 C++ 模块相同功能的 CheckFortranSourceRuns.cmake 模块。

pkg_search_module 仅在 CMake 3.6 中学会了如何定义导入目标,但当前的配方也可以通过手动设置 check_c_source_runs 所需的包含目录和库来与早期版本的 CMake 一起工作,如下所示:set(CMAKE_REQUIRED_INCLUDES ${UUID_INCLUDE_DIRS})set(CMAKE_REQUIRED_LIBRARIES ${UUID_LIBRARIES})

使用生成器表达式微调配置和编译

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

CMake 提供了一种特定于领域的语言来描述如何配置和构建项目。自然地,描述特定条件的变量被引入,并且基于这些变量的条件语句被包含在CMakeLists.txt中。

在本配方中,我们将重新审视生成器表达式,我们在第四章,创建和运行测试中广泛使用它们,以紧凑地引用明确的测试可执行路径。生成器表达式提供了一个强大而紧凑的模式,用于逻辑和信息表达,这些表达在构建系统生成期间被评估,并产生特定于每个构建配置的信息。换句话说,生成器表达式对于引用仅在生成时已知的信息非常有用,但在配置时未知或难以知道;这在文件名、文件位置和库文件后缀的情况下尤其如此。

在本例中,我们将使用生成器表达式来有条件地设置预处理器定义,并有条件地链接消息传递接口(MPI)库,使我们能够构建相同的源代码,无论是顺序执行还是使用 MPI 并行性。

在本例中,我们将使用一个导入的目标来链接 MPI,该功能仅从 CMake 3.9 开始提供。然而,生成器表达式的方面可以转移到 CMake 3.0 或更高版本。

准备就绪

我们将编译以下示例源代码(example.cpp):

#include <iostream>
#ifdef HAVE_MPI
#include <mpi.h>
#endif
int main() {
#ifdef HAVE_MPI
  // initialize MPI
  MPI_Init(NULL, NULL);
  // query and print the rank
  int rank;
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  std::cout << "hello from rank " << rank << std::endl;
  // initialize MPI
  MPI_Finalize();
#else
  std::cout << "hello from a sequential binary" << std::endl;
#endif /* HAVE_MPI */
}

代码包含预处理器语句(#ifdef HAVE_MPI#else#endif),以便我们可以使用相同的源代码编译顺序或并行可执行文件。

如何操作

在编写CMakeLists.txt文件时,我们将重用我们在第三章,检测外部库和程序,第 6 个配方,检测 MPI 并行环境中遇到的构建块:

  1. 我们声明一个 C++11 项目:
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(recipe-09 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 然后,我们引入一个选项USE_MPI,以选择 MPI 并行化,并默认设置为ON。如果它是ON,我们使用find_package来定位 MPI 环境:
option(USE_MPI "Use MPI parallelization" ON)
if(USE_MPI)
  find_package(MPI REQUIRED)
endif()
  1. 然后,我们定义可执行目标,并根据条件设置相应的库依赖项(MPI::MPI_CXX)和预处理器定义(HAVE_MPI),我们将在稍后解释:
add_executable(example example.cpp)
target_link_libraries(example
  PUBLIC
    $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
  )
target_compile_definitions(example
  PRIVATE
    $<$<BOOL:${MPI_FOUND}>:HAVE_MPI>
  )
  1. 如果找到 MPI,我们还打印由FindMPI.cmake导出的INTERFACE_LINK_LIBRARIES,以演示非常方便的cmake_print_properties()函数:
if(MPI_FOUND)
  include(CMakePrintHelpers)
  cmake_print_properties(
    TARGETS MPI::MPI_CXX
    PROPERTIES INTERFACE_LINK_LIBRARIES
    )
endif()
  1. 让我们首先使用默认的 MPI 并行化开关ON配置代码。观察cmake_print_properties()的输出:
$ mkdir -p build_mpi
$ cd build_mpi
$ cmake ..
-- ...
-- 
 Properties for TARGET MPI::MPI_CXX:
 MPI::MPI_CXX.INTERFACE_LINK_LIBRARIES = "-Wl,-rpath -Wl,/usr/lib/openmpi -Wl,--enable-new-dtags -pthread;/usr/lib/openmpi/libmpi_cxx.so;/usr/lib/openmpi/libmpi.so"
  1. 我们编译并运行并行示例:
$ cmake --build .
$ mpirun -np 2 ./example
hello from rank 0
hello from rank 1
  1. 现在,让我们向上移动一个目录,创建一个新的构建目录,这次构建顺序版本:
$ mkdir -p build_seq
$ cd build_seq
$ cmake -D USE_MPI=OFF ..
$ cmake --build .
$ ./example
hello from a sequential binary

工作原理

项目的构建系统由 CMake 在两个阶段生成:配置阶段,其中解析CMakeLists.txt,生成阶段,实际生成构建环境。生成器表达式在这个第二阶段评估,并可用于使用只能在生成时知道的信息调整构建系统。因此,生成器表达式在交叉编译时特别有用,其中一些信息只有在解析CMakeLists.txt后才可用,或者在多配置项目中,构建系统为项目的所有不同配置(如DebugRelease)一次性生成。

在我们的例子中,我们将使用生成器表达式来有条件地设置链接依赖和编译定义。为此,我们可以关注这两个表达式:

target_link_libraries(example
  PUBLIC
    $<$<BOOL:${MPI_FOUND}>:MPI::MPI_CXX>
  )
target_compile_definitions(example
  PRIVATE
    $<$<BOOL:${MPI_FOUND}>:HAVE_MPI>
  )

如果MPI_FOUND为真,那么$将评估为 1。在这种情况下,$<$:MPI::MPI_CXX>将评估为MPI::MPI_CXX,第二个生成器表达式将评估为HAVE_MPI。如果我们设置USE_MPIOFFMPI_FOUND为假,两个生成器表达式都将评估为空字符串,因此不会引入链接依赖,也不会设置预处理器定义。

我们可以通过引入 if 语句来实现相同的效果:

if(MPI_FOUND)
  target_link_libraries(example
    PUBLIC
      MPI::MPI_CXX
    )
  target_compile_definitions(example
    PRIVATE
      HAVE_MPI
    )
endif()

这个解决方案可能不那么紧凑,但可能更易读。我们经常可以使用生成器表达式重新表达 if 语句,选择通常是个人喜好的问题。然而,生成器表达式在需要访问或操作显式文件路径时特别有用,因为这些路径使用变量和 if 子句构造起来可能很困难,在这种情况下,我们明显倾向于使用生成器表达式以提高可读性。在第四章,创建和运行测试中,我们使用生成器表达式来解析特定目标的文件路径。在第十一章,打包项目中,我们也会欣赏生成器表达式。

还有更多

CMake 提供了三种类型的生成器表达式:

  • 逻辑表达式,基本模式为$。基本条件是0表示假,1表示真,但任何布尔值都可以用作条件,只要使用正确的关键字即可。
  • 信息表达式,基本模式为$$。这些表达式评估为某些构建系统信息,例如,包含目录,目标属性等。这些表达式的输入参数可能是目标的名称,如表达式$,其中获取的信息将是tgt目标的prop属性。
  • 输出表达式,基本模式为$$。这些表达式生成输出,可能基于某些输入参数。它们的输出可以直接在 CMake 命令中使用,也可以与其他生成器表达式结合使用。例如,-I$, -I>将生成一个包含正在处理的目标的包含目录的字符串,每个目录前都添加了-I

另请参阅

如需查看生成器表达式的完整列表,请查阅cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html

相关文章
|
15天前
|
并行计算 编译器 Linux
CMake 秘籍(二)(4)
CMake 秘籍(二)
15 0
|
15天前
|
Linux C++ iOS开发
CMake 秘籍(四)(3)
CMake 秘籍(四)
7 0
|
15天前
|
Linux C++ iOS开发
CMake 秘籍(七)(1)
CMake 秘籍(七)
14 0
|
15天前
|
编译器 测试技术 开发工具
CMake 秘籍(八)(4)
CMake 秘籍(八)
15 0
|
15天前
|
Linux 测试技术 iOS开发
CMake 秘籍(四)(4)
CMake 秘籍(四)
13 0
|
15天前
|
消息中间件 Unix C语言
CMake 秘籍(二)(5)
CMake 秘籍(二)
18 1
|
15天前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
17 1
|
15天前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
16 1
|
15天前
|
Linux C++ iOS开发
CMake 秘籍(四)(1)
CMake 秘籍(四)
11 0
|
15天前
|
并行计算 Unix 编译器
CMake 秘籍(七)(5)
CMake 秘籍(七)
33 0

热门文章

最新文章