NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-5/recipe-03 中找到,其中包含一个C++例子。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。
项目的构建目标取决于命令的结果,这些命令只能在构建系统生成完成后的构建执行。CMake提供了三个选项来在构建时执行自定义命令:
使用add_custom_command
编译目标,生成输出文件。
add_custom_target
的执行没有输出。
构建目标前后,add_custom_command
的执行可以没有输出。
这三个选项强制执行特定的语义,并且不可互换。接下来的三个示例将演示具体的用法。
准备工作
我们将重用第3章第4节中的C++示例,以说明如何使用add_custom_command
的第一个选项。代码示例中,我们了解了现有的BLAS和LAPACK库,并编译了一个很小的C++包装器库,以调用线性代数的Fortran实现。
我们将把代码分成两部分。linear-algebra.cpp
的源文件与第3章、第4章没有区别,并且将包含线性代数包装器库的头文件和针对编译库的链接。源代码将打包到一个压缩的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
包,其中包含有源文件。要检测和使用这些文件,必须在构建时提取打包文件。通过使用带有-E
标志的CMake命令,以实现平台独立性。下一个命令会更新提取文件的时间戳。这样做是为了确保没有处理陈旧文件。WORKING_DIRECTORY
可以指定在何处执行命令。示例中,CMAKE_CURRENT_BINARY_DIR
是当前正在处理的构建目录。DEPENDS
参数列出了自定义命令的依赖项。例子中,压缩的tar
是一个依赖项。CMake使用COMMENT
字段在构建时打印状态消息。最后,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_categories
指定,其中wrap_BLAS_LAPACK
声明为INTERFACE
,因为只有依赖于math
目标的目标需要它。
add_custom_command
有两个限制:
只有在相同的CMakeLists.txt
中,指定了所有依赖于其输出的目标时才有效。
对于不同的独立目标,使用add_custom_command
的输出可以重新执行定制命令。这可能会导致冲突,应该避免这种情况的发生。
第二个限制,可以使用add_dependencies
来避免。不过,规避这两个限制的正确方法是使用add_custom_target
命令,我们将在下一节的示例中详细介绍。