CMake 教程

简介

CMake 教程提供了循序渐进的指南,涵盖了 CMake 帮助解决的常见构建系统问题。了解各种主题如何在一个示例项目中协同工作非常有帮助。可以在 CMake 源代码树的 Help/guide/tutorial 目录中找到教程文档和示例源代码。每个步骤都有自己的子目录,其中包含可用作起点的代码。教程示例是渐进的,因此每个步骤都为上一步提供完整的解决方案。

基本起点(步骤 1)

最基本的项目是一个由源代码文件构建的可执行文件。对于简单的项目,三行 CMakeLists.txt 文件就足够了。这将成为我们本教程的起点。在 Step1 目录中创建一个 CMakeLists.txt 文件,如下所示

cmake_minimum_required(VERSION 3.10)

# set the project name
project(Tutorial)

# add the executable
add_executable(Tutorial tutorial.cxx)

请注意,此示例在 CMakeLists.txt 文件中使用了小写命令。CMake 支持大写、小写和混合大小写命令。 tutorial.cxx 的源代码在 Step1 目录中提供,可用于计算数字的平方根。

添加版本号和已配置头文件

我们将添加的第一个功能是为我们的可执行文件和项目提供版本号。虽然我们可以在源代码中单独执行此操作,但使用 CMakeLists.txt 可提供更大的灵活性。

首先,修改 CMakeLists.txt 文件以使用 project 命令设置项目名称和版本号。

cmake_minimum_required(VERSION 3.10)

# set the project name and version
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

// the configured options and settings for 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;
  }

指定 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。确保将 CMAKE_CXX_STANDARD 声明添加到 add_executable 的调用上方。

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

构建并测试

运行 cmake 可执行文件或 cmake-gui 来配置项目,然后使用你选择的构建工具构建它。

例如,从命令行我们可以导航到 CMake 源代码树的 Help/guide/tutorial 目录并创建一个构建目录

mkdir Step1_build

接下来,导航到构建目录并运行 CMake 以配置项目并生成原生构建系统

cd Step1_build
cmake ../Step1

然后调用该构建系统以实际编译/链接项目

cmake --build .

最后,尝试用这些命令使用新构建的 Tutorial

Tutorial 4294967296
Tutorial 10
Tutorial

添加库(步骤 2)

现在我们将向项目添加一个库。此库将包含我们自己用于计算数字平方根的实现。然后,可执行文件可以使用此库,而不是编译器提供的标准平方根函数。

在本教程中,我们将库放入名为 MathFunctions 的子目录中。此目录已包含一个头文件 MathFunctions.h 和一个源文件 mysqrt.cxx。源文件有一个名为 mysqrt 的函数,它提供与编译器的 sqrt 函数类似的功能。

MathFunctions 目录中添加以下一行 CMakeLists.txt 文件

add_library(MathFunctions mysqrt.cxx)

为了使用新库,我们将在顶层 CMakeLists.txt 文件中添加一个 add_subdirectory 调用,以便对库进行构建。我们将新库添加到可执行文件中,并将 MathFunctions 添加为包含目录,以便可以找到 mysqrt.h 头文件。现在,顶层 CMakeLists.txt 文件的最后几行应如下所示

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC MathFunctions)

# add the binary tree to the search path for include files
# so that we will find 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)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

此选项将显示在 cmake-guiccmake 中,其默认值为 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 the executable
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# add the binary tree to the search path for include files
# so that we will find 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 以配置项目,然后使用你选择的构建工具对其进行构建。然后运行构建好的 Tutorial 可执行文件。

现在,让我们更新 USE_MYMATH 的值。最简单的方法是使用 cmake-gui,如果你在终端中,可以使用 ccmake。或者,如果你想从命令行更改选项,请尝试

cmake ../Step2 -DUSE_MYMATH=OFF

重新构建并再次运行教程。

哪个函数能够提供更好的结果,sqrt 或 mysqrt?

为此库添加用法要求(步骤 3)

使用要求可以对库或可执行文件的链接进行更良好的控制,并包含库的 transitive 属性,同时还可以进一步控制 CMake 中目标的 transitive 属性。使用要求的一些主要命令有:

让我们重构我们的代码,从 添加库(步骤 2) 转变为使用现代化的 CMake 方法来使用要求。我们首先声明任何链接到 MathFunctions 的内容都需要包含当前源目录,而 MathFunctions 本身不需要。因此这可以成为一个 INTERFACE 使用要求。

记住 INTERFACE 表示消费者需要但生产者不需要的内容。将下面的行添加到 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 . 来构建它。

安装和测试(步骤 4)

现在我们可以开始在我们的项目中添加安装规则并支持测试。

安装规则

安装规则非常简单:对于 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 命令(在 3.15 中引入,更旧版本的 CMake 必须使用 make install)的 install 选项(从命令行)运行安装步骤。对于多配置工具,不要忘记使用 --config 参数指定配置。如果使用 IDE,只需构建 INSTALL 目标。此步骤将安装相应的头文件、库和可执行文件。例如

cmake --install .

CMake 变量 CMAKE_INSTALL_PREFIX 用于确定文件安装到的根路径。如果使用 cmake --install 命令,可以通过 --prefix 参数替代安装前缀。例如

cmake --install . --prefix "/home/myuser/installdir"

导航到安装目录,验证已安装教程是否运行。

测试支持

接下来,我们测试应用程序。在顶级 CMakeLists.txt 文件末尾,我们可以启用测试,然后添加一些基本测试以验证应用程序是否正常工作。

enable_testing()

# does the application run
add_test(NAME Runs COMMAND Tutorial 25)

# does the usage message work?
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
  )

# define a function to simplify adding tests
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 a bunch of result based tests
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 -Nctest -VV。对于多配置生成器(例如 Visual Studio),必须指定配置类型。例如,要以调试模式运行测试,请从构建目录(而不是 Debug 子目录!)中使用 ctest -C Debug -VV。或者,从 IDE 构建 RUN_TESTS 目标。

添加系统自省(步骤 5)

让我们考虑为我们的项目添加一些代码,它依赖于目标平台可能没有的功能。对于本示例,我们将添加一些依赖于目标平台是否有logexp函数的代码。当然,几乎每个平台都有这些函数,但本教程假设它们不常见。

如果平台有logexp,那么我们将在mysqrt函数中使用它们来计算平方根。我们首先使用CheckSymbolExists模块测试这些函数在MathFunctions/CMakeLists.txt中的可用性。在某些平台上,我们需要链接到 m 库。如果最初没有找到logexp,请要求 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_definitionsHAVE_LOGHAVE_EXP指定为PRIVATE编译定义。

if(HAVE_LOG AND HAVE_EXP)
  target_compile_definitions(MathFunctions
                             PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()

如果系统提供了logexp,那么我们将在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配置项目,然后使用你选择的构建工具构建它并运行 Tutorial 可执行文件。

现在哪个函数给出了更好的结果,sqrt 还是 mysqrt?

添加自定义命令和生成的文件(步骤 6)

假设针对本教程,我们决定永远不想使用平台logexp函数,而希望生成要在mysqrt函数中使用的预计算值表。在本节中,我们将创建表作为构建过程的一部分,然后将该表编译到我们的应用程序中。

首先,让我们移除 logexp 函数在 MathFunctions/CMakeLists.txt 中的检查。然后从 mysqrt.cxx 中移除对 HAVE_LOGHAVE_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 知道 mysqrt.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;
  }

  // use the table to help find an initial value
  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)];
  }

  // do ten iterations
  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。最后,它将编译包含 Table.hmysqrt.cxx 以生成 MathFunctions 库。

运行教程可执行文件并验证它是否正在使用该表格。

构建安装程序(步骤 7)

接下来,假设我们要将项目分发给其他人以供他们使用。我们希望在各种平台上提供二进制和源分发包。这与我们在安装和测试(第 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 module

下一步是以通常的方式构建项目,然后运行 cpack 可执行文件。要从二进制目录构建二进制分发包,请运行

cpack

要指定生成器,请使用 -G 选项。对于多配置构建,请使用 -C 指定配置。例如

cpack -G ZIP -C Debug

若要创建源分发包,你应键入

cpack --config CPackSourceConfig.cmake

或者,运行 make package,或从 IDE 右键单击 Package 目标,然后选取 Build Project

运行在二进制目录中找到的安装程序。然后,运行已安装的可执行文件并验证其是否正常工作。

添加对仪表板的支持(第 8 步)

为仪表盘提交测试结果的过程很简单。我们在 测试支持 中已经为我们的项目定义了许多测试。现在,我们只需运行测试,然后将其提交给仪表盘即可。若要纳入仪表盘支持,可在顶层 CMakeLists.txt 中包含 CTest 模块。

替换

# enable testing
enable_testing()

# enable dashboard scripting
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

混合静态和共享(步骤 9)

在此部分,我们将会演示如何使用 BUILD_SHARED_LIBS 变量,以便控制 add_library 的默认行为,以及控制如何生成没有明确类型的库(STATICSHAREDMODULEOBJECT)。

要完成这些,我们添加 BUILD_SHARED_LIBS 进入顶级的 CMakeLists.txt。使用 option 命令,因为它允许用户选择该值是 ON 还是 OFF。

接下来,重构 MathFunctions,使其成为一个包含使用 mysqrtsqrt 的真实库,而不是要求调用代码执行此逻辑。这意味着 USE_MYMATH 将不再控制 MathFunctions 的构建,而是控制此库的行为。

第一步是更新 CMakeLists.txt 顶层的起始部分,使其看起来像

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# control where the static and shared libraries are built so that on windows
# we don't need to tinker with the path to run the executable
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 a header file to pass the version number only
configure_file(TutorialConfig.h.in TutorialConfig.h)

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)

既然已经让 MathFunctions 始终使用,需要更新该库的逻辑。所以,在 MathFunctions/CMakeLists.txt 中需要创建一个 SqrtLibrary,当 USE_MYMATH 启用时,该库可以有条件地构建和安装。既然这是一个教程,将明确要求 SqrtLibrary 以静态方式构建。

MathFunctions/CMakeLists.txt 的最终结果应该是这样的

# add the library that runs
add_library(MathFunctions MathFunctions.cxx)

# state that anybody linking to us needs to include the current source dir
# to find MathFunctions.h, while we don't.
target_include_directories(MathFunctions
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )

# should we use our own math functions
option(USE_MYMATH "Use tutorial provided math implementation" ON)
if(USE_MYMATH)

  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

  # first we add the executable that generates the table
  add_executable(MakeTable MakeTable.cxx)

  # add the command to generate the source code
  add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
    COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
    DEPENDS MakeTable
    )

  # library that just does sqrt
  add_library(SqrtLibrary STATIC
              mysqrt.cxx
              ${CMAKE_CURRENT_BINARY_DIR}/Table.h
              )

  # state that we depend on our binary dir to find Table.h
  target_include_directories(SqrtLibrary PRIVATE
                             ${CMAKE_CURRENT_BINARY_DIR}
                             )

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()

# define the symbol stating we are using the declspec(dllexport) when
# building on windows
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")

# install rules
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,以使用 mathfunctionsdetail 命名空间

#include <iostream>

#include "MathFunctions.h"

// include the generated table
#include "Table.h"

namespace mathfunctions {
namespace detail {
// a hack square root calculation using simple operations
double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  // use the table to help find an initial value
  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)];
  }

  // do ten iterations
  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

  1. 始终包含 MathFunctions.h

  2. 始终使用 mathfunctions::sqrt

  3. 不要包含 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

namespace mathfunctions {
double DECLSPEC sqrt(double x);
}

此时,如果构建所有内容,可能会注意到链接失败,因为我们结合了一个没有位置无关代码的静态库和一个带有位置无关代码的库。解决这个问题的方法是显式设置 SqrtLibrary 的 POSITION_INDEPENDENT_CODE 目标属性为 True,无论构建类型如何。

  # state that SqrtLibrary need PIC when the default is shared libraries
  set_target_properties(SqrtLibrary PROPERTIES
                        POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
                        )

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary)

练习:更改 MathFunctions.h 以使用 dll 导出定义。使用 CMake 文档,可以找到一个帮助模块来简化此操作吗?

添加生成器表达式(第 10 步)

生成器 表达式在生成构建系统期间经过评估,生成针对每个构建配置信息。

生成器 表达式在诸多目标属性的上下文中允许出现,如 LINK_LIBRARIESINCLUDE_DIRECTORIESCOMPILE_DEFINITIONS 等。在使用填充这些属性的命令时,可以同样使用,如 target_link_librariestarget_include_directoriestarget_compile_definitions 等。

生成器 表达式可用于启用条件链接、条件编译时定义、条件包含目录等。条件可能基于构建配置、目标属性、平台信息或任何其他可查询的信息。

有不同类型的 生成器 表达式,包括逻辑、信息和输出表达式。

逻辑表达式用来创建条件输出。基本表达式是 0 和 1 表达式。$<0:...> 导致空字符串,而 <1:...> 导致“…“的内容。它们还可以嵌套。

通常使用生成器 表达式是为了有条件地添加编译器标志,例如用于语言级别或警告的标志。一种很好的模式是将此信息与INTERFACE target 关联起来,以便传播此信息。现在让我们通过构建INTERFACE target 并指定所需 C++ 标准级别11(而不是使用CMAKE_CXX_STANDARD)来开始。

因此,以下代码

# specify the C++ standard
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,以便所有 target 都调用target_link_libraries以调用tutorial_compiler_flags

添加导出配置(步骤 11)

安装和测试(步骤 4)本教程内容中,我们增加了 CMake 安装项目库和标头的能力。在构建安装程序(步骤 7)中,我们增加了整理这些信息的能力,以便它们可以分发给其他人。

下一步是添加必要的信息,以便其他 CMake 项目可以使用我们的项目,无论是从构建目录还是从本地安装或者打包时。

第一步是更新我们的install(TARGETS)命令,不仅要指定DESTINATION,还要指定EXPORTEXPORT关键字会生成并安装一个 CMake 文件,其中包含从安装树导入安装命令中列出的所有 target 的代码。因此,让我们继续明确EXPORT MathFunctions 库,通过将MathFunctions/CMakeLists.txt中的install命令更新为如下所示

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 正在恰当打包所需的 target 信息,但我们仍需要生成 MathFunctionsConfig.cmake,以便 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)
# generate the config file that is includes the exports
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
  )
# generate the version file for the config file
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  COMPATIBILITY AnyNewerVersion
)

# install the configuration file
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,而不需要对其进行安装。

<a class="headerlink" href="#packaging-debug-and-release-step-12" title="此标题的永久链接">打包 Debug 和 Release (第 12 步)

注意:此示例对于单配置生成器有效,对于多配置生成器(例如 Visual Studio)无效。

默认情况下,CMake 模型中的一个构建目录只包含一个配置,无论是 Debug、Release、MinSizeRel,还是 RelWithDebInfo。但是,可以设置 CPack 以捆绑多个构建目录,并构建一个包含同一项目多个配置的包。

首先,我们要确保调试和发布版本构建使用不同的名称来命名将被安装的可执行文件和库。让我们使用 d 作为调试可执行文件和库的后缀。

设置位于顶层 CMakeLists.txt 文件开头附近的 CMAKE_DEBUG_POSTFIX

set(CMAKE_DEBUG_POSTFIX d)

add_library(tutorial_compiler_flags INTERFACE)

以及教程可执行文件上的 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 中,设置 VERSIONSOVERSION 属性

set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")

Step12 目录中,创建 debugrelease 子目录。布局如下

- Step12
   - debug
   - release

现在我们需要设置调试和发布版本构建。我们可以使用 CMAKE_BUILD_TYPE 来设置配置类型

cd debug
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .
cd ../release
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .

由于调试和发布版本构建现已完成,我们可以使用一个自定义配置文件将两个构建打包到一个发行版中。在 Step12 目录中,创建一个名为 MultiCPackConfig.cmake 的文件。在此文件中,首先包含由 cmake 可执行文件创建的默认配置文件。

接下来,使用 CPACK_INSTALL_CMAKE_PROJECTS 变量来指定要安装哪个项目。在这种情况下,我们希望安装调试和发布两个版本。

include("release/CPackConfig.cmake")

set(CPACK_INSTALL_CMAKE_PROJECTS
    "debug;Tutorial;ALL;/"
    "release;Tutorial;ALL;/"
    )

Step12 目录中,使用 config 选项运行 cpack 来指定我们的自定义配置文件

cpack --config MultiCPackConfig.cmake