UP | HOME

CMake Tutorial

目录

CMake 教程

基本起点(步骤 1)

最基本的项目是从源代码文件构建的可执行文件。对于简单的项目,只需要 3 行 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 支持大写,小写和大小写混合命令。Step1 目录中提供了的 tutorial.cxx 源代码,并可用于计算数字平方根。

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

我们将添加的第一个特性是给我们的执行文件和项目提供一个版本号。虽然我们可以仅在与源代码中执行此操作,但是使用 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)

由于配置文件将被写入 binary 树,我们必须将该目录添加到搜索路径列表中来包含文件。添加下述行到 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 中用 std::stod 替换 atof 来添加一些 C++11 特性到我们的项目中。同时,我们移除 #include <cstdlib>

const double inputValue = std::stod(argv[1]);

我们需要在 CMake 代码中明确声明应该使用正确的 flags。最简单的办法是通过使用 CMAKE_CXX_STANDARD 变量来在 CMake 中开启指定的 C++标准支持。对本教程来说,在 CMakeLists.txt 文件中设置 CMAKE_CXX_STANDARD 变量为 11 并设置 CMAKE_CXX_STANDARD_REQUIRED 为 True。

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
cd Step1_build
cmake ../Step1
cmake --build .

导航到教程的构建目录(可能是 make 目录或 Debug 目录或 Release 构建配置子目录)并运行这些命令:

Tutorial 4294967296
Tutorial 10
Tutorial

添加库(Step2)

现在我们添加库到我们的项目。该库将包含我们自己的计算数字的平方根的实现。然后可执行文件使用此库而不是编译器提供的标准平方根函数。在本教程中,我们将库放入名为 MathFunctions 的子目录中。该目录已经包含一个头文件, MathFunctions.h ,和源文件 mysqrt.cxx 。源文件有一个名为 mysqrt 的函数,该函数提供与编译器的 sqrt 函数类似的功能。

添加以下一行 CMakeLists.txt 文件到 MatchFunctions 目录中:

add_library(MathFunctions mysqrt.cxx)

为了使用新库,我们将在顶层 CMakeLists.txt 文件中添加一个 add_subdirectory() 以便构建该库。我们将新库添加到可执行文件,并将 MathFunctions 添加到 include 目录,以便可以找到 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-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 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 ,如果我们需要它则 include MatchFunctions.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 可执行文件。

使用 ccmake 可执行文件或 cmake-gui 来更新 USE_MYMATH 的值。重新构建并再次运行教程。哪个函数提供更好的结果,sqrt 或 mysqrt?

添加库的使用要求(Step 3)

使用要求可以更好的控制库或可执行文件的链接,同时 include 行还可以更好的控制 CMake 内部目标的传递属性。利用使用要求的主要命令是:

  • target_compile_definitions()
  • target_compile_options()
  • target_include_directories()
  • target_link_libraries()

让我们使用现代 CMake 的使用要求方法来重构 添加库(Step2)中的代码。我们首先声明链接到 MathFunctions 的任何人都需要 include 当前源目录,而 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 .

安装和测试(Step 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 命令的 install 选项(在 3.15 中引入,较早版本的 CMake 必须使用 make install )运行安装步骤,或者从 IDE 构建 INSTALL 目标。这将会安装适当的头文件,库和可执行文件。

CMake 变量 CMAKE_INSTALL_PREFIX 用于确定文件的安装根目录。如果使用 cmake --install ,则可以通过 --prefix 参数指定自定义安装目录。对于多配置文件工具,使用 --config 参数指定配置文件。

确认已安装的 Tutorial 运行。

测试支持

接下来让我们测试我们的应用。在顶层 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")

第一个测试只是简单地验证程序运行,没有段错误或其他崩溃,并且返回值为 0。这是 CTest 的基本形式。

下一个测试使用 PASS_REGULAR_EXPRESSION 测试属性来验证测试输出包含特定的字符串。在这种情况下,验证在提供了错误数量的参数时是否打印了 usage 信息。

最后,我们有一个名为 do_test 的函数,该函数运行应用并且验证给定输入的计算平方根的结果是否正确。对于 do_test 的每次调用,都会根据传递的参数将另一个带有名称、输入和预期结果的测试添加项目中。

重新构建应用程序,然后切换到二进制目录并运行 ctest 可执行文件: ctest -Nctest -VV 。对于多配置生成器(例如 Visual Studio),必须指定配置类型。例如,要在调试模式下运行测试,在构建目录(而不是 Debug 子目录)使用 ctest -C Debug -VV 。或者,从 IDE 构建 RUN_TEST 目标。

添加系统自检(Step 5)

让我们考虑添加一些取决于目标平台可能不具有的特性的代码到项目中。例如,我们添加一些取决于目标平台是否具有 logexp 函数的代码。当然几乎所有的平台都有这些函数,但是假设本教程中他们并不常见。

如果平台有 logexp ,那么我们将在 mysqrt 函数中使用它们来计算平方根。我们首先在顶层 CMakeLists.txt 中使用 CheckSymbolExists 模块测试这些函数是否可用。在一些平台,我们需要链接到 m 库。如果最初没有找到 logexp ,则 require m 库并且重试。

我们将在 TutorialConfig.h.in 中使用新定义,因此确保在文件被配置之前设置它们。

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()

现在让我们添加这些定义到 TutorialConfig.h.in 以便在 mysqrt.cxx 中使用它们。

// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

如果 logexp 在系统上可用,那么我们将使用它们在 mysqrt 函数中计算平方根。添加下列代码到 MatchFunctions/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 来 include cmath

#include <cmath>

运行 cmkae 可执行文件或 cmake-gui 配置项目,然后使用所选的构建工具对其进行构建并运行 Tutorial 可执行文件。

你会发现我们没有使用 logexp ,即使我们认为它们应该可用。我们应该很快发现,我们忘记在 mysqrt.cxx 中 include TutorialConfig.h 了。

我们还需要更新 MatchFunctions/CMakeLists.txt 以便 mysqrt.cxx 知道此文件的位置:

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          PRIVATE ${CMAKE_BINARY_DIR}
          )

进行此次更新之后,继续并再次构建项目,然后运行构建的 TuTorial 可执行文件。如果 logexp 仍旧不能被使用,从构建目录中打开生成的 TutorialConfig.h 。或许它们在当前系统不可用?

现在哪个函数给你更好的结果,sqrt 或 mysqrt?

指定编译定义

除了在 TutorialConfig.h 中保存 HAVE_LOGHAVE_EXP 值,我们还有更好的地方吗?让我们试试使用 target_compile_definitions()。首先,从 TutorialConfig.h.in 中删除定义。我们不再需要在 mysqrt.cxx 中 include TutorialConfig.h 或在 MatchFunctions/CMakeLists.txt 中 extra include 了。

接下来,我们可以将 HAVE_LOGHAVE_EXP 的检查移至 MathFunctions/CMakeLists.txt ,然后将这些值指定为 PRIVATE 编译定义。

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()

# add compile definitions
if(HAVE_LOG AND HAVE_EXP)
  target_compile_definitions(MathFunctions
                             PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()

完成这些更新后,继续并重新构建项目。运行构建的 Tutorial 可执行文件并像本步骤之前的那样验证结果。

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

假设,出于本教程的目的,我们决定不再使用 logexp 函数,而是希望生成一个可在 mysqrt 函数中使用的预计算值表。在本节中,我们将在构建过程中创建表,然后将该表编译到我们的应用程序中。

首先,我们从 MathFunctions/CMakeLists.txt 中移除 logexp 函数的检查。然后从 mysqrt.cxx 中移除 HAVE_LOGHAVE_EXP 的检查。同时,我们还可以移除 #include <math>

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
            )

我们还必须将当前 binary 目录添加到 include 目录列表中,以便 mysqrt.cxx 可以找到并 include Table.h

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
          )

现在,让我们来生成表。首先,修改 mysqrt.cxx 来 include 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 。最后,它将编译 include Table.hmysqrt.cxx 来生成 MathFunctions 库。

运行 Tutorial 可执行文件并验证它正在使用该表。

构建安装器(Step 7)

接下来,假设我们想将项目分发给其他人,以便他们可以使用它。我们希望在各种平台上提供二进制和源码的分发。这与我们之前在安装和测试(Step 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)

这就是全部。我从 include InstallRequiredSystemLibraries 开始。该模块将包括项目目前平台所需的任何运行时库。接下来,我们将一些 CPack 变量设置为我们已存储的该项目的许可证和版本信息。版本信息是在本教程前面设置的, liscense.txt 在此步骤已被包含在顶层源目录中。

最后,我们 include CPack 模块,它将使用这些变量和当前系统的一些其他属性来设置安装器。

下一步是按照通常的方法构建项目,然后运行 cpack 可执行文件。要构建二进制发行版,从二进制目录运行:

cpack

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

cpack -G ZIP -C Debug

要创建源分发,你可以输入:

cpack --config CPackSourceConfig.cmake

或者,运行 make package 或从 IDE 中右击 Package 目标和 Build Project

运行二进制目录中找到的安装器。然后运行安装的可执行文件并验证它是否工作。

添加对 Dashboard 的支持(Step 8)

对将测试结果提交到 Dashboard 的支持非常简单。我们已经在测试支持中为我们的项目添加了许多测试。现在我们只要运行这些测试并将其提交到 Dashboard 即可。为了包含对 Dashboard 的支持,我们在顶层 CMakeLists.txt 中 include 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 可执行文件将会在它运行时读取该文件。要创建一个简单的 Dashboard 你可以运行 cmake 可执行文件或 cmake-gui 来配置项目,但暂时不要构建它。而是切换目录到二进制树,然后运行:

ctest [-VV] -D Experimental

记住,对于多配置生成器(例如 Visual Studio),必须指定配置类型:

ctest [-VV] -C Debug -D Experimental

或,从 IDE,构建 Experimental 目标。

ctest 可执行文件将会构建、测试并提交结果到 Kitware 的公共 dashboard:https://my.cdash.org/index.php?project=CMakeTutorial

混合 Static 和 Shared(Step 9)

在本节中,我们将展示如何使用 BUILD_SHARED_LIBS 变量来控制 add_library()的默认行为,并允许控制如何构建没有显式类型( STATICSHAREDMODULEOBJECT )的库。

为此,我们需要将 BUILD_SHARED_LIBS 添加到顶层 CMakeLists.txt 。我们使用 option()命令,因为它允许用户有选择选择该值应为 ON 还是 OFF。

接下来,我们将重构 MathFunctions 使其成为使用 mysqrt 或 sqrt 封装的真实库,而不是要求调用代码执行此逻辑。这也意味着 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 始终被使用,我们将需要更新该库的逻辑。因此,在 MatchFunctions/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
install(TARGETS MathFunctions 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. 总是 include MathFunctions.h
  2. 总是使用 mathfunctions::sqrt
  3. 不要 include 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 文档,你可以找到一个帮助模块来简化此过程吗?

添加生成器表达式(Step 10)

在构建系统生成期间会评估生成器表达式,以生成特定于每个构建配置的信息。

生成器表达式在许多目标属性的上下文中被允许,例如 LINK_LIBRARIESINCLUDE_DIRECTORIESCOMPILE_DEFINITIONS 等等。在使用命令填充这些属性时,也可以使用它们,例如 target_link_libraries()target_include_directories()target_compile_definitions() 等等。

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

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

逻辑表达式用于创建条件输出。基本表达式是 0 和 1 的表达式。 A$<0:...> 结果是空字符串, <1:...> 导致内容“…”。他们也可以被嵌套。

生成表达式的常见用法是有条件地添加编译 flags,例如用于语言级别或警告。一个不错的模式是将该信息与一个 INTERFACE 目标关联,以允许该信息传播。让我们从构造一个 INTERFACE 目标并指定所需的 C++标准 11 开始,而不是使用 CMAKE_CXX_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)

接下来,我们为项目添加所需的编译器警告 flags。由于警告 flags 因编译器而异,因此我们使用 COMPILE_LANG_AND_ID 生成器表达式来控制在给定一种语言和一组编译 ID 的情况下应应用的 flags,如下所示:

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>>"
)

查看此内容,我们看到警告 flags 封装在 BUILD_INTERFACE 条件内。这样做是为了使我们已安装项目的使用者不会继承我们的警告 flags。

练习 :修改 MathFunctions/CMakeLists.txt ,使所有有 target_link_libraries 的目标调用 tutorial_compiler_flags

添加导出配置(Step 11)

在教程的安装和测试(Step 4)中,我们添加了 CMake 的安装项目的库和头文件的功能。在构建安装器(Step 7)中,我们添加了打包此信息的功能,以便可以分发给其他人。

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

第一步是更新我们的 install(TARGETS) 命令,不仅要指定 DESTINATION ,还要指定 EXPORTEXPORT 关键字生成并安装一个 CMake 文件,该文件包含从安装树目录中导入 install 命令中列出的所有目标的代码。所以让我们继续并通过更新 MathFunctions/CMakeLists.txt 中的 install 命令,来显式 EXPORT MathFunctions 库,如下所示:

install(TARGETS MathFunctions tutorial_compiler_flags
        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 所需的目标信息,但仍需要生成 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 由其他项目使用,而无需安装它。

打包 Debug 和 Release(Step 12)

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

默认情况下,CMake 的模型是构建目录仅包含单个配置,可以是 Debug,Release,MinSizeRel 或 RelWithDebugInfo。但是,可以将 CPack 设置为捆绑多个构建目录,并构建一个包含同一项目的多个配置的包。

首先,我们要确保 debug 版本和 release 版对要安装的可执行文件和库使用不同的名称。让我们使用 d 作为可知性文件和库的后缀。

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

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 中,设置 VERSIONSOVERSION 属性:

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

在 Step 12 目录中,创建 debugrelease 子目录。布局如下所示:

- 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 构建都已完成,我们可以使用自定义配置文件将两个版本打包到单一 release 中。在 Step 12 目录,创建一个名为 MultiCPackConfig.cmake 的文件。在该文件中,首先 include 由 cmake 可执行文件创建的默认配置文件。

接下来,使用 CPACK_INSTALL_CMAKE_PROJECTS 变量指定要安装的项目。在此例中,我们想要安装 debug 和 release。

include("release/CPackConfig.cmake")

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

Step 12 目录,运行 cpack 使用 config 选项指定我们自定义的配置文件。

cpack --config MultiCPackConfig.cmake

作者: Petrus.Z

Created: 2021-09-01 Wed 00:38