CMake 秘籍(三)(4)

简介: CMake 秘籍(三)

CMake 秘籍(三)(3)https://developer.aliyun.com/article/1524621

第六章:配置时间和构建时间操作

在本章中,我们将涵盖以下食谱:

  • 使用平台无关的文件操作
  • 在配置时间运行自定义命令
  • 在构建时间运行自定义命令:I. 使用 add_custom_command
  • 在构建时间运行自定义命令:II. 使用 add_custom_target
  • 在构建时间对特定目标运行自定义命令
  • 探测编译和链接
  • 探测编译器标志
  • 探测执行
  • 使用生成器表达式微调配置和编译

引言

在本章中,我们将学习如何在配置时间和构建时间执行自定义操作。让我们简要回顾一下与由 CMake 管理的项目工作流程相关的时间概念:

  1. CMake 时间配置时间:这是当 CMake 正在运行并处理项目中的CMakeLists.txt文件时。
  2. 生成时间:这是当生成用于本地构建工具的文件,如 Makefiles 或 Visual Studio 项目文件时。
  3. 构建时间:这是当平台和工具本地的构建工具被调用时,在之前由 CMake 生成的平台和工具本地的构建脚本上。此时,编译器将被调用,目标(可执行文件和库)将在特定的构建目录中被构建。
  4. CTest 时间测试时间:当我们运行测试套件以检查目标是否按预期执行时。
  5. CDash 时间报告时间:当测试项目的结果上传到一个仪表板以与其他开发者共享时。
  6. 安装时间:当从构建目录到安装位置安装目标、源文件、可执行文件和库时。
  7. CPack 时间打包时间:当我们打包我们的项目以供分发,无论是作为源代码还是二进制。
  8. 包安装时间:当新制作的包被系统全局安装时。

完整的流程及其对应的时间在下图中描述:

本章关注于在配置时间和构建时间自定义行为。我们将学习如何使用这些命令:

  • execute_process 以从 CMake 内部执行任意进程并检索其输出
  • add_custom_target 以创建将执行自定义命令的目标
  • add_custom_command 以指定必须执行以生成文件或在其他目标的特定构建事件上的命令

使用平台无关的文件操作

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

在构建某些项目时,我们可能需要与主机平台文件系统进行交互。与文件的交互可能只是检查文件是否存在,创建一个新文件来存储临时信息,创建或提取存档等等。使用 CMake,我们不仅能够在不同的平台上生成构建系统,还能够执行这些操作,而不需要复杂的逻辑来抽象不同的操作系统。本节将展示如何以可移植的方式提取先前下载的存档。

准备就绪

我们将展示如何提取包含 Eigen 库的存档,并使用提取的源文件来编译我们的项目。在本节中,我们将重用来自第三章,检测外部库和程序,第七部分,检测 Eigen 库的线性代数示例linear-algebra.cpp。本节还假设包含 Eigen 源代码的存档已下载在与项目本身相同的目录中。

如何做到这一点

项目需要解包 Eigen 存档,并相应地设置目标的包含目录:

  1. 让我们首先声明一个 C++11 项目:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 我们向构建系统添加一个自定义目标。该自定义目标将在构建目录内提取存档:
add_custom_target(unpack-eigen
  ALL
  COMMAND
    ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/eigen-eigen-5a0156e40feb.tar.gz
  COMMAND
    ${CMAKE_COMMAND} -E rename eigen-eigen-5a0156e40feb eigen-3.3.4
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  COMMENT
    "Unpacking Eigen3 in ${CMAKE_CURRENT_BINARY_DIR}/eigen-3.3.4"
  )
  1. 我们为源文件添加一个可执行目标:
add_executable(linear-algebra linear-algebra.cpp)
  1. 由于我们的源文件的编译依赖于 Eigen 头文件,我们需要明确指定可执行目标对自定义目标的依赖:
add_dependencies(linear-algebra unpack-eigen)
  1. 最后,我们可以指定我们需要编译源文件的包含目录:
target_include_directories(linear-algebra
  PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/eigen-3.3.4
  )

它是如何工作的

让我们更仔细地看一下add_custom_target的调用:

add_custom_target(unpack-eigen
  ALL
  COMMAND
    ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/eigen-eigen-5a0156e40feb.tar.gz
  COMMAND
    ${CMAKE_COMMAND} -E rename eigen-eigen-5a0156e40feb eigen-3.3.4
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  COMMENT
    "Unpacking Eigen3 in ${CMAKE_CURRENT_BINARY_DIR}/eigen-3.3.4"
  )

我们正在向构建系统引入一个名为unpack-eigen的目标。由于我们传递了ALL参数,该目标将始终被执行。COMMAND参数允许您指定要执行的命令。在本例中,我们希望提取存档并将提取的目录重命名为eigen-3.3.4。这是通过这两个命令实现的:

  1. ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/eigen-eigen-5a0156e40feb.tar.gz
  2. ${CMAKE_COMMAND} -E rename eigen-eigen-5a0156e40feb eigen-3.3.4

注意我们是如何调用 CMake 命令本身,使用-E标志来执行实际的工作。对于许多常见操作,CMake 实现了一个在它运行的所有操作系统上都通用的接口。这使得构建系统的生成在很大程度上独立于特定的平台。add_custom_target命令中的下一个参数是工作目录,在我们的例子中对应于构建目录:CMAKE_CURRENT_BINARY_DIR。最后一个参数COMMENT用于指定在执行自定义目标时 CMake 应该打印出什么消息。

还有更多

add_custom_target 命令可用于在构建过程中执行一系列没有输出的自定义命令。正如我们在本食谱中所展示的,自定义目标可以被指定为项目中其他目标的依赖项。此外,自定义目标也可以依赖于其他目标,从而提供了在我们的构建中设置执行顺序的可能性。

使用 CMake 的 -E 标志,我们可以以操作系统无关的方式运行许多常见操作。在特定操作系统上可以运行的完整命令列表可以通过运行 cmake -Ecmake -E help 获得。例如,这是一个在 Linux 系统上的命令摘要:

Usage: cmake -E <command> [arguments...]
Available commands: 
  capabilities - Report capabilities built into cmake in JSON format
  chdir dir cmd [args...] - run command in a given directory
  compare_files file1 file2 - check if file1 is same as file2
  copy <file>... destination - copy files to destination (either file or directory)
  copy_directory <dir>... destination - copy content of <dir>... directories to 'destination' directory
  copy_if_different <file>... destination - copy files if it has changed
  echo [<string>...] - displays arguments as text
  echo_append [<string>...] - displays arguments as text but no new line
  env [--unset=NAME]... [NAME=VALUE]... COMMAND [ARG]...
                            - run command in a modified environment
  environment - display the current environment
  make_directory <dir>... - create parent and <dir> directories
  md5sum <file>... - create MD5 checksum of files
  remove [-f] <file>... - remove the file(s), use -f to force it
  remove_directory dir - remove a directory and its contents
  rename oldname newname - rename a file or directory (on one volume)
  server - start cmake in server mode
  sleep <number>... - sleep for given number of seconds
  tar [cxt][vf][zjJ] file.tar [file/dir1 file/dir2 ...]
                            - create or extract a tar or zip archive
  time command [args...] - run command and return elapsed time
  touch file - touch a file.
  touch_nocreate file - touch a file but do not create it.
Available on UNIX only:
  create_symlink old new - create a symbolic link new -> old

在配置时运行自定义命令

本食谱的代码可在 github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-05/recipe-02 获取。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

运行 CMake 会生成构建系统,从而指定本地构建工具必须执行哪些命令来构建您的项目,以及以什么顺序执行。我们已经看到 CMake 在配置时运行许多子任务,以找出工作编译器和必要的依赖项。在本食谱中,我们将讨论如何在配置时通过使用 execute_process 命令来运行自定义命令。

如何做到这一点

在 第三章,检测外部库和程序,食谱 3,检测 Python 模块和包中,我们已经展示了在尝试查找 NumPy Python 模块时使用 execute_process 的情况。在这个例子中,我们将使用 execute_process 命令来检查特定的 Python 模块(在这种情况下,Python CFFI)是否存在,如果存在,我们将发现其版本:

  1. 对于这个简单的示例,我们将不需要任何语言支持:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-02 LANGUAGES NONE)
  1. 我们将要求 Python 解释器执行一个简短的 Python 代码片段,为此我们使用 find_package 来发现解释器:
find_package(PythonInterp REQUIRED)
  1. 然后我们调用 execute_process 来运行一个简短的 Python 代码片段;我们将在下一节中更详细地讨论这个命令:
# this is set as variable to prepare
# for abstraction using loops or functions
set(_module_name "cffi")
execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import ${_module_name}; print(${_module_name}.__version__)"
  OUTPUT_VARIABLE _stdout
  ERROR_VARIABLE _stderr
  OUTPUT_STRIP_TRAILING_WHITESPACE
  ERROR_STRIP_TRAILING_WHITESPACE
  )
  1. 然后,我们打印结果:
if(_stderr MATCHES "ModuleNotFoundError")
  message(STATUS "Module ${_module_name} not found")
else()
  message(STATUS "Found module ${_module_name} v${_stdout}")
endif()
  1. 一个示例配置产生以下结果(假设 Python CFFI 包已安装在相应的 Python 环境中):
$ mkdir -p build
$ cd build
$ cmake ..
-- Found PythonInterp: /home/user/cmake-cookbook/chapter-05/recipe-02/example/venv/bin/python (found version "3.6.5") 
-- Found module cffi v1.11.5

它是如何工作的

execute_process 命令会在当前执行的 CMake 进程中产生一个或多个子进程,从而提供了一种强大且方便的方式来在配置项目时运行任意命令。在一次 execute_process 调用中可以执行多个命令。然而,请注意,每个命令的输出将被管道传输到下一个命令。该命令接受多个参数:

  • WORKING_DIRECTORY 允许您指定在哪个目录中执行命令。
  • RESULT_VARIABLE将包含运行进程的结果。这要么是一个整数,表示成功执行,要么是一个包含错误条件的字符串。
  • OUTPUT_VARIABLEERROR_VARIABLE将包含执行命令的标准输出和标准错误。请记住,由于命令的输出被输入,只有最后一个命令的标准输出将被保存到OUTPUT_VARIABLE中。
  • INPUT_FILEOUTPUT_FILEERROR_FILE指定最后一个命令的标准输入和标准输出文件名,以及所有命令的标准错误文件名。
  • 通过设置OUTPUT_QUIETERROR_QUIET,CMake 将分别忽略标准输出和标准错误。
  • 通过设置OUTPUT_STRIP_TRAILING_WHITESPACEERROR_STRIP_TRAILING_WHITESPACE,可以分别去除标准输出和标准错误中运行命令的尾随空格。

通过这些解释,我们可以回到我们的示例:

set(_module_name "cffi")
execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import ${_module_name}; print(${_module_name}.__version__)"
  OUTPUT_VARIABLE _stdout
  ERROR_VARIABLE _stderr
  OUTPUT_STRIP_TRAILING_WHITESPACE
  ERROR_STRIP_TRAILING_WHITESPACE
  )
if(_stderr MATCHES "ModuleNotFoundError")
  message(STATUS "Module ${_module_name} not found")
else()
  message(STATUS "Found module ${_module_name} v${_stdout}")
endif()

该命令检查python -c "import cffi; print(cffi.__version__)"的输出。如果找不到模块,_stderr将包含ModuleNotFoundError,我们在 if 语句中对此进行检查,在这种情况下,我们会打印找不到 cffi 模块。如果导入成功,Python 代码将打印模块版本,该版本被输入到_stdout,以便我们可以打印以下内容:

message(STATUS "Found module ${_module_name} v${_stdout}")

还有更多内容

在本示例中,我们仅打印了结果,但在实际项目中,我们可以警告、中止配置或设置可以查询以切换某些配置选项的变量。

将代码示例扩展到多个 Python 模块,如 Cython,避免代码重复,这将是一个有趣的练习。一种选择可能是使用foreach循环遍历模块名称;另一种方法可能是将代码抽象为函数或宏。我们将在第七章,项目结构化中讨论此类抽象。

在第九章,混合语言项目中,我们将使用 Python CFFI 和 Cython,而本节内容可以作为一个有用且可复用的代码片段,用于检测这些包是否存在。

在构建时运行自定义命令:I. 使用add_custom_command

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

项目构建目标可能依赖于只能在构建时执行的命令的结果,即在构建系统生成完成后。CMake 提供了三种选项来在构建时执行自定义命令:

  1. 使用add_custom_command生成要在目标内编译的输出文件。
  2. 使用 add_custom_target 执行没有输出的命令。
  3. 使用 add_custom_command 执行没有输出的命令,在目标构建之前或之后。

这三个选项强制特定的语义,并且不可互换。接下来的三个配方将阐明它们的使用案例。

准备就绪

我们将重用 第三章,检测外部库和程序,第 4 个配方,检测 BLAS 和 LAPACK 数学库 中的 C++ 示例,以说明 add_custom_command 第一种变体的使用。在该代码示例中,我们探测现有的 BLAS 和 LAPACK 库,并编译了一个微小的 C++ 包装器库,以调用我们需要的线性代数例程的 Fortran 实现。

我们将代码分成两部分。linear-algebra.cpp 的源文件与 第三章,检测外部库和程序,第 4 个配方,检测 BLAS 和 LAPACK 数学库 相比没有变化,并将包含线性代数包装器库的头文件并链接到编译库。然而,该库的源文件将被打包成一个与示例项目一起交付的压缩 tar 存档。该存档将在构建时提取,并在可执行文件之前编译线性代数包装器库。

如何做到这一点

我们的 CMakeLists.txt 将不得不包含一个自定义命令来提取线性代数包装器库的源文件。让我们详细看一下:

  1. 我们从熟悉的 CMake 版本、项目名称和支持的语言的定义开始:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-03 LANGUAGES CXX Fortran)
  1. 我们一如既往地选择 C++11 标准:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 然后是时候在我们的系统上寻找 BLAS 和 LAPACK 库了:
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)
  1. 我们声明一个变量 wrap_BLAS_LAPACK_sources,用于保存 wrap_BLAS_LAPACK.tar.gz 存档中包含的源文件的名称:
set(wrap_BLAS_LAPACK_sources
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.hpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.hpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.cpp
  )
  1. 我们声明自定义命令以提取 wrap_BLAS_LAPACK.tar.gz 存档并更新提取文件的时间戳。请注意,wrap_BLAS_LAPACK_sources 变量的内容是自定义命令的预期输出:
add_custom_command(
  OUTPUT
    ${wrap_BLAS_LAPACK_sources}
  COMMAND
    ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMAND
${CMAKE_COMMAND} -E touch ${wrap_BLAS_LAPACK_sources}
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMENT
    "Unpacking C++ wrappers for BLAS/LAPACK"
  VERBATIM
  )
  1. 接下来,我们添加一个库目标,其源文件是新提取的文件:
add_library(math "")
target_sources(math
  PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.cpp
  PUBLIC
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.hpp
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.hpp
  )
target_include_directories(math
  INTERFACE
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK
  )
target_link_libraries(math
  PUBLIC
    ${LAPACK_LIBRARIES}
  )
  1. 最后,添加了 linear-algebra 可执行目标。此可执行目标链接到包装器库:
add_executable(linear-algebra linear-algebra.cpp)
target_link_libraries(linear-algebra
  PRIVATE
    math
  )
  1. 有了这个,我们就可以配置、构建和执行示例:
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
$ ./linear-algebra 1000
C_DSCAL done
C_DGESV done
info is 0
check is 4.35597e-10

它是如何工作的

让我们更仔细地看一下 add_custom_command 的调用:

add_custom_command(
  OUTPUT
    ${wrap_BLAS_LAPACK_sources}
  COMMAND
    ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMAND
    ${CMAKE_COMMAND} -E touch ${wrap_BLAS_LAPACK_sources}
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMENT
    "Unpacking C++ wrappers for BLAS/LAPACK"
  VERBATIM
  )

add_custom_command 向目标添加规则,以便它们知道如何通过执行命令来生成输出。任何目标add_custom_command 的同一目录中声明,即在同一个 CMakeLists.txt 中,并且使用输出中的 任何文件 作为其源文件,将在构建时被赋予生成这些文件的规则。目标和自定义命令之间的依赖关系在构建系统生成时自动处理,而源文件的实际生成发生在构建时。

在我们特定的情况下,输出是包含在压缩的 tar 存档中的源文件。为了检索和使用这些文件,必须在构建时解压缩存档。这是通过使用 CMake 命令本身与-E标志来实现的,以实现平台独立性。下一个命令更新提取文件的时间戳。我们这样做是为了确保我们不会处理陈旧的源文件。WORKING_DIRECTORY指定执行命令的位置。在我们的例子中,这是CMAKE_CURRENT_BINARY_DIR,即当前正在处理的构建目录。DEPENDS关键字后面的参数列出了自定义命令的依赖项。在我们的例子中,压缩的 tar 存档是一个依赖项。COMMENT字段将由 CMake 用于在构建时打印状态消息。最后,VERBATIM告诉 CMake 为特定的生成器和平台生成正确的命令,从而确保完全的平台独立性。

让我们也仔细看看创建带有包装器的库的方式:

add_library(math "")
target_sources(math
  PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.cpp
  PUBLIC
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.hpp
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.hpp
  )
target_include_directories(math
  INTERFACE
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK
  )
target_link_libraries(math
  PUBLIC
    ${LAPACK_LIBRARIES}
  )

我们声明一个没有源文件的库目标。这是因为我们随后使用target_sources来填充目标的源文件。这实现了非常重要的任务,即让依赖于此目标的其他目标知道它们需要哪些包含目录和头文件,以便成功使用该库。C++源文件对于目标是PRIVATE,因此仅用于构建库。头文件是PUBLIC,因为目标及其依赖项都需要使用它们来成功编译。使用target_include_directories指定包含目录,并将wrap_BLAS_LAPACK声明为INTERFACE,因为只有math目标的依赖项才需要它。

add_custom_command的这种形式有两个限制:

  • 只有当所有依赖于其输出的目标都在同一个CMakeLists.txt中指定时,它才有效。
  • 对于不同的独立目标使用相同的输出,add_custom_command可能会重新执行自定义命令规则。这可能导致冲突,应予以避免。

第二个限制可以通过仔细使用add_dependencies引入依赖关系来避免,但为了规避这两个问题,正确的方法是使用add_custom_target命令,我们将在下一个示例中详细说明。

在构建时运行自定义命令:II. 使用 add_custom_target

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

正如我们在前一个配方中讨论的,add_custom_command有一些局限性,可以通过使用add_custom_target来规避。这个 CMake 命令将在构建系统中引入新的目标。这些目标反过来执行不返回输出的命令,与add_custom_command相反。命令add_custom_targetadd_custom_command可以结合使用。这样,自定义目标可以在与其依赖项不同的目录中指定,这在为项目设计模块化 CMake 基础设施时非常有用。

准备工作

对于这个配方,我们将重用前一个配方的源代码示例。然而,我们将稍微修改源文件的布局。特别是,我们不再将压缩的 tar 存档存储在顶层目录中,而是将其放置在一个名为deps的子目录中。这个子目录包含自己的CMakeLists.txt,它将被主CMakeLists.txt包含。

如何操作

我们将从主CMakeLists.txt开始,然后转到deps/CMakeLists.txt

  1. 与之前一样,我们声明一个 C++11 项目:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX Fortran)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
  1. 此时,我们转到deps/CMakeLists.txt。这是通过add_subdirectory命令实现的:
add_subdirectory(deps)
  1. deps/CMakeLists.txt内部,我们首先定位必要的库(BLAS 和 LAPACK):
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)
  1. 然后,我们将 tarball 存档的内容收集到一个变量MATH_SRCS中:
set(MATH_SRCS
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.hpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.hpp
  )
  1. 列出要提取的源文件后,我们定义一个自定义目标和一个自定义命令。这种组合在${CMAKE_CURRENT_BINARY_DIR}中提取存档。然而,我们现在处于不同的作用域,并引用deps/CMakeLists.txt,因此 tarball 将被提取到主项目构建目录下的deps子目录中:
add_custom_target(BLAS_LAPACK_wrappers
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  DEPENDS
    ${MATH_SRCS}
  COMMENT
    "Intermediate BLAS_LAPACK_wrappers target"
  VERBATIM
  )
add_custom_command(
  OUTPUT
    ${MATH_SRCS}
  COMMAND
    ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_BINARY_DIR}
  DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
  COMMENT
    "Unpacking C++ wrappers for BLAS/LAPACK"
  )
  1. 然后,我们将math库作为目标添加,并指定相应的源文件、包含目录和链接库:
add_library(math "")
target_sources(math
  PRIVATE
    ${MATH_SRCS}
  )
target_include_directories(math
  INTERFACE
    ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK
  )
# BLAS_LIBRARIES are included in LAPACK_LIBRARIES
target_link_libraries(math
  PUBLIC
    ${LAPACK_LIBRARIES} 
  )
  1. 一旦deps/CMakeLists.txt中的命令执行完毕,我们返回到父作用域,定义可执行目标,并将其与我们在下一目录定义的math库链接:
add_executable(linear-algebra linear-algebra.cpp)
target_link_libraries(linear-algebra
  PRIVATE
    math
  )

它是如何工作的

使用add_custom_target,用户可以在目标内部执行自定义命令。这与我们之前讨论的add_custom_command配方有所不同。通过add_custom_target添加的目标没有输出,因此总是被执行。因此,可以在子目录中引入自定义目标,并且仍然能够在顶层的CMakeLists.txt中引用它。

在本例中,我们通过结合使用add_custom_targetadd_custom_command提取了一个源文件归档。随后,这些源文件被用来编译一个库,我们设法在不同的(父)目录范围内将其链接起来。在构建CMakeLists.txt文件时,我们简要注释了 tarball 在deps下被提取,即项目构建目录的下一级子目录。这是因为,在 CMake 中,构建树的结构模仿了源树的层次结构。

在这个配方中,有一个值得注意的细节,我们应该讨论的是,我们将数学库源文件标记为PRIVATE的奇特事实:

set(MATH_SRCS
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxBLAS.hpp
  ${CMAKE_CURRENT_BINARY_DIR}/wrap_BLAS_LAPACK/CxxLAPACK.hpp
  )
# ...
add_library(math "")
target_sources(math
  PRIVATE
    ${MATH_SRCS}
  )
# ...

尽管这些源文件是PRIVATE,我们在父作用域中编译了linear-algebra.cpp,并且该源代码包含了CxxBLAS.hppCxxLAPACK.hpp。为什么在这里使用PRIVATE,以及如何可能编译linear-algebra.cpp并构建可执行文件?如果我们将头文件标记为PUBLIC,CMake 会在 CMake 时停止并报错,“找不到源文件”,因为尚未在文件树中生成(提取)的源文件不存在。

这是一个已知的限制(参见gitlab.kitware.com/cmake/cmake/issues/14633,以及相关博客文章:samthursfield.wordpress.com/2015/11/21/cmake-dependencies-between-targets-and-files-and-custom-commands)。我们通过将源文件声明为PRIVATE来规避这个限制。这样做,我们在 CMake 时没有得到任何对不存在源文件的文件依赖。然而,CMake 内置的 C/C++文件依赖扫描器在构建时识别了它们,并且源文件被编译和链接。

CMake 秘籍(三)(5)https://developer.aliyun.com/article/1524634

相关文章
|
6月前
|
编译器 Linux C语言
CMake 秘籍(二)(2)
CMake 秘籍(二)
51 2
|
6月前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
47 1
|
6月前
|
编译器 开发工具 git
CMake 秘籍(八)(1)
CMake 秘籍(八)
27 1
|
6月前
|
Shell Linux C++
CMake 秘籍(六)(4)
CMake 秘籍(六)
49 1
|
6月前
|
编译器 Linux 开发工具
CMake 秘籍(四)(2)
CMake 秘籍(四)
26 0
|
6月前
|
编译器 测试技术 开发工具
CMake 秘籍(八)(4)
CMake 秘籍(八)
28 0
|
6月前
|
并行计算 编译器 Linux
CMake 秘籍(二)(3)
CMake 秘籍(二)
32 0
|
6月前
|
并行计算 编译器 Linux
CMake 秘籍(二)(4)
CMake 秘籍(二)
52 0
|
6月前
|
XML 监控 Linux
CMake 秘籍(七)(4)
CMake 秘籍(七)
62 0
|
6月前
|
编译器 开发工具
CMake 秘籍(八)(2)
CMake 秘籍(八)
31 0