CMake教程

CMake教程CMake Tutorial

本教程一步一步的介绍了CMake帮助文档中常见的构建系统内容。通过一个样例工程解析各种内容如何协同工作,非常有助于理解CMake。教程文档和示例的源代码可以在CMake源代码树的Help/guide/tutorial目录中找到。每个步骤对应一个以该步骤命名的目录,教程内容都是以这些目录内的文件为基础开始的。本教程示例是渐进式的,因此每一步都为前一步提供完整的解决方案。

第1步 基本起始点 A Basic Starting Point (Step 1)

最基本的项目是从源文件构建可执行文件。对于简单的项目,只需要一个三行CMakeLists.txt文件(以下省略"文件"两个字)。本教程从这里开始。在Step1目录中创建一个CMakeLists.txt,内容如下:

cmake_minimum_required(VERSION 3.10)

# 设置项目名称
project(Tutorial)

# 生成可执行文件
add_executable(Tutorial tutorial.cxx)

注意,本例在CMakeLists.txt中使用了小写命令,CMake支持大写、小写和混合大小写命令。Step1目录中提供了tutorial.cxx的源代码,它用来计算一个数字的平方根。

1.1 添加项目版本号和配置头文件

我们要使用的第一个CMake功能是为执行文件和项目添加一个版本号。虽然可以直接在源代码中这样做,但是在CMakeLists.txt中配置更加灵活。

首先,修改CMakeLists.txt,使用project()命令设置项目名称和版本号

cmake_minimum_required(VERSION 3.10)

# 设置项目名称和版本号
project(Tutorial VERSION 1.0)

然后, 配置一个头文件,用于将版本号传递给源代码:

configure_file(TutorialConfig.h.in TutorialConfig.h)

由于配置的文件会生成在编译目录(${PROJECT_BINARY_DIR}),我们需要将该目录添加到头文件搜索路径列表中。在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) {
    // 输出版本号
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
              << Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

1.2 指定C++标准

接下来,我们为项目增加一点C++11特性。我们将tutorial.cxx中的atof替换为std::stod,同时,删除#include <cstdlib>

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

为了让CMake能够使用正确的标志,我们需要在CMake代码中显式地声明相应的标志。在CMake中支持特定C++标准的最简单方法是使用CMAKE_CXX_STANDARD变量。在本教程中,将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)

# add the executable
add_executable(Tutorial tutorial.cxx)

1.3 构建和测试

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

例如,我们可以从命令行导航到CMake源代码树的Help/guide/tutorial/Step1目录,并运行以下命令:

$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .

运行编译结果目录(可能是make目录或Debug或Release子目录)的执行程序,win10示例如下:

PS build> Debug\Tutorial.exe 4294967296
The square root of 4.29497e+09 is 65536
PS build> Debug\Tutorial.exe 10
The square root of 10 is 3.16228
PS build> Debug\Tutorial.exe
xxx\Debug\Tutorial.exe number

第2步 生成库 Adding a Library (Step 2)

现在,我们将在项目中添加一个包含计算平方根功能的库。最终生成的可执行文件可以使用这个库而不是使用标准数学库中的函数。

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

将下一行添加到MathFunctions目录下的CMakeLists.txt:

add_library(MathFunctions mysqrt.cxx)

为了利用这个新库,我们需要在顶层CMakeLists.txt中添加add_subdirectory()命令,该命令会构建MathFunctions库。接着,我们将新库添加到可执行文件中,并将MathFunctions添加到头文件搜索路径,以便可以找到mqsqrt.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可执行文件(参见第一步)。

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

第3步 为库添加使用要求 Adding Usage Requirements for Library (Step 3)

使用要求对库或可执行文件的链接和包含(link and include)提供了更好的控制,也对CMake内传递目标属性更加可控。影响使用要求的主要命令有:

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

让我们用新式CMake中的使用要求来重构《添加库》(第二步)。首先要说明一下,任何链接到MathFunctions的人都需要包含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步 安装和测试 Installing and Testing (Step 4)

现在,我们将开始向项目添加安装规则和测试支持。

4.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
  )

这就是创建基本的tutorial本地安装需要做的全部工作。

运行cmake可执行文件或cmake-gui来配置项目,然后使用您选择的构建工具来构建它。在命令行中使用cmake命令的install选项(在3.15中引入,较老的cmake版本必须使用make install)来运行安装步骤,或者从IDE构建INSTALL目标。这将安装相应的头文件、库和可执行文件。

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

验证安装的Tutorial程序是否运行。

win10各参数命令行示例如下:

PS > cmake --install . --prefix D:\Temp2\ttt\debug --config debug
根据cmake_install.cmake文件及目录下项目文件安装

4.2 测试支持 Testing Support

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

第一个测试只是验证应用程序能够运行,不会发生断错误(segfault)或其他崩溃,并且返回0。这是CTest中test的基本形式。

下一个测试使用PASS_REGULAR_EXPRESSION测试属性来验证测试的输出是否包含某些字符串。在本例中,验证当输入错误的参数数量时应用程序能够输出使用说明。

最后,我们定义一个名为do_test的函数,该函数运行应用程序并且验证输出的平方根与给定的结果相同。每次do_test调用,该函数都会基于调用参数向项目添加一个带名称、输入和预期结果的测试。

重新构建应用程序,然后将切换(cd)到执行文件目录中,并运行ctest可执行文件:ctest -Nctest -VV

对于多配置生成器(例如Visual Studio),必须指定配置类型。例如,要在调试模式下运行测试,请在构建目录(不是调试子目录!)下执行ctest -C Debug -VV。或者,在IDE中构建RUN_TESTS目标。

第5步 添加系统自检 Adding System Introspection (Step 5)

让我们在源码中添加一些目标平台可能不支持的特性。对于本例,我们将添加一些代码,同时会根据目标平台是否支持logexp函数来选择是否包含这些代码。当然,几乎每个平台都有这些函数,但是本教程假设它们并不常见。

如果平台支持logexp函数,那么我们将在mysqrt函数中使用它们来计算平方根。我们首先在顶层CMakeLists.txt中使用CheckSymbolExists模块来检查这些函数的可用性。因为我们将在TutorialConfig.h.in中使用新定义,因此,一定要在文件配置之前设置它们。

include(CheckSymbolExists)
set(CMAKE_REQUIRED_LIBRARIES "m")
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)

现在让我们将这些定义添加到TutorialConfig.h.in中,这样我们就可以从mysqrt.cxx中使用它们:

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

如果系统支持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;
#endif

我们还需要修改mysqrt.cxx以包含cmath.

#include <cmath>

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

您将注意到,我们没有使用logexp,即使我们认为它们应该可用。我们应该很快意识到,我们忘了在mysqrt.cxx中包含TutorialConfig.h

我们还需要更新MathFunctions/CMakeLists.txt,以便让mysqrt.cxx知道这个文件的位置:

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

进行此更新后,继续重新构建项目并运行构建的教程可执行文件。如果仍然没有使用logexp,那么在构建目录中查看生成的TutorialConfig.h文件,也许它们在当前的系统中不可用?

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

指定编译时宏定义 Specify Compile Definition

除了在TutorialConfig.h中保存HAVE_LOGHAVE_EXP值,还有更好的地方吗? 让我们尝试使用target_compile_definitions()

首先,从TutorialConfig.h.in中删除这些定义。我们不再需要在mysqrt.cxx中包含TutorialConfig.h以及在MathFunctions/CMakeLists.txt中包含额外的头文件路径。

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

include(CheckSymbolExists)
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_compile_definitions(MathFunctions
                             PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()

完成这些更新之后,重新构建项目。运行构建的Tutorial可执行文件,并验证结果是否与本步骤前面的结果相同。

第6步 添加自定义命令以及生成文件 Adding a Custom Command and Generated File (Step 6)

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

首先,让我们删除MathFunctions/CMakeLists.txt中对logexp函数的检查。然后从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。通过在MathFunctions的源文件列表中添加Table.h来实现。

add_library(MathFunctions
            mysqrt.cxx
            ${CMAKE_CURRENT_BINARY_DIR}/Table.h
            )

我们还必须将当前的二进制目录添加到头文件包含路径中,以便mysqrt.cxx能够找到并包含Table.h

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;最后,它将编译mysqrt.cxx,其中包含Table.h来生成MathFunctions库。

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

第7步 构建安装程序 Building an Installer (Step 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模块,它将使用这些变量和当前系统的一些其他属性来设置安装程序。

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

cpack

要指定生成器,请使用-G选项。对于多配置构建,使用-C来指定配置。例如:
To specify the generator, use the -G option. For multi-config builds, use -C to specify the configuration.

cpack -G ZIP -C Debug

要创建一个源发行版,你需要输入:

cpack --config CPackSourceConfig.cmake

或者,运行make package或右键单击Package 目标并从IDE构建项目。

VS generator 注意:目录不能含有中文
$ cmake --build . --target PACKAGE --config Release[Debug]

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

注: 目标包和package_source

  1. 如果CMake是用Makefile、Ninja或Xcode生成器运行的,那么include(CPack)将生成一个目标package,这样,可以通过CMake、Make或Ninja命令来构建二进制安装程序(cmake --build . --target package or make package or ninja package)来代替cpack。
  2. VS生成器创建一个大写目标PACKAGE
  3. 如果CMake是用Makefile或Ninja生成器运行的,那么include(CPack)还会生成一个目标package_source。要构建一个源包,可以调用cmake --build . --target package_source, make package_source, or ninja package_source.来替代cpack -G TGZ --config CPackConfig.cmake

第8步 添加对仪表板的支持 Adding Support for a Dashboard (Step 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步 混合静态和共享库 Mixing Static and Shared (Step 9)

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

为此,我们需要将BUILD_SHARED_LIBS添加到顶层CMakeLists.txt中。我们使用option()命令,它允许用户在ONOFF值中选择。

接下来,我们将把MathFunctions重构为一个真正的库,它使用mysqrtsqrt进行封装,而不需要调用代码来执行此逻辑。这也意味着USE_MYMATH将不控制构建mathfunction,而是控制库的行为。

第一步是将顶层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
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. 总是包含MathFunctions.h
  2. 总是使用mathfunctions::sqrt
  3. 不包括cmath

最后,更新MathFunctions/MathFunctions.h以使用dll导出(dll export)定义:

#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);
}

此时,如果您构建所有内容,您将注意到链接(linking)失败了,因为我们正在将一个没有位置无关代码的静态库与一个具有位置无关代码的库组合在一起。解决方案是显式地将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步 添加生成器表达式 Adding Generator Expressions (Step 10)

生成器表达式在构建过程中计算,以生成特定于每个构建配置的信息。

生成器表达式可以在许多目标属性的上下文中使用,比如LINK_LIBRARIESINCLUDE_DIRECTORYCOMPILE_DEFINITION等。他们也可以用在给这些属性添加值的命令上,比如:target_link_libraries()target_include_directory()target_compile_definitions()等。

生成器表达式可用于条件链接、条件宏定义、条件头文件路径等。这些条件可能基于构建配置、目标属性、平台信息或其他可查询信息。

有不同类型的生成器表达式,包括:逻辑表达式、信息表达式和输出表达式(Logical, Informational, and Output expressions)。

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

生成器表达式通常用来按需添加编译标志,例如针对语言级别或警告的标志。一个好模式是把该信息关联到INTERFACE目标,以允许该信息传播。让我们首先构造一个INTERFACE目标,并指定所需的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_link_libraries()调用tutorial_compiler_flags

第11步 添加导出配置 Adding Export Configuration (Step 11)

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

下一步是添加必要的信息,以便其他CMake项目可以通过构建目录、本地安装或安装包来使用我们的项目。

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

install(TARGETS MathFunctions tutorial_compiler_flags
        DESTINATION lib
        EXPORT MathFunctionsTargets)
install(FILES MathFunctions.h DESTINATION include)

现在, 我们已经导出了mathfunction,我们还需要显式地安装生成的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(),使它明白在构建目录内和install / package中,需要不同的INTERFACE位置。这意味着将mathfunction的target_include_directories()调用转换为如下形式:

target_include_directories(MathFunctions
                           INTERFACE
                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                            $<INSTALL_INTERFACE:include>
                           )

一旦更新这个,我们可以重新运行CMake并验证它不再发出警告。

此时,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可以被其他项目使用,而不需要安装它。

第12步 打包调试版和发布版 Packaging Debug and Release (Step 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)

以及可执行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")

Step12目录中,创建build\debugbuild\release子目录。目录层次看起来像:

- Step12
  - build
    - debug
    - release

现在我们需要生成debugrelease版。我们可以使用CMAKE_BUILD_TYPE来设置配置类型:

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

现在debugrelease版构建都完成了,我们可以使用一个自定义配置文件将两个构建打包到一个单独的发布中。在Step12目录中,创建一个名为MultiCPackConfig.cmake的文件。在这个文件中,首先包含由cmake可执行文件创建的默认配置文件。

接下来,使用CPACK_INSTALL_CMAKE_PROJECTS变量来指定安装哪些项目。在本例中,我们希望同时安装debug和release。

include("build/release/CPackConfig.cmake")

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

Step12\build目录运行cpack,并使用config选项指定我们的自定义配置文件:

cpack --config ../MultiCPackConfig.cmake

备注

MAC配置cmake

brew install cmake
REM ln –s 源文件 目标文件
sudo ln -s /usr/local/Cellar/cmake/3.14.4/bin/cmake /usr/local/bin/cmake

参考:

posted @ 2020-05-08 16:22  一花一世界,一叶一乾坤  阅读(1780)  评论(1编辑  收藏  举报