CMAKE 教程与构建系统

 

CMake教程提供了一个逐步的指南,涵盖了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中用std::stod替换atof,为我们的项目添加一些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添加为包含目录,这样就可以找到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-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中,如果我们需要的话,就把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哪个函数的结果更好?

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

使用要求允许更好地控制库或可执行文件的链接和包含,同时也给予 CMake 内部目标的转义属性更多的控制。利用使用要求的主要命令有:

让我们从添加库(步骤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)来运行安装步骤。对于多配置工具,不要忘记使用—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")

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

下一个测试利用PASS_REGULAR_EXPRESSION测试属性来验证测试的输出是否包含某些字符串。在这种情况下,验证当提供的参数数量不正确时,是否会打印出使用信息。

最后,我们有一个名为do_test的函数,它运行应用程序并验证给定输入的计算平方根是否正确。每调用一次do_test,就会在项目中添加一个测试,包括名称、输入和基于传递的参数的预期结果。

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

增加系统反省(步骤5)

让我们考虑在我们的项目中添加一些代码,这些代码取决于目标平台可能没有的功能。在这个例子中,我们将添加一些代码,这些代码取决于目标平台是否有log和exp函数。当然,几乎每个平台都有这些功能,但在本教程中,假设它们并不常见。

如果平台有log和exp,那么我们将使用它们来计算mysqrt函数中的平方根。我们首先使用顶层CMakeLists.txt中的CheckSymbolExists模块来测试这些函数是否可用。在某些平台上,我们需要链接到m库。如果最初没有找到 log 和 exp,则需要 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

如果系统上有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 来配置项目,然后用你选择的构建工具来构建它,并运行 Tutorial 可执行文件。

你会注意到我们没有使用log和exp,即使我们认为它们应该是可用的。我们应该很快意识到,我们已经忘记在mysqrt.cxx中包含TutorialConfig.h。

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

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

做完这个更新后,再去构建项目,并运行构建的Tutorial可执行文件。如果仍然没有使用log和exp,请打开构建目录中生成的TutorialConfig.h文件。也许它们在当前系统中不可用?

sqrt和mysqrt哪个函数现在给出的结果更好?

指定编译定义

除了在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)
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可执行文件,并验证结果是否与前面这一步相同。

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

假设,为了本教程的目的,我们决定永远不要使用log和exp函数,而是想生成一个预计算值的表,以便在mysqrt函数中使用。在本节中,我们将创建该表作为构建过程的一部分,然后将该表 编译到我们的应用程序中。

首先,让我们删除MathFunctions/CMakeLists.txt中对log和exp函数的检查。然后从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
            )

我们还要将当前的二进制目录添加到include目录列表中,这样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.h的mysqrt.cxx以产生MathFunctions库。

运行Tutorial可执行文件并验证它是否使用了表。

建立一个安装程序(步骤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变量,将这个项目的许可证和版本信息存储在那里。版本信息在本教程的前面已经设置好了,许可证.txt已经包含在这一步的顶层源目录中。

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

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

cpack

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

cpack -G ZIP -C Debug

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

cpack --config CPackSourceConfig.cmake

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

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

添加对Dashboard的支持(步骤8)

添加支持将我们的测试结果提交到Dashboard很简单。我们已经在测试支持中为我们的项目定义了一些测试。现在我们只需要运行这些测试并将它们提交到仪表板。为了包含对仪表盘的支持,我们在顶层的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来配置项目,但先不要构建它。相反,将目录改为二进制树(binray tree),然后运行。

ctest [-VV] -D Experimental

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

ctest [-VV] -C Debug -D Experimental

或者,从IDE中构建实验目标。

ctest可执行文件将构建和测试项目,并将结果提交到Kitware的公共仪表板:https://my.cdash.org/index.php?project=CMakeTutorial。

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

在这一节中,我们将展示如何使用BUILD_SHARED_LIBS变量来控制add_library()的默认行为,并允许控制没有明确类型(STATIC, SHARED, MODULE或OBJECT)的库的构建方式。

为了达到 这个目的,我们需要在顶层的CMakeLists.txt中添加BUILD_SHARED_LIBS。我们使用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 始终被使用,我们就需要更新该库的逻辑。因此, 在 MathFunctions/CMakeLists.txt 中, 我们需要创建一个 SqrtLibrary, 当USE_MYMATH 启用时, 这个 SqrtLibrary 将有条件地被构建和安装。由于这是一个教程, 我们将明确要求静态地构建 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,使用 mathfunctions 和 detail 命名空间:

#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_libraries()target_include_directories()target_compile_definitions()等。

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

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

逻辑表达式用于创建条件输出。基本的表达式是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)

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

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

第一步是更新我们的 install(TARGETS) 命令,不仅指定 DESTINATION,而且指定 EXPORT。EXPORT关键字会生成并安装一个CMake文件,该文件包含了从安装树中导入安装命令中列出的所有目标的代码。因此,让我们继续通过更新 MathFunctions/CMakeLists.txt 中的安装命令来显式地 EXPORT 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,以便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)

注意:这个例子对单配置生成器有效,对多配置生成器(如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中,设置VERSION和SOVERSION属性:

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

从Step12目录中,创建调试和发布子目录。布局将是这样的:

- 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目录下,运行cpack,用config选项指定我们的自定义配置文件:

cpack --config MultiCPackConfig.cmake

一个基于CMake的构建系统被组织成一组高级逻辑目标。每个目标对应于一个可执行文件或库,或者是一个包含自定义命令的自定义目标。目标之间的依赖性在构建系统中被表达出来,以确定构建顺序和响应变化的再生规则。

二进制目标

可执行文件和库使用add_executable()add_library()命令来定义。生成的二进制文件有适用于目标平台适当的PREFIX、SUFFIX和扩展名。二进制目标之间的依赖关系使用 target_link_libraries() 命令来表示。

add_library(archive archive.cpp zip.cpp lzma.cpp)
add_executable(zipapp zipapp.cpp)
target_link_libraries(zipapp archive)

archive被定义为STATIC库–一个包含从archive.cpp、zip.cpp和lzma.cpp编译而来的对象的存档。zipapp被定义为一个由编译和链接zipapp.cpp形成的可执行文件。当链接 zipapp 可执行文件时,会将 archive 静态库链接进来。

二进制可执行文件

add_executable()命令定义了一个可执行目标:

add_executable(mytool mytool.cpp)

诸如add_custom_command()这样的命令,会在构建时生成要运行的规则,可以透明地将一个EXECUTABLE目标作为COMMAND可执行文件。构建系统规则将确保在尝试运行命令之前,可执行文件已经构建完成。

二进制库类型

普通库

默认情况下,除非指定了类型,否则 add_library() 命令会定义一个静态库。当使用该命令时,可以指定一个类型。

add_library(archive SHARED archive.cpp zip.cpp lzma.cpp)
add_library(archive STATIC archive.cpp zip.cpp lzma.cpp)

BUILD_SHARED_LIBS变量可以被启用,以改变add_library()的行为,使其默认为构建共享库。

在整个构建系统定义的上下文中,特定的库是SHARED还是STATIC是无关紧要的–无论库的类型如何,命令、依赖性规范和其他API的工作都是类似的。

MODULE库类型的不同之处在于,它通常不被链接,因为在target_link_libraries()命令的右侧没有使用它。它是一个使用运行时技术作为插件加载的类型。

如果库中没有导出任何未管理的符号(如Windows资源DLL、C++/CLI DLL),则要求该库不是SHARED库,因为CMake希望SHARED库至少导出一个符号。

add_library(archive MODULE 7z.cpp)
苹果架构

SHARED 库可以用 FRAMEWORK 目标属性标记,以创建 macOS 或 iOS Framework Bundle。具有 FRAMEWORK 目标属性的库还应设置 FRAMEWORK_VERSION 目标属性。根据 macOS 惯例,此属性通常被设置为 “A “值。MACOSX_FRAMEWORK_IDENTIFIER设置了CFBundleIdentifier键,它可以唯一地标识捆绑包。

add_library(MyFramework SHARED MyFramework.cpp)
set_target_properties(MyFramework PROPERTIES
  FRAMEWORK TRUE
  FRAMEWORK_VERSION A # Version "A" is macOS convention
  MACOSX_FRAMEWORK_IDENTIFIER org.cmake.MyFramework
)

对象库

OBJECT库类型定义了编译给定源文件后产生的非存档的对象文件集合。对象文件集合可以通过使用语法$<TARGET_OBJECTS:name>作为其他目标的源输入。这是一个生成器表达式,可以用来向其他目标提供OBJECT库内容。

add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)
add_library(archiveExtras STATIC $<TARGET_OBJECTS:archive> extras.cpp)
add_executable(test_exe $<TARGET_OBJECTS:archive> test.cpp)

这些其他目标的链接(或归档)步骤除了使用来自自己来源的对象文件集合外,还将使用对象文件集合。

另外,对象库也可以链接到其他目标中。

add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)

add_library(archiveExtras STATIC extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)

add_executable(test_exe test.cpp)
target_link_libraries(test_exe archive)

这些其他目标的链接(或归档)步骤将使用直接链接的OBJECT库的对象文件。此外,当在这些其他目标中编译源文件时,OBJECT库的使用要求将被尊重。此外,这些使用要求将被传播到其他目标的依赖者身上。

在使用add_custom_command(TARGET)命令签名时,对象库不能被用作TARGET。然而,对象列表可以通过使用$<TARGET_OBJECTS:objlib>add_custom_command(OUTPUT)file(GENERATE)使用。

构建规格和使用必需件

target_include_directories()target_compile_definitions()target_compile_options()命令指定了二进制目标的构建规范和使用要求。这些命令分别填充 INCLUDE_DIRECTORIESCOMPILE_DEFINITIONSCOMPILE_OPTIONS 目标属性,和/或 INTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONSINTERFACE_COMPILE_OPTIONS 目标属性。

每条命令都有PRIVATE、PUBLIC和INTERFACE模式。PRIVATE模式只填充目标属性的non-INTERFACE_变体,INTERFACE模式只填充INTERFACE_变体。PUBLIC 模式则会填充各自目标属性的两个变体。每条命令可以通过多次使用每个关键字来调用。

target_compile_definitions(archive
  PRIVATE BUILDING_WITH_LZMA
  INTERFACE USING_ARCHIVE_LIB
)

需要注意的是,使用必需件(usage requirements)并不仅仅是为了方便地让下游使用特定的COMPILE_OPTIONSCOMPILE_DEFINITIONS等。而是属性的内容必须被满足,而不仅仅是推荐或方便。

请参阅 cmake-packages(7) 手册中的创建可迁移包一节,以了解在创建用于再分发的包时,指定使用要求时必须注意的其他事项。

目标属性

当编译二进制目标的源文件时,INCLUDE_DIRECTORIESCOMPILE_DEFINITIONSCOMPILE_OPTIONS目标属性的内容被适当地使用。

INCLUDE_DIRECTORIES中的条目以-I-isystem为前缀,按照属性值的出现顺序添加到编译行中。

COMPILE_DEFINITIONS中的条目以-D/D为前缀,并以未指定的顺序添加到编译行中。

DEFINE_SYMBOL目标属性也会被添加为编译定义,作为SHARED和MODULE库目标的特殊方便情况。

COMPILE_OPTIONS中的条目对shell进行了转义,并按照属性值的出现顺序添加。有几个编译选项有特殊的单独处理,如POSITION_INDEPENDENT_CODE

INTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONSINTERFACE_COMPILE_OPTIONS目标属性的内容是使用要求–它们指定了消费者必须使用的内容,以正确编译和链接它们出现的目标。对于任何二进制目标,在 target_link_libraries() 命令中指定的每个目标上的 INTERFACE_ 属性的内容都会被消耗。

set(srcs archive.cpp zip.cpp)
if (LZMA_FOUND)
  list(APPEND srcs lzma.cpp)
endif()
add_library(archive SHARED ${srcs})
if (LZMA_FOUND)
  # The archive library sources are compiled with -DBUILDING_WITH_LZMA
  target_compile_definitions(archive PRIVATE BUILDING_WITH_LZMA)
endif()
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)

add_executable(consumer)
# Link consumer to archive and consume its usage requirements. The consumer
# executable sources are compiled with -DUSING_ARCHIVE_LIB.
target_link_libraries(consumer archive)

由于通常要求将源代码目录和相应的联编目录添加到 INCLUDE_DIRECTORIES 中,因此可以启用 CMAKE_INCLUDE_CURRENT_DIR 变量来方便地将相应的目录添加到所有目标的 INCLUDE_DIRECTORIES 中。启用 CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE 变量,可以将相应的目录添加到所有目标的 INTERFACE_INCLUDE_DIRECTORIES 中。这样,通过使用 target_link_libraries() 命令,可以方便地使用多个不同目录中的目标。

传递使用要求

目标的使用要求可以转发到依赖者。target_link_libraries()命令有PRIVATE、INTERFACE和PUBLIC关键字来控制传播。

add_library(archive archive.cpp)
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)

add_library(serialization serialization.cpp)
target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)

add_library(archiveExtras extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)
target_link_libraries(archiveExtras PRIVATE serialization)
# archiveExtras is compiled with -DUSING_ARCHIVE_LIB
# and -DUSING_SERIALIZATION_LIB

add_executable(consumer consumer.cpp)
# consumer is compiled with -DUSING_ARCHIVE_LIB
target_link_libraries(consumer archiveExtras)

因为archive是archiveExtras的公共依赖,所以它的使用要求也会传播给消费者。因为序列化是 archiveExtras 的 PRIVATE 依赖,所以它的使用要求不会传播给消费者。

一般来说,如果

  • 一个依赖关系只被一个库的实现使用,而不是在头文件中使用,那么就应该在 target_link_libraries() 的使用中用 PRIVATE 关键字来指定它。
  • 一个依赖关系在库的头文件中被额外使用(例如类的继承),那么它应该被指定为PUBLIC依赖关系。
  • 一个不被库的实现所使用,而只被库的头文件使用的依赖关系,应该被指定为接口依赖关系。target_link_libraries() 命令可以在调用时多次使用每个关键字。
target_link_libraries(archiveExtras
  PUBLIC archive
  PRIVATE serialization
)

通过从依赖关系中读取目标属性的INTERFACE_变体,并将其值追加到操作数的非INTERFACE_变体中,来传播使用需求。例如,读取依赖项的INTERFACE_INCLUDE_DIRECTORIES,并将其附加到操作数的INCLUDE_DIRECTORIES上。如果顺序是相关的,并且得到了维护,而由target_link_libraries()调用产生的顺序不允许正确的编译,那么使用适当的命令直接设置属性可以更新顺序。

例如,如果一个目标的链接库必须按 lib1 lib2 lib3 的顺序指定,但包含目录必须按 lib3 lib1 lib2 的顺序指定:

target_link_libraries(myExe lib1 lib2 lib3)
target_include_directories(myExe
  PRIVATE $<TARGET_PROPERTY:lib3,INTERFACE_INCLUDE_DIRECTORIES>)

请注意,在为将使用 install(EXPORT) 命令导出安装的目标指定使用要求时,必须小心谨慎。更多内容请参见创建包

兼容接口属性

有些目标属性是需要目标和每个依赖的接口之间兼容的。例如,POSITION_INDEPENDENT_CODE目标属性可以指定一个布尔值,即目标是否应该被编译为位置独立代码,这具有平台特定的后果。目标也可以指定使用要求INTERFACE_POSITION_INDEPENDENT_CODE来传达消费者必须被编译为位置独立代码。

add_executable(exe1 exe1.cpp)
set_property(TARGET exe1 PROPERTY POSITION_INDEPENDENT_CODE ON)

add_library(lib1 SHARED lib1.cpp)
set_property(TARGET lib1 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1)

这里, exe1 和 exe2 都会被编译为 position-independent 代码。lib1 也会被编译为 position-independent 代码,因为这是 SHARED 库的默认设置。如果依赖关系有冲突的、不兼容的需求, cmake(1) 会发出一个诊断。

add_library(lib1 SHARED lib1.cpp)
set_property(TARGET lib1 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)

add_library(lib2 SHARED lib2.cpp)
set_property(TARGET lib2 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE OFF)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1)
set_property(TARGET exe1 PROPERTY POSITION_INDEPENDENT_CODE OFF)

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1 lib2)

lib1的需求INTERFACE_POSITION_INDEPENDENT_CODE与exe1目标的POSITION_INDEPENDENT_CODE属性不 “兼容”。库要求消费者以position-independent-code的方式构建,而可执行文件指定不以position-independent-code的方式构建,所以会发出诊断。

lib1和lib2的要求并不 “兼容”。其中一个要求消费者被构建为position-independent-code,而另一个要求消费者不被构建为position-independent-code。因为exe2链接到这两个要求,而它们是冲突的,所以发出了CMake错误信息。

CMake Error: The INTERFACE_POSITION_INDEPENDENT_CODE property of "lib2" does
not agree with the value of POSITION_INDEPENDENT_CODE already determined
for "exe2".

为了 “兼容”,POSITION_INDEPENDENT_CODE属性如果被设置的话,必须或者在布尔意义上与所有被设置该属性的过境指定的依赖关系的INTERFACE_POSITION_INDEPENDENT_CODE属性相同。

这个 “兼容接口要求 “的属性可以通过在COMPATIBLE_INTERFACE_BOOL目标属性的内容中指定该属性来扩展到其他属性。每个指定的属性必须在消费目标和每个依赖关系中带有INTERFACE_前缀的相应属性之间兼容。

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_CUSTOM_PROP ON)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_BOOL CUSTOM_PROP
)

add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_CUSTOM_PROP OFF)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1Version2) # CUSTOM_PROP will be ON

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1Version2 lib1Version3) # Diagnostic

非boolean属性也可以参与 “兼容接口 “计算。COMPATIBLE_INTERFACE_STRING属性中指定的属性必须是未指定的,或者在所有转义指定的依赖关系中比较相同的字符串。这对于确保一个库的多个不兼容版本不会通过目标的转接需求连接在一起是很有用的。

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_LIB_VERSION 2)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_STRING LIB_VERSION
)

add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_LIB_VERSION 3)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1Version2) # LIB_VERSION will be "2"

add_executable(exe2 exe2.cpp)
target_link_libraries(exe2 lib1Version2 lib1Version3) # Diagnostic

COMPATIBLE_INTERFACE_NUMBER_MAX目标属性指定了内容将被数值化评估,并将计算所有指定的最大数量。

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY INTERFACE_CONTAINER_SIZE_REQUIRED 200)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_NUMBER_MAX CONTAINER_SIZE_REQUIRED
)

add_library(lib1Version3 SHARED lib1_v3.cpp)
set_property(TARGET lib1Version3 PROPERTY INTERFACE_CONTAINER_SIZE_REQUIRED 1000)

add_executable(exe1 exe1.cpp)
# CONTAINER_SIZE_REQUIRED will be "200"
target_link_libraries(exe1 lib1Version2)

add_executable(exe2 exe2.cpp)
# CONTAINER_SIZE_REQUIRED will be "1000"
target_link_libraries(exe2 lib1Version2 lib1Version3)

类似地,COMPATIBLE_INTERFACE_NUMBER_MIN可用于从依赖关系中计算属性的数字最小值。

每个计算出的 “兼容 “属性值可以在生成时使用生成器表达式在消费者中读取。

请注意,对于每个依赖者,每个兼容接口属性中指定的属性集不得与任何其他属性中指定的属性集相交。

属性起源调试

因为构建规范可以由依赖关系决定,所以创建目标的代码和负责设置构建规范的代码缺乏 locality,可能会使代码更难推理。 cmake(1) 提供了一个调试设施,可以打印可能由依赖关系决定的属性内容的来源。可以调试的属性列在 CMAKE_DEBUG_TARGET_PROPERTIES 变量文档中。

set(CMAKE_DEBUG_TARGET_PROPERTIES
  INCLUDE_DIRECTORIES
  COMPILE_DEFINITIONS
  POSITION_INDEPENDENT_CODE
  CONTAINER_SIZE_REQUIRED
  LIB_VERSION
)
add_executable(exe1 exe1.cpp)

COMPATIBLE_INTERFACE_BOOLCOMPATIBLE_INTERFACE_STRING中列出的属性的情况下,调试输出显示了哪个目标负责设置该属性,以及哪些其他依赖关系也定义了该属性。在COMPATIBLE_INTERFACE_NUMBER_MAXCOMPATIBLE_INTERFACE_NUMBER_MIN的情况下,调试输出显示了来自每个依赖的属性值,以及该值是否决定了新的极端。

使用生成器表达式构建规范

构建规范可以使用包含内容的生成器表达式,这些内容可能是有条件的,或者只有在生成时才知道。例如,计算出的属性 “兼容 “值可以用TARGET_PROPERTY表达式来读取。

add_library(lib1Version2 SHARED lib1_v2.cpp)
set_property(TARGET lib1Version2 PROPERTY
  INTERFACE_CONTAINER_SIZE_REQUIRED 200)
set_property(TARGET lib1Version2 APPEND PROPERTY
  COMPATIBLE_INTERFACE_NUMBER_MAX CONTAINER_SIZE_REQUIRED
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1Version2)
target_compile_definitions(exe1 PRIVATE
    CONTAINER_SIZE=$<TARGET_PROPERTY:CONTAINER_SIZE_REQUIRED>
)

在这种情况下,exe1源文件将以-DCONTAINER_SIZE=200进行编译。

配置确定的构建规格可以使用CONFIG生成器表达式方便地设置。

target_compile_definitions(exe1 PRIVATE
    $<$<CONFIG:Debug>:DEBUG_BUILD>
)

CONFIG参数与正在构建的配置进行了不分大小写的比较。在存在 IMPORTED 目标的情况下, MAP_IMPORTED_CONFIG_DEBUG 的内容也会被这个表达式所考虑。

一些由 cmake(1) 生成的构建系统在 CMAKE_BUILD_TYPE 变量中设置了一个预定的构建配置。Visual Studio 和 Xcode 等 IDE 的构建系统是独立于构建配置生成的,而实际的构建配置要到构建时才知道。因此,像这样的代码:

string(TOLOWER ${CMAKE_BUILD_TYPE} _type)
if (_type STREQUAL debug)
  target_compile_definitions(exe1 PRIVATE DEBUG_BUILD)
endif()

可能看起来对Makefile生成器和Ninja生成器有效,但不能移植到IDE生成器中。此外,这样的代码并没有考虑到IMPORTED配置映射,所以应该避免使用。

一元化的TARGET_PROPERTY生成器表达式和TARGET_POLICY生成器表达式是以消耗的目标上下文来评估的。这意味着,使用需求规范可能会根据消费者的情况进行不同的评估:

add_library(lib1 lib1.cpp)
target_compile_definitions(lib1 INTERFACE
  $<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>:LIB1_WITH_EXE>
  $<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,SHARED_LIBRARY>:LIB1_WITH_SHARED_LIB>
  $<$<TARGET_POLICY:CMP0041>:CONSUMER_CMP0041_NEW>
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1)

cmake_policy(SET CMP0041 NEW)

add_library(shared_lib shared_lib.cpp)
target_link_libraries(shared_lib lib1)

exe1可执行文件将使用-DLIB1_WITH_EXE进行编译,而shared_lib共享库将使用-DLIB1_WITH_SHARED_LIB-DCONSUMER_CMP0041_NEW进行编译,因为在创建shared_lib目标时,策略CMP0041是NEW。

BUILD_INTERFACE表达式封装了一些需求,这些需求只有在从同一构建系统中的目标消耗时才会使用,或者在使用export()命令从目标导出到构建目录时才会使用。INSTALL_INTERFACE 表达式封装的需求,只在从一个已经安装并使用 install(EXPORT) 命令导出的目标中消耗时使用。

add_library(ClimbingStats climbingstats.cpp)
target_compile_definitions(ClimbingStats INTERFACE
  $<BUILD_INTERFACE:ClimbingStats_FROM_BUILD_LOCATION>
  $<INSTALL_INTERFACE:ClimbingStats_FROM_INSTALLED_LOCATION>
)
install(TARGETS ClimbingStats EXPORT libExport ${InstallArgs})
install(EXPORT libExport NAMESPACE Upstream::
        DESTINATION lib/cmake/ClimbingStats)
export(EXPORT libExport NAMESPACE Upstream::)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 ClimbingStats)

在这种情况下, exe1 可执行文件将以 -DClimbingStats_FROM_BUILD_LOCATION 编译。导出命令会生成IMPORTED目标,并省略INSTALL_INTERFACEBUILD_INTERFACE,同时去掉*_INTERFACE标记。一个消耗ClimbingStats包的单独项目将包含:

find_package(ClimbingStats REQUIRED)

add_executable(Downstream main.cpp)
target_link_libraries(Downstream Upstream::ClimbingStats)

根据 ClimbingStats 包是在联编位置还是安装位置使用,下游目标将使用 -DClimbingStats_FROM_BUILD_LOCATION -DClimbingStats_FROM_INSTALL_LOCATION 进行编译。关于软件包和导出的更多信息, 请参见 cmake-packages(7) 手册。

包括目录和使用要求

在指定使用要求和与生成器表达式一起使用时,包含目录需要一些特殊的考虑。target_include_directories()命令既接受相对的也接受绝对的include目录。

add_library(lib1 lib1.cpp)
target_include_directories(lib1 PRIVATE
  /absolute/path
  relative/path
)

相对路径是相对于命令出现的源目录进行解释的。在IMPORTED目标的INTERFACE_INCLUDE_DIRECTORIES中不允许使用相对路径。

在使用non-trivial 生成器表达式的情况下,INSTALL_PREFIX表达式可以在INSTALL_INTERFACE表达式的参数内使用。它是一个替换标记,当消费项目导入时,会扩展到安装前缀。

包括目录的使用要求通常在build-treeinstall-tree之间有所不同。BUILD_INTERFACEINSTALL_INTERFACE生成器表达式可以用来描述基于使用位置的单独使用要求。INSTALL_INTERFACE表达式中允许使用相对路径,并且相对于安装前缀进行解释。例如:

add_library(ClimbingStats climbingstats.cpp)
target_include_directories(ClimbingStats INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>
  $<INSTALL_INTERFACE:/absolute/path>
  $<INSTALL_INTERFACE:relative/path>
  $<INSTALL_INTERFACE:$<INSTALL_PREFIX>/$<CONFIG>/generated>
)

提供了两个与包含目录使用要求有关的方便 API。CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE 变量可以启用,其效果等同于:

set_property(TARGET tgt APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR};${CMAKE_CURRENT_BINARY_DIR}>
)

为每个受影响的目标提供服务。对于已安装的目标,方便的是安装(TARGETS)命令的INCLUDES DESTINATION组件。

install(TARGETS foo bar bat EXPORT tgts ${dest_args}
  INCLUDES DESTINATION include
)
install(EXPORT tgts ${other_args})
install(FILES ${headers} DESTINATION include)

这相当于在 install(EXPORT) 生成时, 将 ${CMAKE_INSTALL_PREFIX}/include 附加到每个已安装的 IMPORTED 目标的 INTERFACE_INCLUDE_DIRECTORIES 中。

当使用导入目标的 INTERFACE_INCLUDE_DIRECTORIES时,该属性中的条目会被视为 SYSTEM 包含目录,就像它们被列在依赖关系的 INTERFACE_SYSTEM_INCLUDE_DIRECTORIES 中一样。这可能会导致在这些目录中发现的头文件的编译器警告被忽略。对于导入目标的这种行为,可以通过在导入目标的消费者上设置NO_SYSTEM_FROM_IMPORTED目标属性来控制。

如果一个二进制目标被转接到macOS FRAMEWORK上,框架的Headers目录也会被视为使用需求。这与将框架目录作为include目录传递的效果是一样的。

链接库和生成器表达式

与构建规范一样,链接库也可以用生成器表达条件来指定。但是,由于使用需求的消耗是基于从链接依赖关系中收集的,因此有一个额外的限制,即链接依赖关系必须形成一个 “定向无环图”。也就是说,如果链接到一个目标是依赖于一个目标属性的值,那么该目标属性可能不依赖于链接依赖关系。

add_library(lib1 lib1.cpp)
add_library(lib2 lib2.cpp)
target_link_libraries(lib1 PUBLIC
  $<$<TARGET_PROPERTY:POSITION_INDEPENDENT_CODE>:lib2>
)
add_library(lib3 lib3.cpp)
set_property(TARGET lib3 PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 lib1 lib3)

由于 exe1 目标的 POSITION_INDEPENDENT_CODE 属性的值依赖于所链接的库 (lib3),而链接 exe1 的边缘也是由相同的 POSITION_INDEPENDENT_CODE 属性决定的,所以上面的依赖图包含一个循环, cmake(1) 发出错误信息。

输出工件

add_library()add_executable()命令创建的构建系统目标创建规则来创建二进制输出。二进制文件的确切输出位置只能在生成时确定,因为它可能取决于构建配置和链接依赖的链接语言等。TARGET_FILETARGET_LINKER_FILE和相关表达式可以用来访问生成的二进制文件的名称和位置。但这些表达式对OBJECT库不起作用,因为这类库生成的单文件与表达式无关。

目标可以构建的输出工件有三种,详见以下章节。它们的分类在DLL平台和非DLL平台之间有所不同。包括Cygwin在内的所有基于Windows的系统都是DLL平台。

运行时输出工件

一个构建系统目标的运行时输出工件可以是:

  • 通过add_executable()命令创建的可执行目标的可执行文件(如.exe)。

  • 在DLL平台上:通过带有SHARED选项的add_library()命令创建的共享库目标的可执行文件(例如.dll)。

RUNTIME_OUTPUT_DIRECTORYRUNTIME_OUTPUT_NAME目标属性可用于控制运行时输出工件在构建树中的位置和名称。

库输出工件

一个构建系统目标的库输出工件可以是。

  • 通过带有MODULE选项的add_library()命令创建的模块库目标的可加载模块文件(如.dll或.so)。

  • 在非DLL平台上:由add_library()命令和SHARED选项创建的共享库目标的共享库文件(例如.so或.dylib)。

LIBRARY_OUTPUT_DIRECTORYLIBRARY_OUTPUT_NAME 目标属性可用于控制构建树中库输出工件的位置和名称。

归档输出工件

一个构建系统目标的存档输出工件可以是:

  • 静态库目标的静态库文件(如.lib或.a),由带有STATIC选项的add_library()命令创建。

  • 在 DLL 平台上:通过带有 SHARED 选项的 add_library() 命令创建的共享库目标的导入库文件(例如 .lib)。只有当该库至少导出一个非托管符号时,才会保证该文件的存在。

  • 在DLL平台上:当可执行库的ENABLE_EXPORTS目标属性被设置时,由add_executable()命令创建的可执行库目标的导入库文件(例如.lib)。

  • 在 AIX 上:当 ENABLE_EXPORTS目标属性被设置时,由 add_executable() 命令创建的可执行目标的链接器导入文件 (例如 .imp)。

ARCHIVE_OUTPUT_DIRECTORY ARCHIVE_OUTPUT_NAME 目标属性可用于控制构建树中的存档输出工件位置和名称。

目录范围内的命令

target_include_directories()target_compile_definitions()target_compile_options()命令一次只对一个目标产生影响。命令add_compile_definitions()add_compile_options()include_directories()有类似的功能,但为了方便,在目录范围而不是目标范围内操作。

伪目标

有些目标类型不代表构建系统的输出,而只代表输入,如外部依赖、别名或其他非构建工件。在生成的构建系统中不表示伪目标。

Import 目标

IMPORTED 目标代表了一个已经存在的依赖关系。通常这样的目标是由上游包定义的,应该被视为不可改变的。在声明一个 IMPORTED target 之后,可以像其他常规 target 一样,通过使用 target_compile_definitions()target_include_directories()target_compile_options()target_link_libraries()等命令调整它的 target 属性。

IMPORTED target可能具有与二进制目标相同的使用要求属性,如INTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONSINTERFACE_COMPILE_OPTIONSINTERFACE_LINK_LIBRARIESINTERFACE_POSITION_INDEPENDENT_CODE

LOCATION 也可以从 IMPORTED 目标中读取,尽管很少有理由这样做。像add_custom_command()这样的命令可以透明地使用一个IMPORTED的EXECUTABLE目标作为COMMAND可执行文件。

IMPORTED target的定义范围是它被定义的目录。它可以从子目录中访问和使用,但不能从父目录或同级目录中访问和使用。这个范围类似于 cmake 变量的范围。

也可以定义一个 GLOBAL IMPORTED 目标,它可以在构建系统中全局访问。

参见 cmake-packages(7) 手册以了解更多关于创建带有 IMPORTED 目标的软件包的信息。

别名目标

ALIAS目标是一个名称,它可以在只读的情况下与二进制目标名称互换使用。ALIAS目标的一个主要用途是例如或伴随着一个库的单元测试可执行文件,它可能是同一构建系统的一部分或根据用户配置单独构建。

add_library(lib1 lib1.cpp)
install(TARGETS lib1 EXPORT lib1Export ${dest_args})
install(EXPORT lib1Export NAMESPACE Upstream:: ${other_args})

add_library(Upstream::lib1 ALIAS lib1)

在另一个目录中,我们可以无条件地链接到Upstream::lib1目标,它可能是一个从包中导入的目标,或者是一个ALIAS目标,如果作为同一个构建系统的一部分构建的话。

if (NOT TARGET Upstream::lib1)
  find_package(lib1 REQUIRED)
endif()
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Upstream::lib1)

ALIAS目标是不可更改、不可安装或不可导出的。它们完全是本地的构建系统描述。可以通过读取ALIASED_TARGET属性来测试一个名字是否是ALIAS名字:

get_target_property(_aliased Upstream::lib1 ALIASED_TARGET)
if(_aliased)
  message(STATUS "The name Upstream::lib1 is an ALIAS for ${_aliased}.")
endif()

接口库

INTERFACE库目标不编译源,也不在磁盘上产生库制品,所以它没有LOCATION。

它可以指定使用要求,如INTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONSINTERFACE_COMPILE_OPTIONSINTERFACE_LINK_LIBRARIESINTERFACE_SOURCESINTERFACE_POSITION_INDEPENDENT_CODE。只有 target_include_directories()target_compile_definitions()target_compile_options()target_sources()target_link_libraries()命令的 INTERFACE 模式可以用于 INTERFACE 库。

自 CMake 3.19 起,INTERFACE 库目标可以选择包含源文件。一个包含源文件的接口库将作为一个构建目标包含在生成的构建系统中。它不编译源文件,但可能包含自定义命令来生成其他源文件。此外,IDE会将源文件作为目标的一部分显示出来,供交互式阅读和编辑。

INTERFACE库的一个主要用例是仅有头的库。

add_library(Eigen INTERFACE
  src/eigen.h
  src/vector.h
  src/matrix.h
  )
target_include_directories(Eigen INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
  $<INSTALL_INTERFACE:include/Eigen>
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)

在这里,来自Eigen目标的用法需求在编译时被消耗和使用,但它对链接没有影响。

另一种用例是对使用需求采用完全以目标为中心的设计:

add_library(pic_on INTERFACE)
set_property(TARGET pic_on PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON)
add_library(pic_off INTERFACE)
set_property(TARGET pic_off PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE OFF)

add_library(enable_rtti INTERFACE)
target_compile_options(enable_rtti INTERFACE
  $<$<OR:$<COMPILER_ID:GNU>,$<COMPILER_ID:Clang>>:-rtti>
)

add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 pic_on enable_rtti)

这样一来,exe1的构建规范完全以链接目标的形式来表达,而编译器特定标志的复杂性被封装在INTERFACE库目标中。

INTERFACE库可以安装和导出。它们所引用的任何内容必须单独安装:

set(Eigen_headers
  src/eigen.h
  src/vector.h
  src/matrix.h
  )
add_library(Eigen INTERFACE ${Eigen_headers})
target_include_directories(Eigen INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
  $<INSTALL_INTERFACE:include/Eigen>
)

install(TARGETS Eigen EXPORT eigenExport)
install(EXPORT eigenExport NAMESPACE Upstream::
  DESTINATION lib/cmake/Eigen
)
install(FILES ${Eigen_headers}
  DESTINATION include/Eigen
)

CMake 基础知识

GCC的编译过程

编译单个文件

编译:编写和翻译;编译就是把你编写的文本代码翻译成机器可以运行的01代码。这个编译过程是通过gcc这个编译程序实现的。 具体的编译过程如下:

  • 首先,gcc能译的代码是汇编语言代码,所以要先把我们的代码翻译成汇编语言代码。
  • 而在翻译之前,又要先处理一下我们写的应用代码,因为应用代码里有很多宏和include的快捷代码。所以我们先把快捷方式include展开成完整代码
    • gcc -E main.c -o main.i
  • 有了完整代码,再把代码翻译成汇编语言
    • gcc -S main.i -o main.s
  • 有了汇编代码,再把汇编翻译成计算机01代码
    • gcc -c main.s -o main.o
  • 最后,还要把库代码链接进来,成为一个可执行程序
    • gcc main.o -o main

  • 通过 gcc main.c -o main 可以得到一个 main.out 执行文件
  • gcc –E hello.c –o hello.i:Preprocess only; do not compile, assemble or link
    • 仅预处理,不翻译、编译和链接
    • 宏展开,包含头文件……
  • gcc –S hello.i –o hello.s:Compile only; do not assemble or link
    • 仅翻译,不编译和链接
    • 翻译成汇编语言,不同构架有不同的汇编语言
  • gcc –c hello.s –o hello.o: Compile and assemble, but do not link
    • 把汇编语言书写的程序翻译成与之等价的机器语言程序
  • gcc hello.o –o hello
    • 把库函数链接进来得到最终的程序
  • 使用哪个c语言标准编译

     // 使用c99编译
     gcc -std=c99 
     // 使用gnu99(c99的gnu扩展)编译
     gcc -std=gnu99
    
  • 编译静态库

      gcc -c hello.c -o hello.o
      // 静态库名称规则: lib+名字+.a, 否则在使用-l链接的时候会找不到
      ar -r libhello.a hello.o hello2.o  
      gcc main.c libhello.a -o main
      // 或者:-L制定库的搜索路径,-l调用链接库
      gcc -L ./ main.c -lhello -o main  
    
  • 编译动态库

      // fpic:采用浮动地址
      gcc -c -fpic hello.c 
      // .so告诉编译器编译成动态库,省略的话会编译成exe
      gcc -shared hello.o -o hello.so  
    

编译多个文件

gcc func1.c main.c -o main

有多个文件时,就用makefile写一个批处理文件。但是makefile又是用os的命令来实现的,所以要用cmake对不同的os生成不同的makefile

用CMake编译项目

单个文件

在目录有tutorial.cxx文件,创建CMakeLists.txt文件:

  • 首先指定CMake的版本:cmake_minimum_required(VERSION 3.10)
  • 然后是项目的名字 project(Tutorial)
  • 最后是生成的程序名字 add_executable(Tutorial tutorial.cxx)
  • 在当前目录执行cmake:cmake .,生成这个os平台的makefile,最后执行make来编译

多个文件

cmake_minimum_required(VERSION 3.10)

project(Tutorial)

add_executable(Tutorial tutorial.cxx file1.cxx)

可以用命令aux_source_directory(dir,var)把dir目录中的所有文件保存到var变量中

cmake_minimum_required(VERSION 3.10)

project(Tutorial)

aux_source_directory(. dir_srcs)

add_executable(Tutorial ${dir_srcs})

多文件多目录

CMakeLists.txt demo.cpp mylib/CmakeLists.txt mylib/mymath.cpp mylib/mymath.hpp
  • 子目录下的CMakeLists.txt

      aux_source_directory(. dir_srcs)
    
      add_library(Mylib ${dir_srcs})
    
    • add_library(Mylib STATIC ${dir_srcs})
    • add_library(Mylib SHARED ${dir_srcs})
  • 顶级目录下

      cmake_minimum_required(VERSION 3.10)
    
      project(Tutorial)
    
      # 把子目录加到当前工程中
      add_subdirectory(./mylib) 
    
      aux_source_directory(. dir_srcs)
    
      add_executable(Tutorial ${dir_srcs})
    
      # 把子目录中的库链接到程序中
      target_link_libraries(Tutorial Mylib)
    

在标准工程中构建

.
├── build
├── CMakeLists.txt
├── mylib
│   ├── CMakeLists.txt
│   ├── power.c
│   └── power.h
└── src
    ├── CMakeLists.txt
    └── demo.c

  • 首先在mylib中写好CMakeLists.txt

      aux_source_directory(. dir_srcs)
    
      add_library(mylib ${dir_srcs})
    
  • 然后写src/CMakeLists.txt

      include_directories(${PROJECT_SOURCE_DIR}/mylib)
      aux_source_directory(. dir_srcs)
      add_executable(demo ${dir_srcs})
      target_link_libraries(demo mylib)
    
  • 第三,写项目CMakeLists.txt

      cmake_minimum_required(VERSION 3.20)
    
      project(demo)
      add_definition(-std=c99)
    
      # 设置调试和编译选项
      set(CMAKE_BUILD_TYPE "debug")
      set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
      set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
    
      add_subdirectory(./mylib)
      add_subdirectory(./src)
    
  • 第四,进入build中构建

      >>> cd build
      >>> cmake ..
      >>> make
    

改变库和程序构建后的路径

  • mylib/CMakeLists.txt

      aux_source_directory(. dir_srcs)
      # 设置库文件的输出路径
      set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
      add_library(mylib ${dir_srcs})
    
  • src/CMakeLists.txt

      include_directories(${PROJECT_SOURCE_DIR}/mylib)
      # 设置可执行程序的输出路径
      set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
      aux_source_directory(. dir_srcs)
      add_executable(demo ${dir_srcs})
      target_link_libraries(demo mylib)
    

    自定义编译选项

  • src/CMakeLists.txt

      include_directories(${PROJECT_SOURCE_DIR}/mylib)
      include_directories(${PROJECT_SOURCE_DIR}/config)
      set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
    
      # 设置配置文件的路径,参数:由"输入文件"生成的"输出文件"
      configure_file(
          "${PROJECT_SOURCE_DIR}/config/config.h.in"
          "${PROJECT_SOURCE_DIR}/config/config.h"
      )
      # 定义一下下面config.h.in中的宏定义是什么
      option(
          USE_MYMATH ON
      )
      # 如果USE_MYMATH为1,则引入mylib
      if (USE_MYMATH)
          include_directories(${PROJECT_SOURCE_DIR}/mylib)
      endif(USE_MYMATH)
    
      aux_source_directory(. dir_srcs)
      add_executable(demo ${dir_srcs})
      target_link_libraries(demo mylib)
    
  • config/config.hpp.in

      #cmakedefine USE_MYMATH
    
  • 在build目录中执行ccmake配置USE_MYMATH选项的值
  • 编译

      cd build
      ccmake .
      make
      ../bin/demo
    

学习网站