前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
17 CMake 教程
17.1 介绍
CMake 教程提供了一个循序渐进的指南,涵盖了 CMake 帮助解决的常见构建系统问题。了解各种主题如何在示例项目中协同工作将非常有帮助。教程文档和示例源代码可以在 CMake 源代码树的 Help/guide/tutorial 目录中找到。每个步骤都有自己的子目录,其中包含可以用作起点的代码。本教程示例是渐进的,因此每一步都为前一步提供完整的解决方案。
17.2 基本的起点(步骤 1 )
最基本的项目是从源代码文件构建的可执行文件。对于简单的项目,三行 CMakeLists.txt 文件就足够了。这将是本教程的起点。在 Step1 目录下创建一个 CMakeLists.txt 文件,如下所示:
cmake_minimum_required(VERSION 3.10)
# 设置项目名称
project(Tutorial)
# 添加可执行文件
add_executable(Tutorial tutorial.cxx)
注意,这个例子在 CMakeLists.txt 文件中使用小写命令。 CMake 支持大写、小写和混合大小写命令。 tutorial.cxx 的源代码在 Step1 目录中提供,可用于计算数字的平方根。
17.2.1 添加版本号和配置的头文件
我们要添加的第一个特性是为可执行文件和项目提供版本号。虽然我们可以只在源代码中这样做,但使用 CMakeLists.txt 提供了更多的灵活性。
首先,修 改CMakeLists.txt 文件,使用 project 命令设置项目名称和版本号。
cmake_minimum_required(VERSION 3.10)
# 设置项目名称和版本号
project(Tutorial VERSION 1.0)
然后,配置一个头文件,将版本号传递给源代码:
configure_file(TutorialConfig.h.in TutorialConfig.h)
由于配置的文件将被写入二进制树,我们必须将该目录添加到搜索包含文件的路径列表中。在 CMakeLists.txt 文件的末尾添加以下几行:
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
使用你喜欢的编辑器,在源码目录中创建 TutorialConfig.h.in ,包含以下内容:
// 为 Tutorial 配置的选项和设置
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
当 CMake 配置这个头文件时, @Tutorial_VERSION_MAJOR@ 和 @Tutorial_VERSION_MINOR@ 的值将被替换。
接下来修改 tutorial.cxx 来包含已配置的头文件 TutorialConfig.h 。
最后,让我们通过修改 tutorial.cxx 来打印出可执行文件的名称和版本号如下:
if (argc < 2) {
// report version
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}
17.2.2 指定 C++ 标准
接下来,让我们通过将 tutorial.cxx 中的 atof 替换为 std::stod 来为我们的项目添加一些 C++ 11 特性。同时,删除 #include <cstdlib> 。
const double inputValue = std::stod(argv[1]);
我们需要在 CMake 代码中明确声明它应该使用正确的标志。在 CMake 中启用对特定 C++ 标准的支持的最简单方法是使用 CMAKE_CXX_STANDARD 变量。在本教程中,将 CMakeLists.txt 文件中的 CMAKE_CXX_STANDARD 变量设置为 11 ,将 CMAKE_CXX_STANDARD_REQUIRED 设置为 True 。请确保在 add_executable 调用的上方添加 CMAKE_CXX_STANDARD 声明。
17.2.3 构建和测试
运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具构建它。
例如,我们可以从命令行导航到 CMake 源代码树的 Help/guide/tutorial 目录,并创建一个构建目录:
mkdir Step1_build
接下来,导航到 build 目录并运行 CMake 来配置项目并生成一个本地构建系统:
cd Step1_build
cmake ../Step1
然后调用构建系统来编译/链接项目:
cmake --build .
最后,尝试以以下命令来使用新构建的 Tutorial :
Tutorial 4294967296
Tutorial 10
Tutorial
17.3 添加库(步骤 2 )
现在我们将向项目中添加一个库。这个库将包含我们自己的计算数字平方根的实现。然后,可执行文件可以使用这个库,而不是编译器提供的标准平方根函数。
在本教程中,我们将把库放入名为 MathFunctions 的子目录中。这个目录已经包含了一个头文件 MathFunctions.h 和一个源文件 mysqrt.cxx 。源文件有一个名为 mysqrt 的函数,它提供了与编译器的 sqrt 函数类似的功能。
将以下一行 CMakeLists.txt 文件添加到 MathFunctions 目录中:
add_library(MathFunctions mysqrt.cxx)
为了使用新库,我们将在顶层 CMakeLists.txt 文件中添加一个 add_subdirectory 调用,以便来构建库。我们将新库添加到可执行文件中,并将 MathFunctions 添加为包含目录,以便可以找到 MathFunctions.h 头文件。顶层 CMakeLists.txt 文件的最后几行现在看起来像:
# 添加 MathFunctions 库
add_subdirectory(MathFunctions)
# 添加可执行文件
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)
# 将二进制树添加到包含文件的搜索路径中,这样我们就可以找到 TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)
现在让我们将 MathFunctions 库设置为可选的。虽然对于教程来说,确实没有必要这样做,但对于大型项目来说,这是一种常见的情况。第一步是向顶层的 CMakeLists.txt 文件添加一个选项。
option(USE_MYMATH "Use tutorial provided math implementation" ON)
# 配置一个头文件来传递一些 CMake 设置到源代码
configure_file(TutorialConfig.h.in TutorialConfig.h)
该选项将显示在 cmake-gui 和 ccmake 中,默认值为 ON ,可由用户更改。这个设置将被存储在缓存中,这样用户就不需要每次在构建目录上运行 CMake 时都设置这个值。
下一个更改是使 MathFunctions 库的构建和链接成为有条件的。要做到这一点,我们将顶层 CMakeLists.txt 文件的末尾修改为如下所示:
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()
# 添加可执行文件
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
# 将二进制树添加到包含文件的搜索路径中,这样我们就可以找到 TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
${EXTRA_INCLUDES}
)
注意,使用变量 EXTRA_LIBS 收集以后要链接到可执行文件中的任何可选库。变量 EXTRA_INCLUDES 类似地用于可选头文件。这是处理许多可选组件时的经典方法,我们将在下一步介绍现代方法。
对源代码的相应更改相当简单。首先,在 tutorial.cxx中 ,如果需要的话,包含 MathFunctions.h 头文件:
#ifdef USE_MYMATH
# include "MathFunctions.h"
#endif
然后,在同一个文件中,让 USE_MYMATH 控制使用哪个平方根函数:
#ifdef USE_MYMATH
const double outputValue = mysqrt(inputValue);
#else
const double outputValue = sqrt(inputValue);
#endif
由于源代码现在需要 USE_MYMATH ,我们可以将其添加到 TutorialConfig.h.in 中,使用以下行:
#cmakedefine USE_MYMATH
练习:为什么在 USE_MYMATH 选项之后配置 TutorialConfig.h.in 很重要?如果我们把这两个倒置会怎样?
运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具构建它。然后运行构建的教程可执行文件。
现在让我们更新 USE_MYMATH 的值。最简单的方法是使用 cmake-gui ,如果你在终端中,则使用 ccmake 。或者,如果你想从命令行更改选项,尝试:
cmake ../Step2 -DUSE_MYMATH=OFF
重新构建并再次运行教程。
哪个函数给出更好的结果, sqrt 或 mysqrt ?
17.4 增加库的使用要求 (步骤 3 )
使用要求允许对库或可执行文件的链接和包含进行更好的控制,同时也允许对 CMake 中目标的可传递属性进行更多的控制。利用使用要求的主要命令是:
让我们来重构添加库(步骤2)中的代码,以使用现代的 CMake 方法来满足使用需求。我们首先声明,任何链接到 MathFunctions 的人都需要包含当前源码目录,而 MathFunctions 本身则不需要。因此,这可以是 INTERFACE 使用需求。
记住, INTERFAC E意味着消费者需要而生产者不需要的东西。在 MathFunctions/CMakeLists.txt 的末尾添加以下几行:
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
现在我们已经指定了 MathFunctions 的使用需求,我们可以安全地从顶层的 CMakeLists.txt 中删除对 EXTRA_INCLUDES 变量的使用,如下所示:
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
endif()
还有这里:
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
一旦完成了这些,运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具或使用 cmake --build . 来从构建目录来构建它。
17.5 安装和测试 (步骤 4 )
现在我们可以开始向项目添加安装规则和测试支持。
17.5.1 安装规则
安装规则相当简单:对于 MathFunctions ,我们希望安装库和头文件,对于应用程序,我们希望安装可执行文件和配置的头文件。
因此,在 MathFunctions/CMakeLists.txt 的末尾,我们添加:
install(TARGETS MathFunctions DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)
在顶层 CMakeLists.txt 的末尾,我们添加:
install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include
)
这就是创建本教程的基本本地安装所需的全部内容。
现在运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具构建它。
然后使用 cmake 命令的 install 选项(在 3.15 中引入,旧版本的 cmake 必须使用 make install )从命令行运行安装步骤。对于多配置工具,不要忘记使用 --config 参数来指定配置。如果使用 IDE ,只需构建 INSTALL 目标。这一步将安装适当的头文件、库和可执行文件。例如:
cmake --install .
CMake 变量 CMAKE_INSTALL_PREFIX 用于确定文件安装的根目录。如果使用 cmake --install 命令,可以通过 --prefix 参数覆盖安装前缀。例如:
cmake --install . --prefix "/home/myuser/installdir"
导航到安装目录,并验证安装的教程是否能运行。
17.5.2 测试支持
接下来让我们测试我们的应用程序。在顶层 CMakeLists.txt 文件的末尾,我们可以启用测试,然后添加一些基本测试来验证应用程序是否正常工作。
enable_testing()
# 应用程序是否运行?
add_test(NAME Runs COMMAND Tutorial 25)
# 用法消息是否有效?
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
)
# 定义一个函数来简化添加测试
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endfunction(do_test)
# 做一堆基于结果的测试
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is [-nan|nan|0]")
do_test(Tutorial 0.0001 "0.0001 is 0.01")
第一个测试只是验证应用程序的运行,没有出现分段故障或以其他方式崩溃,并且返回值为零。这是 CTest 测试的基本形式。
下一个测试使用 PASS_REGULAR_EXPRESSION 测试属性来验证测试的输出是否包含某些字符串。在本例中,验证当提供的参数数量不正确时是否打印使用消息。
最后,我们有一个名为 do_test 的函数,它运行应用程序并验证给定输入的计算平方根是否正确。对于 do_test 的每次调用,都会在项目中添加一个带有名称、输入和基于参数被传递的预期结果的测试。
重新构建应用程序,然后 cd 到二进制目录并运行 ctest 可执行文件: ctest -N 和 ctest -VV 。对于多配置生成器(例如 Visual Studio ),必须指定配置类型。例如,要在 Debug 模式下运行测试,请在构建目录(而不是 Debug 子目录!)中使用 ctest -C Debug -VV 。或者,从 IDE 构建 RUN_TESTS 目标。
17.6 添加系统检查 (步骤 5 )
让我们考虑在我们的项目中添加一些代码,这些代码依赖于目标平台可能没有的特性。对于本例,我们将添加一些代码,这些代码取决于目标平台是否具有 log 和 exp 函数。当然,几乎每个平台都有这些功能,但在本教程中,我们假设它们并不常见。
如果平台有 log 和 exp ,那么我们将在 mysqrt 函数中使用它们计算平方根。我们首先使用 MathFunctions/CMakeLists.txt 中的 CheckSymbolExists 模块测试这些函数的可用性。在某些平台上,我们需要链接到 m 库。如果最初没有找到 log 和 exp ,则需要 m 库并再次尝试。
include(CheckSymbolExists)
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)
if(NOT (HAVE_LOG AND HAVE_EXP))
unset(HAVE_LOG CACHE)
unset(HAVE_EXP CACHE)
set(CMAKE_REQUIRED_LIBRARIES "m")
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)
if(HAVE_LOG AND HAVE_EXP)
target_link_libraries(MathFunctions PRIVATE m)
endif()
endif()
如果可用,使用 target_compile_definitions 指定 HAVE_LOG 和 HAVE_EXP 作为 PRIVATE 编译定义。
if(HAVE_LOG AND HAVE_EXP)
target_compile_definitions(MathFunctions
PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
如果系统上有 log 和 exp ,那么我们将在 mysqrt 函数中使用它们来计算平方根。将以下代码添加到 MathFunctions/mysqrt.cxx 的 mysqrt 函数中(在返回结果之前不要忘记 #endif ! ):
#if defined(HAVE_LOG) && defined(HAVE_EXP)
double result = exp(log(x) * 0.5);
std::cout << "Computing sqrt of " << x << " to be " << result
<< " using log and exp" << std::endl;
#else
double result = x;
我们还需要修改 mysqrt.cxx 来包含 cmath 。
#include <cmath>
运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具构建它,并运行教程可执行文件。
哪个函数现在给出了更好的结果, sqrt 还是 mysqrt ?
17.7 添加自定义命令并生成文件 (步骤 6 )
假设,出于本教程的目的,我们决定不使用平台的 log 和 exp 函数,而是希望生成一个预先计算值的表,以便在 mysqrt 函数中使用。在本节中,我们将创建表作为构建过程的一部分,然后将该表编译到我们的应用程序中。
首先,让我们删除 MathFunctions/CMakeLists.txt 中对 log 和 exp 函数的检查。然后从 mysqrt.cxx 中删除对 HAVE_LOG 和 HAVE_EXP 的检查。同时,我们可以删除 #include <cmath> 。
在 MathFunctions 子目录中,提供了一个名为 MakeTable.cxx 的新源文件,用于生成表格。
检查完文件后,我们可以看到表是作为有效的 c++ 代码生成的,并且输出文件名以参数形式传入。
下一步是将适当的命令添加到 MathFunctions/CMakeLists.txt 文件中,以构建 MakeTable 可执行文件,然后将其作为构建过程的一部分运行。需要几个命令来完成这个任务。
首先,在 MathFunctions/CMakeLists.txt 的顶部,添加 MakeTable 的可执行文件,就像添加任何其他可执行文件一样。
add_executable(MakeTable MakeTable.cxx)
然后添加一个自定义命令,指定如何通过运行 MakeTable 生成 Table.h 。
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)
接下来我们要让 CMake 知道 myaqrt.cxx 依赖于生成的 Table.h 文件。这是通过将生成的 Table.h 添加到库 MathFunctions 的源码列表中来实现的。
add_library(MathFunctions
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
我们还必须将当前二进制目录添加到包含目录列表中,以便可以找到 Table.h 并由 mysqrt.cxx 包含。
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
)
现在让我们使用生成的表。首先,修改 mysqrt.cxx 以包 Table.h 。接下来,我们可以重写 mysqrt 函数来使用这个表:
double mysqrt(double x)
{
if (x <= 0) {
return 0;
}
// 使用表来帮助查找初始值
double result = x;
if (x >= 1 && x < 10) {
std::cout << "Use the table to help find an initial value " << std::endl;
result = sqrtTable[static_cast<int>(x)];
}
// 进行十次迭代
for (int i = 0; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta = x - (result * result);
result = result + 0.5 * delta / result;
std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
}
return result;
}
运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具构建它。
当构建这个项目时,它将首先构建 MakeTable 可执行文件。然后,它将运行 MakeTable 以生成 Table.h 。最后,它将编译 mysqrt.cpp ,它包含了 Table.h 来生成 MathFunctions 库。
运行教程可执行文件并验证它是否正在使用表。
17.8 构建一个安装器 (步骤 7 )
接下来假设我们想要将我们的项目分发给其他人,以便他们可以使用它。我们希望在各种平台上提供二进制和源代码发行版。这与我们之前在 安装和测试(步骤 4 ) 中所做的安装略有不同,在步骤 4 中,我们安装了从源代码构建的二进制文件。在本例中,我们将构建支持二进制安装和包管理特性的安装包。为此,我们将使用 CPack 来创建特定于平台的安装器。具体来说,我们需要在顶层 CMakeLists.txt 文件的底部添加几行。
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include(CPack)
这就是所有要做的。我们从包含 InstallRequiredSystemLibraries 开始。此模块将包含当前平台上项目所需的任何运行时库。接下来,我们将一些 CPack 变量设置到存储此项目的许可证和版本信息的位置。本教程前面设置了版本信息,并且 license.txt 已包含在此步骤的顶层源代码目录中。
最后,我们将包含 CPack 模块,它将使用这些变量和当前系统的一些其他属性来设置安装程序。
下一步是以常规的方式构建项目,然后运行 cpack 可执行文件。要构建一个二进制的发行版,需要从二进制目录运行:
cpack
要指定生成器,请使用 -G 选项。对于多配置构建,使用 -C 来指定配置。例如:
cpack -G ZIP -C Debug
要创建一个源代码发行版,你可以输入:
cpack --config CPackSourceConfig.cmake
或者,运行 make package 或在 IDE 中右键单击 Package 目标并选择 Build Project 。
运行二进制目录中的安装程序。然后运行已安装的可执行文件并验证它是否有效。
17.9 添加对指示板的支持 (步骤 8 )
添加将测试结果提交到指示板的支持很简单。我们已经在测试支持中为我们的项目定义了许多测试。现在我们只需要运行这些测试并将它们提交到指示板上。为了包含对指示板的支持,我们在顶层的 CMakeLists.txt 中包含了 CTest 模块。
将
# 启用测试
enable_testing()
替换为
# 启用指示板脚本
include(CTest)
CTest 模块将自动调用 enable_testing() ,所以我们可以从 CMake 文件中删除它。
我们还需要在顶层目录中创建一个 CTestConfig.cmake 文件,以让我们可以指定项目的名称和提交指示板的位置。
set(CTEST_PROJECT_NAME "CMakeTutorial")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")
set(CTEST_DROP_SITE_CDASH TRUE)
ctest 可执行程序在运行时将读入该文件。要创建一个简单的指示板,你可以运行 cmake 可执行文件或 cmake-gui 来配置项目,但还没有构建它。相反,(如果要构建它,)将目录更改为二进制树目录,然后运行:
ctest [-VV] -D Experimental
记住,对于多配置生成器(例如 Visual Studio ),必须指定配置类型:
ctest [-VV] -C Debug -D Experimental
或者,从 IDE 上构建 Experimental 目标。
ctest 可执行文件将构建和测试项目,并将结果提交到 Kitware 的公共指示板: https://my.cdash.org/index.php?project=CMakeTutorial 。
17.10 混合静态和共享 (步骤 9 )
在本节中,我们将展示如何使用 BUILD_SHARED_LIBS 变量来控制 add_library 的默认行为,并允许控制如何构建没有显式类型( STATIC 、 SHARED 、 MODULE 或 OBJECT )的库。
要做到这一点,我们需要将 BUILD_SHARED_LIBS 添加到顶层的 CMakeLists.txt 中。我们使用 option 命令是因为它允许用户选择该值是 ON 还是 OFF 。
接下来,我们将重构 MathFunctions ,使其成为使用 mysqrt 或 sqrt 封装的真正库,而不是要求调用代码执行此逻辑。这也意味着 USE_MYMATH 将不控制 MathFunctions 的构建,而是控制这个库的行为。
第一步是更新顶层 CMakeLists.txt 的起始部分,如下所示:
cmake_minimum_required(VERSION 3.10)
# 设置项目名称和版本
project(Tutorial VERSION 1.0)
# 指定 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 控制静态和共享库的构建位置,这样在 Windows 上我们就不需要修改运行可执行文件的路径
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
# 将头文件配置为只传递版本号
configure_file(TutorialConfig.h.in TutorialConfig.h)
# 添加 MathFunctions 库
add_subdirectory(MathFunctions)
# 添加可执行文件
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)
现在我们会总是使用 MathFunctions ,所以我们将需要更新该库的逻辑。因此,在 MathFunctions/CMakeLists.txt 中,我们需要创建一个 SqrtLibrary ,它将在 USE_MYMATH 启用时有条件地构建和安装。现在,由于这是一个教程,我们将显式地要求 SqrtLibrary 是静态构建的。
最终的结果是 MathFunctions/CMakeLists.txt 应该看起来像:
# 添加运行的库
add_library(MathFunctions MathFunctions.cxx)
# 声明任何链接到我们的人都需要包含当前的源码目录才能找到 MathFunctions.h ,而我们自身是不需要。
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
# 我们是否应该使用自己的数学函数
option(USE_MYMATH "Use tutorial provided math implementation" ON)
if(USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
# 首先,我们添加生成表的可执行文件
add_executable(MakeTable MakeTable.cxx)
# 添加生成源代码的命令
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)
# 只做 sqrt 的库
add_library(SqrtLibrary STATIC
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
# 声明我们依赖二进制目录来查找 Table.h
target_include_directories(SqrtLibrary PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
# 定义在 Windows 上构建时使用 declspec(dllexport) 的符号
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")
# 安装规则
set(installable_libs MathFunctions)
if(TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)
接下来,更新 MathFunctions/mysqrt.cxx 以使用 mathfunctions 和 detail 命名空间:
#include <iostream>
#include "MathFunctions.h"
// 包含生成的表
#include "Table.h"
namespace mathfunctions {
namespace detail {
// 使用简单的操作处理一个平方根计算
double mysqrt(double x)
{
if (x <= 0) {
return 0;
}
// 使用表来协助查找初始值
double result = x;
if (x >= 1 && x < 10) {
std::cout << "Use the table to help find an initial value " << std::endl;
result = sqrtTable[static_cast<int>(x)];
}
// 做十次迭代
for (int i = 0; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta = x - (result * result);
result = result + 0.5 * delta / result;
std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
}
return result;
}
}
}
我们还需要在 tutorial.cxx 中做一些修改。这样它就不再使用 USE_MYMATH 了:
- 始终包含 MathFunctions.h
- 始终使用 mathfunctions::sqrt
- 不要包含 cmath
最后,更新 MathFunctions/MathFunctions.h 以使用 dll 导出定义:
#if defined(_WIN32)
# if defined(EXPORTING_MYMATH)
# define DECLSPEC __declspec(dllexport)
# else
# define DECLSPEC __declspec(dllimport)
# endif
#else // non windows
# define DECLSPEC
#endif
此时,如果你构建了所有内容,你可能会注意到链接失败,因为我们正在将没有位置无关代码的静态库与具有位置无关代码的库组合在一起。解决这个问题的方法是无论构建类型如何,都显式地将 SqrtLibrary 的 POSITION_INDEPENDENT_CODE 目标属性设置为 True 。
# 当默认为共享库时,声明 SqrtLibrary 需要 PIC
set_target_properties(SqrtLibrary PROPERTIES
POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
练习:我们修改了 MathFunctions.h 以使用 dll 导出定义。使用 CMake 文档你能找到一个辅助模块来简化这个吗?
17.11 添加生成器表达式 (步骤 10 )
生成器表达式在构建系统的生成期间求值,以生成特定于每个生成配置的信息。
生成器表达式允许在许多目标属性的上下文中使用,例如 LINK_LIBRARIES 、 INCLUDE_DIRECTORIES 、 COMPILE_DEFINITIONS 等。当使用命令填充这些属性时,也可以使用它们,例如 target_link_libraries 、 target_include_directories 、 target_compile_definitions 等。
生成器表达式可用于启用条件链接、编译时使用的条件定义、条件包含目录等。条件可以基于构建配置、目标属性、平台信息或任何其他可查询的信息。
有不同类型的生成器表达式,包括逻辑表达式、信息表达式和输出表达式。
逻辑表达式用于创建条件输出。基本表达式是 0 和 1 表达式。 $ < 0:…> 返回空字符串, $<1:…> 的结果是“…”的内容。它们也可以嵌套。
生成器表达式的常见用法是有条件地添加编译器标志,例如用于语言级别或警告的标志。一个不错的模式是将此信息关联到一个 INTERFACE 目标,从而允许此信息传播。让我们首先构造一个 INTERFACE 目标,并指定所需的 C++ 标准级别为 11 ,而不是使用 CMAKE_CXX_STANDARD 。
所以下面的代码:
# 指定 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
将会被替换为:
add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)
接下来,我们添加项目所需的编译器警告标志。由于警告标志因编译器的不同而不同,我们使用 COMPILE_LANG_AND_ID 生成器表达式来控制给定语言和一组编译器 id 来应用哪些标志,如下所示:
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(tutorial_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
在这里,我们看到警告标志被封装在 BUILD_INTERFACE 条件中。这样做是为了使已安装项目的使用者不会继承我们的警告标志。
练习:修改 MathFunctions/CMakeLists.txt ,以便所有目标都有对 tutorial_compiler_flags 的 target_link_libraries 调用。
17.12 添加导出配置 (步骤 11 )
在 安装和测试(步骤 4 ) 的教程中,我们添加了 CMake 安装项目库和头文件的能力。在 构建安装程序(步骤7) 中,我们添加了打包这些信息的功能,以便将其分发给其他人。
下一步是添加必要的信息,以便其他 CMake 项目无论是从构建目录、本地安装还是所打的包都可以使用我们的项目。
第一步是更新我们的 install(TARGETS) 命令,不仅要指定 DESTINATION ,还要指定 EXPORT 。 EXPORT 关键字生成并安装一个 CMake 文件,其中包含从安装树中导入 install 命令中列出的所有目标的代码。因此,让我们继续,通过更新 MathFunctions/CMakeLists.txt 中的安装命令显式地导出 MathFunctions 库,如下所示:
set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs}
DESTINATION lib
EXPORT MathFunctionsTargets)
install(FILES MathFunctions.h DESTINATION include)
现在我们已经导出了 MathFunctions ,我们还需要显式地安装生成的 MathFunctionsTargets.cmake 文件。这是通过在顶层 CMakeLists.txt 的底部添加以下内容来完成的:
install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)
现在,你应该尝试运行 CMake 。如果一切设置正确,你会看到 CMake 将生成一个错误,看起来像:
Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
path:
"/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions"
which is prefixed in the source directory.
CMake 想说的是,在生成导出信息期间,它将导出一个与当前机器本质上绑定的路径,并且在其他机器上无效。这个问题的解决方案是更新 MathFunctions 的 target_include_directories ,以表示从构建目录和安装/包中使用它时需要不同的 INTERFACE 位置。这意味着要将 MathFunctions 的 target_include_directories 调用转换为如下样子:
target_include_directories(MathFunctions
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
一旦这个被更新,我们可以重新运行 CMake 并验证它不再发出警告。
此时,我们已经让 CMake 正确地打包了所需的目标信息,但我们仍然需要生成一个 MathFunctionsConfig.cmake ,这样 find_package 命令可以找到我们的项目。因此,让我们继续,在项目的顶层添加一个名为 Config.cmake.in 的新文件,其内容如下:
@PACKAGE_INIT@
include ( "${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake" )
然后,为了正确配置和安装该文件,将以下内容添加到顶层 CMakeLists.txt 的底部:
install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)
include(CMakePackageConfigHelpers)
# 生成包含导出的配置文件
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
INSTALL_DESTINATION "lib/cmake/example"
NO_SET_AND_CHECK_MACRO
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
# 生成配置文件的版本文件
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
COMPATIBILITY AnyNewerVersion
)
# 安装配置文件
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
DESTINATION lib/cmake/MathFunctions
)
至此,我们已经为我们的项目生成了一个可重新定位的 CMake 配置,它可以在项目安装或打包后使用。如果我们想让我们的项目也从构建目录中使用,我们只需要在顶层 CMakeLists.txt 的底部添加以下内容:
export(EXPORT MathFunctionsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)
通过这个导出调用,我们现在生成一个Targets.cmake ,这允许配置的 MathFunctionsConfig.cmake 在构建目录中,以供其他项目使用,而无需安装它。
17.13 打包 Debug 和 Release (步骤 12 )
注意:这个例子适用于单配置生成器,不适用于多配置生成器(例如 Visual Studio )。
默认情况下, CMake 的模型是一个构建目录只包含一个配置,可能是 Debug , Release , MinSizeRel 或 RelWithDebInfo 。但是,可以设置 CPack 来捆绑多个构建目录,并构造一个包含同一项目的多个配置的包。
首先,我们希望确保 Debug 和 Release 版本对将要安装的可执行文件和库使用不同的名称。让我们使用 d 作为 Debug 可执行文件和库的后缀。
将 CMAKE_DEBUG_POSTFIX 设置在顶层 CMakeLists.txt 文件的开头附近:
set(CMAKE_DEBUG_POSTFIX d)
add_library(tutorial_compiler_flags INTERFACE)
并在 tutorial 可执行文件上设置 DEBUG_POSTFIX 属性:
add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})
target_link_libraries(Tutorial PUBLIC MathFunctions)
我们还将版本编号添加到 MathFunctions 库中。在 MathFunctions/CMakeLists.txt 中,设置 VERSION 和 SOVERSION 属性:
set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")
在 Step12 目录中,创建 Debug 和 Release 子目录。布局看起来像这样:
- Step12
- debug
- release
现在我们需要构建 Debug 和 Release 版本。我们可以使用 CMAKE_BUILD_TYPE 来设置配置类型:
cd debug
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .
cd ../release
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
现在 Debug 和 Release 构建都完成了,我们可以使用自定义配置文件将这两个构建打包到一个单独的发布中。在 Step12 目录中,创建一个名为 MultiCPackConfig.cmake 的文件。在这个文件中,首先包含由 cmake 可执行文件创建的默认配置文件。
接下来,使用 CPACK_INSTALL_CMAKE_PROJECTS 变量指定要安装的项目。在这种情况下,我们希望同时安装 Debug 和 Release 。
include("release/CPackConfig.cmake")
set(CPACK_INSTALL_CMAKE_PROJECTS
"debug;Tutorial;ALL;/"
"release;Tutorial;ALL;/"
)
在 Step12 目录下,运行 cpack ,指定我们的自定义配置文件和 config 选项:
cpack --config MultiCPackConfig.cmake