CMake 秘籍(三)(3)https://developer.aliyun.com/article/1524621
第六章:配置时间和构建时间操作
在本章中,我们将涵盖以下食谱:
- 使用平台无关的文件操作
- 在配置时间运行自定义命令
- 在构建时间运行自定义命令:I. 使用
add_custom_command
- 在构建时间运行自定义命令:II. 使用
add_custom_target
- 在构建时间对特定目标运行自定义命令
- 探测编译和链接
- 探测编译器标志
- 探测执行
- 使用生成器表达式微调配置和编译
引言
在本章中,我们将学习如何在配置时间和构建时间执行自定义操作。让我们简要回顾一下与由 CMake 管理的项目工作流程相关的时间概念:
- CMake 时间或配置时间:这是当 CMake 正在运行并处理项目中的
CMakeLists.txt
文件时。 - 生成时间:这是当生成用于本地构建工具的文件,如 Makefiles 或 Visual Studio 项目文件时。
- 构建时间:这是当平台和工具本地的构建工具被调用时,在之前由 CMake 生成的平台和工具本地的构建脚本上。此时,编译器将被调用,目标(可执行文件和库)将在特定的构建目录中被构建。
- CTest 时间或测试时间:当我们运行测试套件以检查目标是否按预期执行时。
- CDash 时间或报告时间:当测试项目的结果上传到一个仪表板以与其他开发者共享时。
- 安装时间:当从构建目录到安装位置安装目标、源文件、可执行文件和库时。
- CPack 时间或打包时间:当我们打包我们的项目以供分发,无论是作为源代码还是二进制。
- 包安装时间:当新制作的包被系统全局安装时。
完整的流程及其对应的时间在下图中描述:
本章关注于在配置时间和构建时间自定义行为。我们将学习如何使用这些命令:
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 存档,并相应地设置目标的包含目录:
- 让我们首先声明一个 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)
- 我们向构建系统添加一个自定义目标。该自定义目标将在构建目录内提取存档:
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" )
- 我们为源文件添加一个可执行目标:
add_executable(linear-algebra linear-algebra.cpp)
- 由于我们的源文件的编译依赖于 Eigen 头文件,我们需要明确指定可执行目标对自定义目标的依赖:
add_dependencies(linear-algebra unpack-eigen)
- 最后,我们可以指定我们需要编译源文件的包含目录:
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
。这是通过这两个命令实现的:
${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/eigen-eigen-5a0156e40feb.tar.gz
${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 -E
或 cmake -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)是否存在,如果存在,我们将发现其版本:
- 对于这个简单的示例,我们将不需要任何语言支持:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-02 LANGUAGES NONE)
- 我们将要求 Python 解释器执行一个简短的 Python 代码片段,为此我们使用
find_package
来发现解释器:
find_package(PythonInterp REQUIRED)
- 然后我们调用
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 )
- 然后,我们打印结果:
if(_stderr MATCHES "ModuleNotFoundError") message(STATUS "Module ${_module_name} not found") else() message(STATUS "Found module ${_module_name} v${_stdout}") endif()
- 一个示例配置产生以下结果(假设 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_VARIABLE
和ERROR_VARIABLE
将包含执行命令的标准输出和标准错误。请记住,由于命令的输出被输入,只有最后一个命令的标准输出将被保存到OUTPUT_VARIABLE
中。INPUT_FILE
、OUTPUT_FILE
和ERROR_FILE
指定最后一个命令的标准输入和标准输出文件名,以及所有命令的标准错误文件名。- 通过设置
OUTPUT_QUIET
和ERROR_QUIET
,CMake 将分别忽略标准输出和标准错误。 - 通过设置
OUTPUT_STRIP_TRAILING_WHITESPACE
和ERROR_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 提供了三种选项来在构建时执行自定义命令:
- 使用
add_custom_command
生成要在目标内编译的输出文件。 - 使用
add_custom_target
执行没有输出的命令。 - 使用
add_custom_command
执行没有输出的命令,在目标构建之前或之后。
这三个选项强制特定的语义,并且不可互换。接下来的三个配方将阐明它们的使用案例。
准备就绪
我们将重用 第三章,检测外部库和程序,第 4 个配方,检测 BLAS 和 LAPACK 数学库 中的 C++ 示例,以说明 add_custom_command
第一种变体的使用。在该代码示例中,我们探测现有的 BLAS 和 LAPACK 库,并编译了一个微小的 C++ 包装器库,以调用我们需要的线性代数例程的 Fortran 实现。
我们将代码分成两部分。linear-algebra.cpp
的源文件与 第三章,检测外部库和程序,第 4 个配方,检测 BLAS 和 LAPACK 数学库 相比没有变化,并将包含线性代数包装器库的头文件并链接到编译库。然而,该库的源文件将被打包成一个与示例项目一起交付的压缩 tar 存档。该存档将在构建时提取,并在可执行文件之前编译线性代数包装器库。
如何做到这一点
我们的 CMakeLists.txt
将不得不包含一个自定义命令来提取线性代数包装器库的源文件。让我们详细看一下:
- 我们从熟悉的 CMake 版本、项目名称和支持的语言的定义开始:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-03 LANGUAGES CXX Fortran)
- 我们一如既往地选择 C++11 标准:
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 然后是时候在我们的系统上寻找 BLAS 和 LAPACK 库了:
find_package(BLAS REQUIRED) find_package(LAPACK REQUIRED)
- 我们声明一个变量
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 )
- 我们声明自定义命令以提取
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 )
- 接下来,我们添加一个库目标,其源文件是新提取的文件:
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} )
- 最后,添加了
linear-algebra
可执行目标。此可执行目标链接到包装器库:
add_executable(linear-algebra linear-algebra.cpp) target_link_libraries(linear-algebra PRIVATE math )
- 有了这个,我们就可以配置、构建和执行示例:
$ 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_target
和add_custom_command
可以结合使用。这样,自定义目标可以在与其依赖项不同的目录中指定,这在为项目设计模块化 CMake 基础设施时非常有用。
准备工作
对于这个配方,我们将重用前一个配方的源代码示例。然而,我们将稍微修改源文件的布局。特别是,我们不再将压缩的 tar 存档存储在顶层目录中,而是将其放置在一个名为deps
的子目录中。这个子目录包含自己的CMakeLists.txt
,它将被主CMakeLists.txt
包含。
如何操作
我们将从主CMakeLists.txt
开始,然后转到deps/CMakeLists.txt
:
- 与之前一样,我们声明一个 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)
- 此时,我们转到
deps/CMakeLists.txt
。这是通过add_subdirectory
命令实现的:
add_subdirectory(deps)
- 在
deps/CMakeLists.txt
内部,我们首先定位必要的库(BLAS 和 LAPACK):
find_package(BLAS REQUIRED) find_package(LAPACK REQUIRED)
- 然后,我们将 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 )
- 列出要提取的源文件后,我们定义一个自定义目标和一个自定义命令。这种组合在
${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" )
- 然后,我们将
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} )
- 一旦
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_target
和add_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.hpp
和CxxLAPACK.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