CMake 官方完整版

CMake 官方完整版

  • 一些概念
    • Target
    • Signature
  • 1. A Basic Starting Point(Step 1)
    • Adding a Version Number and Configured Header File
    • Specify the C++ Standard
    • Build and Test
  • 2. Adding a Library(Step 2)-添加一个库
    • Adding an Option
  • 3. Adding Usage Requirements for Library(Step 3)-为库添加使用需求
    • Setting the C++ Standard with Interface Libraries
  • 4. Adding Generator Expressions (Step 4)-添加生成器表达式
  • 5. Installing and Testing(Step 5)-安装和测试
    • Install Rules
    • Testing Support
      • CTest 运行方式
        • ctest -N
        • ctest -VV
  • 6. Adding Support for a Testing Dashboard(Step 6)-添加测试仪表盘支持
  • 7. Adding System Introspection(Step 7)-添加系统自省
  • 8. Adding a Custom Command and Generated File(Step 8)-增加自定义命令并生成文件
  • 9. Building an Installer(Step 9)-构建一个安装程序
  • 10. Selecting Static or Shared Libraries(Step 10)-混合静态库和动态库
  • 11. Adding Export Configuration (Step 11)-添加导出配置
  • 12. Packaging Debug and Release (Step 12)-打包调试和发布

Reference:

  1. CMake Tutorial-v3.20
  2. CMake教程
  3. CMake Tutorial-v3.27

相关文章:

  1. CMakeLists Option使用简介

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


一些概念

Target

在 CMake 中,target 是一个重要的概念,用于表示项目中的各种构建目标,例如可执行程序、静态库、共享库等。以下是常见的 CMake target 类型:

  1. 可执行程序(Executable Targets)
    • add_executable(target_name source_files...) 用于定义一个可执行程序目标
    • 示例:add_executable(my_app main.cpp)
  2. 静态库(Static Library Targets)
    • add_library(target_name STATIC source_files...) 用于定义一个静态库目标
    • 示例:add_library(my_static_lib STATIC lib.cpp)
  3. 共享库(Shared Library Targets)
    • add_library(target_name SHARED source_files...) 用于定义一个共享库目标
    • 示例:add_library(my_shared_lib SHARED lib.cpp)
  4. 模块库(Module Library Targets)
    • add_library(target_name MODULE source_files...) 用于定义一个模块库目标,通常用于插件和动态加载的场景。
    • 示例:add_library(my_module MODULE plugin.cpp)
  5. 接口库(Interface Library Targets)
    • add_library(target_name INTERFACE) 用于定义一个接口库目标,它通常不包含实际的源文件,主要用于传递编译器和链接器的设置。
    • 示例:add_library(my_interface_lib INTERFACE)
  6. 自定义目标(Custom Targets)
    • 你可以使用 add_custom_target 来定义自定义的构建目标,这些目标可以执行自定义的构建命令。
    • 示例:add_custom_target(my_custom_target COMMAND echo "Custom Target")
  7. 自定义命令(Custom Commands)
    • 使用 add_custom_command 可以定义自定义的构建命令,这些命令可以与目标关联。
    • 示例:add_custom_command(TARGET my_target POST_BUILD COMMAND my_command)
  8. 导入目标(Imported Targets)
    • 用于导入外部项目或库的目标,通常由 find_packagefind_library 等命令生成。
    • 示例:find_package(Boost REQUIRED)

Signature

在 CMake 中,签名(Signature)指的是 CMake 命令的参数及其类型和顺序。CMake 命令的签名定义了如何正确使用该命令以及应该提供哪些参数。每个 CMake 命令都有一个特定的签名,开发者必须按照该签名的要求提供参数,以便CMake正确地处理它们。

CMake 命令的签名通常包括以下信息:

  1. 命令名称:命令的名称,例如 add_executableadd_librarytarget_include_directories 等。

  2. 参数列表:参数的名称和顺序。每个参数都有特定的名称,并且按照特定的顺序提供给命令。

  3. 参数类型:每个参数的类型,例如 STRINGLISTPATH 等。参数的类型指定了参数应该是一个字符串、列表、路径或其他类型的数据。

  4. 默认值:有些命令的参数有默认值,如果不提供参数,则将使用默认值。

例如,以下是 add_executable 命令的签名:

add_executable( [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 …])

在这个签名中:

  • 是一个必须提供的参数,表示要创建的可执行程序的名称。
  • [WIN32][MACOSX_BUNDLE] 是可选参数,用于指定可执行程序的类型。
  • [EXCLUDE_FROM_ALL] 也是可选参数,用于指定是否将目标从所有构建中排除。
  • source1 [source2 ...] 是必须提供的参数,表示可执行程序的源文件列表。
    签名的存在是为了帮助开发者正确使用 CMake 命令,以避免错误和不一致性。在 CMake 文档中,你可以找到每个命令的签名及其详细说明,以便了解如何正确使用它们。

1. A Basic Starting Point(Step 1)

最基础的项目是将源代码构建成可执行文件。对于一个简单的项目,只需要一个 3 3 3 行的 CMakeLists.txt 文件即可。在Step1目录下创建一个 CMakeLists.txt 文件:

# 指明了对cmake的最低版本要求
cmake_minimum_required(VERSION 3.10)

# 设置项目名
project(Tutorial)

# 添加可执行目标
add_executable(Tutorial tutorial.cxx) //每次看到cxx总感觉不太适应

注意:CMake 支持大写、小写或大小写混合命令。但在这些例子中,每个 CMakeLists.txt 文件都使用小写命令。tutorial.cxx 中的源代码放在Step1目录下,它用于计算一个数的平方根。

Adding a Version Number and Configured Header File

我们将添加的第一个特性是:我们的可执行文件和项目提供一个版本号。尽管我们可以在源代码中做到这一点,但是使用 CMakeLists.txt 会更加灵活。

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

cmake_minimum_required(VERSION 3.10)

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

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

  • configure_file:将文件复制到另一个位置并修改其内容。
    input 文件复制到 output 文件中,并替换输入文件内容中引用的变量值,如 @VAR@${VAR}
configure_file(TutorialConfig.h.in TutorialConfig.h)

因为配置的文件将被写到 binary tree 目录下(CMakeCache.txt 所在的目录,即 build 目录----${PROJECT_BINARY_DIR}),所以我们必须将那个目录添加到 include 搜索路径列表中。

  • target_include_directories:向目标添加包含目录。target_include_directories( [SYSTEM] [BEFORE] [items1...] [ [items2...] ...])
    • target 是要为其设置包含目录的目标名称。PRIVATEPUBLICINTERFACE 关键字用于指定包含目录的可见性和作用范围。
    • PRIVATE:只有目标本身可以看到这些包含目录。
    • PUBLIC:目标本身和依赖于该目标的其他目标都可以看到这些包含目录。
    • INTERFACE:只有依赖于该目标的其他目标可以看到这些包含目录。

将下面这行添加到 CMakeLists.txt 文件的末尾:

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

使用你最喜欢的编辑器,在源代码目录(Step1目录)中创建TutorialConfig.h.in,其中包含以下内容:

// 该教程的配置选项和设置
#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;
}

Specify the C++ Standard

接下来,给我们的项目添加一些 C++11 特性,在 tutorial.cxx 中用 std::stod 替换 atof(不带std的是 C 的,带std的是 C++ 的)。同时,删除 #include

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

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

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

# 指定c++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

Build and Test

运行 cmakecmake-gui 来配置(configure)项目,然后使用你所选的构建工具构建(build)它。

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

mkdir Step1_build
cd Step1_build
cmake ../Step1
cmake --build .

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

Tutorial 4294967296
Tutorial 10
Tutorial

2. Adding a Library(Step 2)-添加一个库

现在,我们已经看到了如何使用CMake创建一个基本项目。在这一步中,我们将学习如何在项目中创建和使用库。我们还将看到如何使库的使用成为可选的。
CMake 官方完整版_第1张图片

要在 CMake 中添加一个库,使用 add_library() 命令并指定哪些源文件应该组成该库。

  • add_library:使用指定的源文件向项目添加库。
add_library(MathFunctions mysqrt.cxx)

我们可以使用一个或多个子目录组织项目,而不是将所有源文件放在一个目录中。在本例中,我们将专门为库创建一个子目录。在这里,我们可以添加一个新的 CMakeLists.txt 文件和一个或多个源文件。在顶层的 CMakeLists.txt 文件中,我们将使用 add_subdirectory() 命令将子目录添加到构建中。

创建库之后,使用 target_include_directories()target_link_libraries() 将其连接到可执行目标。

MathFunctions 目录下的 CMakeLists.txt 文件中,我们使用 add_library() 创建了一个名为 MathFunctions 的库目标。库的源文件作为参数传递给 add_library()。这看起来像下面这行:

add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)

为了使用新库,我们将在顶层 CMakeLists.txt 文件中添加 add_subdirectory() 调用,以便库将被构建。

  • add_subdirectory:向 build 中添加一个子目录。
    source_dir 指定源 CMakeLists.txt 和代码文件所在的目录。如果它是一个相对路径,它将相对于当前目录进行评估(典型的用法),但它也可以是一个绝对路径。用于在当前项目的 CMakeLists.txt 文件中包含另一个子目录的 CMakeLists.txt 文件。
add_subdirectory(MathFunctions)

接下来,使用 target_link_libraries() 将新的库目标链接到可执行目标。

target_link_libraries(Tutorial PUBLIC MathFunctions)

最后,我们需要指定库的头文件位置。修改 target_include_directories() 以添加 MathFunctions 子目录作为包含目录,以便可以找到 MathFunctions.h 头文件。

target_include_directories(Tutorial PUBLIC
                          "${PROJECT_BINARY_DIR}"
                          "${PROJECT_SOURCE_DIR}/MathFunctions"
                          )

现在让我们使用库。在 tutorial.cxx,包含 MathFunctions.h

#include "MathFunctions.h"

最后,用库函数 mathfunctions::mysqrt 替换 sqrt

 const double outputValue = mathfunctions::sqrt(inputValue);

Adding an Option

现在让我们在 MathFunctions 库中添加一个选项,允许开发人员选择自定义的平方根实现或内置的标准实现。虽然对于教程来说,确实没有必要这样做,但对于大型项目来说,这是一种常见的情况。

CMake 可以使用 option() 命令执行此操作。这为用户提供了一个可以在配置 cmake 构建时更改的变量。这个设置将被存储在缓存中,这样用户就不需要每次在构建目录上运行 CMake 时都设置这个值(意思是不需要使用 -D 设置吧)。

第一步是在 MathFunctions/CMakeLists.txt 中添加一个选项。该选项将显示在 cmake-guiccmake 中,默认值为 ON,可由用户更改。

option(USE_MYMATH "Use tutorial provided math implementation" ON)

接下来,使用这个新选项使库与 mysqrt 函数的构建和链接成为有条件的。

创建一个 if() 语句来检查 USE_MYMATH 的值。在 if() 块中,放入 target_compile_definitions() 命令和编译定义 USE_MYMATH。(类似CMakeLists Option使用简介,但链接内使用的是全局的 add_definitions())

if (USE_MYMATH)
  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

USE_MYMATHON 时,编译定义 USE_MYMATH 将被设置。然后,我们可以使用这个编译定义来启用或禁用源代码的部分。

对源代码的相应更改相当简单。在 MathFunctions.cxx,我们让 USE_MYMATH 控制使用哪个平方根函数:

#ifdef USE_MYMATH
  return detail::mysqrt(x);
#else
  return std::sqrt(x);
#endif

接下来,如果定义了 USE_MYMATH,我们需要包含 mysqrt.h

#ifdef USE_MYMATH
#  include "mysqrt.h"
#endif

最后,在使用 std::sqrt 时,我们需要包含 cmath

#include 

此时,如果 USE_MYMATHOFFmysqrt.cxx 不会被使用,但它仍然会被编译,因为 MathFunctions 目标有 mysqrt.cxx 列在来源下面。

有几种方法可以解决这个问题。第一个选项是使用 target_sources() 来添加 mysqrt。从 USE_MYMATH 块中取出。另一个选择是在 USE_MYMATH 块中创建一个额外的库,它负责编译 mysqrt.cxx。在本教程中,我们将创建一个额外的库。

首先,在 USE_MYMATH 中创建一个名为 SqrtLibrary 的库,其源代码为 mysqrt.cxx

add_library(SqrtLibrary STATIC
            mysqrt.cxx
            )

接下来,当启用 USE_MYMATH 时,我们将 SqrtLibrary 链接到 MathFunctions

target_link_libraries(MathFunctions PUBLIC SqrtLibrary)

最后,我们可以删除 mysqrt。从 MathFunctions 库源列表中取出 cxx,因为它将在包含 SqrtLibrary 时被拉入。

add_library(MathFunctions MathFunctions.cxx)

通过这些更改,mysqrt 函数现在对于正在构建和使用 MathFunctions 库的人来说完全是可选的。用户可以切换 USE_MYMATH 来操作构建中使用的库。


现在使用另一种方式使构建和链接 MathFunctions 库成为一个条件。为此,我们将根目录下 CMakeLists.txt 文件的结尾更改为:

  • list:列表操作。
# Use list() and APPEND to create a list of optional libraries
# called EXTRA_LIBS and a list of optional include directories called
# EXTRA_INCLUDES. Add the MathFunctions library and source directory to
# the appropriate lists.
if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
  list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()

# 添加可执行目标
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# 添加binary tree目录到include文件的搜索路径
# 以便我们能找到TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           ${EXTRA_INCLUDES}
                           )

注意:使用变量 EXTRA_LIBS 收集所有可选库(list 的作用),以便之后链接到可执行文件中(target_include_directories)。类似地,变量 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 文件中(这种方法的原理是将 TutorialConfig.h.in 里面的 #cmakedefine USE_MYMATH,变成 TutorialConfig.h#define USE_MYMATH。这是种方法但感觉还是不够方便,我更倾向于在前面那种方法,即使用 option 后接 add_definitionstarget_compile_definitions):

#cmakedefine USE_MYMATH

运行 cmakecmake-gui 配置并构建项目,然后运行生成的可执行文件。

现在让我们更新USE_MYMATH的值。最简单的方式是使用 cmake-guiccmake ,如果你在终端中的话。或者,如果你想通过命令行更改选项的话,你可以:

cmake ../Step2 -DUSE_MYMATH=OFF

然后构建并重新运行。

3. Adding Usage Requirements for Library(Step 3)-为库添加使用需求

目标参数的 使用需求(Usage requirements) 让我们可以对库或可执行文件的链接和 include 行进行更好的控制,同时也让我们对 CMake 中目标的传递属性进行更多的控制。利用使用需求的主要命令有:

  • target_compile_definitions()
  • target_compile_options()
  • target_include_directories()
  • target_link_libraries()(这个在3.17咋不写出来了)
  • target_link_directories()
  • target_link_options()
  • target_precompile_headers()
  • target_sources()

现在,让我们使用现代的 CMake 方式(通过使用需求)重构 Step 2。首先声明任何链接到 MathFunctions 的都需要包含当前的 source 目录,而 MathFunctions 本身不需要。所以这可以成为一个 INTERFACE 使用需求。(相关内容请参考 CMake 构建系统 https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html)

记住,INTERFACE 意味着这玩意的使用者需要,但库的制作者不需要(看名字就可以知道,这里包含头文件是用来做接口的。所以编译的当前 library 并不需要用到它,而如果想使用这个 library,就需要相应的头文件了)。添加下面这行到 MathFunctions/CMakeLists.txt 文件尾:

#State that anybody linking to MathFunctions needs to include the
# current source directory, while MathFunctions itself doesn't.
target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          )

现在,我们已经指定了 MathFunctions 的使用需求,我们可以从根目录安全的删除 EXTRA_INCLUDES 变量的使用(之前的 list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions") 被删了):

if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
endif()

还有这里:

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

完成这些操作之后,运行 cmakecmake-gui 来配置并构建项目。

(这样写很有意思啊,经验证 target_include_directories(MathFunctions) 内使用 PUBLICINTERFACE 均可编译通过,而如果使用 PRIVATE 会报错:fatal error: MathFunctions.h: No such file or directory #include "MathFunctions.h"。不过感觉这种写法只适合在 add_subdirectory 时使用,这里的 target_include_directories 使用 INTERFACE 传递的应该是路径,我如果向其他人提供这种方式编译出的库文件,缺少了接口头文件应该还是会报找不到头文件的错,这个在 Step 11 有解决方法)

代码可以看到代码中还有以下片段:

target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)

注意,使用这种技术,我们的可执行目标使用我们的库所做的唯一事情就是用库目标的名称调用 target_link_libraries()。在较大的项目中,手动指定库依赖项的经典方法很快就会变得非常复杂。比如像我这样的一长条?
CMake 官方完整版_第2张图片

Setting the C++ Standard with Interface Libraries

既然我们已经将代码转换为更现代的方法,那么让我们演示一种为多个目标设置属性的现代技术。让我们重构现有代码以使用 INTERFACE 库。在这里,我们将重构代码,使用 INTERFACE 库指定 C++ 标准。让我们更新上一步的代码,使用接口库来设置 C++ 需求。

首先,我们需要删除变量 CMAKE_CXX_STANDARDCMAKE_CXX_STANDARD_REQUIRED 上的两个 set() 调用。具体要删除的行如下:

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

接下来,我们需要创建一个接口库 tutorial_compiler_flags。然后使用 target_compile_features() 来添加编译器特性 cxx_std_11

  • add_library:使用指定的源文件向项目添加库。
    • add_library( [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [source1] [source2 ...])
    • v3.15 的 Interface Libraries
      • add_library( INTERFACE)

      • 创建接口库。INTERFACE 库目标不编译源代码,也不会在磁盘上生成一个库工件(artifacts)。但是,它可以设置属性,并且可以安装和导出。通常,INTERFACE_* 属性在接口目标上使用以下命令填充:

        • set_property()
        • target_link_libraries(INTERFACE)
        • target_link_options(INTERFACE)
        • target_include_directories(INTERFACE)
        • target_compile_options(INTERFACE)
        • target_compile_definitions(INTERFACE)
        • target_sources(INTERFACE)

        然后像其他目标一样将它用作 target_link_libraries() 的参数。使用上述签名创建的接口库本身没有源文件,也不会作为目标包含在生成的构建系统中。

  • target_compile_features:向目标添加预期的编译器特性。
    • target_compile_features( [...])
    • 指定编译给定目标时所需的编译器特性。如果该特性没有在 CMAKE_C_COMPILE_FEATURES 变量或CMAKE_CXX_COMPILE_FEATURES 变量中列出,则 CMake 将报告错误。如果使用该特性需要额外的编译器标志,例如 -std=gnu++11,则会自动添加该标志。
add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

最后,在设置好接口库之后,我们需要将可执行的 TargetMathFunctions 库和 SqrtLibrary 库链接到新的 tutorial_compiler_flags 库。分别,代码看起来像这样:

target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)

这样:

target_link_libraries(SqrtLibrary PUBLIC tutorial_compiler_flags)

还有这样:

target_link_libraries(MathFunctions PUBLIC SqrtLibrary)

尽管如此,我们所有的代码仍然需要 C++ 11 来构建。注意,使用这种方法,我们可以明确哪些目标需要特定的需求。此外,我们在接口库中创建了一个单一的事实来源。

4. Adding Generator Expressions (Step 4)-添加生成器表达式

生成器表达式(Generator expressions) 在 build 生成系统时被评估,以产生特定于每个构建配置的信息。

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

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

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

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

生成器表达式的常用用途是条件性地添加编译器标志,例如语言级别或警告标志。一个好的模式是将这些信息与一个 INTERFACE 目标关联起来,以便这些信息能够传播。

更新 cmake_minimum_required() 以至少要求 CMake 版本 3.15:

cmake_minimum_required(VERSION 3.15)

接下来,我们确定系统当前正在使用哪个编译器进行构建,因为警告标志会根据我们使用的编译器而变化。这是通过 COMPILE_LANG_AND_ID 生成器表达式完成的。我们在变量 gcc_like_cxxmsvc_cxx 中设置结果如下:

# 将一个生成表达式赋值给变量 gcc_like_cxx。
# COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC 是一个生成表达式 
# 它用于检查当前编译器是否与其中任何一个指定的编译器相关。具体来说,它检查当前编译器是否是C++编译器(CXX)
# 并且是否与 ARMClang、AppleClang、Clang、GNU、LCC中的任何一个匹配
# 如果当前编译器匹配这些条件,那么 gcc_like_cxx 变量的值将被设置为 1(表示真),否则将被设置为 0(表示假)

# Create helper variables to determine which compiler we are using:
# * Create a new variable gcc_like_cxx that is true if we are using CXX and
#   any of the following compilers: ARMClang, AppleClang, Clang, GNU, LCC
set(gcc_like_cxx "$")
# * Create a new variable msvc_cxx that is true if we are using CXX and MSVC
set(msvc_cxx "$")

接下来,我们添加项目所需的编译器警告标志(这里说的警告标志是下面的 -Wall 等警告标志)。

  1. 通用警告标志:
    • -Wall:启用所有常规警告。
    • -Wextra:启用额外的警告。
    • -Werror:将警告视为错误。
    • -pedantic:启用更严格的警告。
    • -Wno-:禁用特定警告,例如 -Wno-unused-variable 可以禁用未使用变量的警告。
  2. 警告级别:
    • -W1-W2-W3:设置警告级别,例如 -W3 表示启用大多数警告。
  3. 特定警告:
    --W:启用特定类型的警告,例如 -Wunused-variable 启用未使用变量的警告。
  4. 禁用警告:
    • -w-Wno-*:禁用所有警告或特定警告。
  • target_compile_options():向目标添加编译选项。
    • target_compile_options( [BEFORE] [items1...] [ [items2...] ...])

使用变量 gcc_like_cxxmsvc_cxx,我们可以使用另一个生成器表达式,仅在变量为真时应用各自的标志。我们使用 target_compile_options() 将这些标志应用于接口库。

# Add warning flag compile options to the interface library tutorial_compiler_flags.
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>"
  "$<${msvc_cxx}:-W3>"
)

最后,我们只希望在构建期间使用这些警告标志。已安装项目的使用者(Consumers of our installed project) 不应该继承我们的警告标志(警告标志仅是在我们编译库时使用的)。为了指定这一点,我们使用 BUILD_INTERFACE 条件将标志包装在生成器表达式中。生成的完整代码如下所示:

# With nested generator expressions, only use the flags for the build-tree
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:$>"
  "$<${msvc_cxx}:$>"
)

5. Installing and Testing(Step 5)-安装和测试

通常,仅仅构建可执行文件是不够的,它还应该是可安装的。使用 CMake,我们可以使用 install() 命令指定安装规则。在 CMake 中支持本地安装通常就像指定安装位置和要安装的目标和文件一样简单。

现在,我们可以开始给我们的项目添加 安装规则(install rules)测试支持(testing support)

Install Rules

安装规则相当简单:对于 MathFunctions,我们想安装库和头文件;对于应用程序,我们想安装可执行文件和配置好的头文件。

所以在 MathFunctions/CMakeLists.txt 文件的末尾添加:

# Create a variable called installable_libs that is a list of all
# libraries we want to install (e.g. MathFunctions and tutorial_compiler_flags)
# Then install the installable libraries to the lib folder.
set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
# Install the library headers to the include folder.
install(FILES MathFunctions.h DESTINATION include)

并在最上层目录下的 CMakeLists.txt 文件末尾处添加:

# Install Tutorial in the bin directory
install(TARGETS Tutorial DESTINATION bin)
# Install TutorialConfig.h to the include directory
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  DESTINATION include
  )

这就是创建一个基本的本地安装规则所需的全部内容。

现在,运行 cmakecmake-gui 配置并构建项目。然后,在命令行中使用 cmake 命令的 install 选项(CMake 3.15 中引入,旧版的 CMake 必须使用 make install 等本地的构建命令)。对于多配置的工具,不要忘了使用 --config 参数时指定配置。如果使用一个 IDE,你只需要构建 INSTALL 这个目标。这将安装合适的头文件、库和可执行文件。例如:

cmake --install .

默认情况下,需要以管理员身份运行,否则可能会权限不足。

对于多配置工具,不要忘记使用 --config 参数来指定配置。

cmake --install . --config Release

如果使用 IDE,只需构建 INSTALL 目标。您可以从命令行构建相同的安装目标,如下所示:

cmake --build . --target install --config Debug

CMake 变量 CMAKE_INSTALL_PREFIX 用于决定这些文件被安装到哪里,它是一个安装前缀,即一个目录。如果使用 cmake --install 命令,可以通过 --prefix 参数重写安装前缀。例如:

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

导航到 install 目录,并验证安装的 Tutorial 是否运行。

在运行以上指令后,可以看到在对应路径下的文件如下:
CMake 官方完整版_第3张图片

Testing Support

CTest 提供了一种轻松管理项目测试的方法。可以通过 add_test() 命令添加测试。尽管在本教程中没有明确介绍,但 CTest 和其他测试框架(如 GoogleTest)之间有很多兼容性。

接下来让我们测试我们的应用程序。在顶层 CMakeLists.txt 文件的末尾,我们首先需要使用 enable_testing() 命令启用测试。

enable_testing()

启用测试后,我们将添加一些基本测试来验证应用程序是否正常工作。首先,我们使用 add_test() 创建一个测试,它运行教程可执行文件,并传入参数 25 25 25。对于这个测试,我们不打算检查可执行程序的计算答案。此测试将验证应用程序是否运行,没有出现分段故障或以其他方式崩溃,并且返回值为零。这是 CTest 测试的基本形式。

  • add_test:将测试添加到要由 ctest(1) 运行的项目中。
    • add_test(NAME COMMAND [...] [CONFIGURATIONS ...] [WORKING_DIRECTORY ] [COMMAND_EXPAND_LISTS])
    • 其中第二项是测试名,自己命名的。
# Add a test called Runs which runs the following command:$ Tutorial 25
add_test(NAME Runs COMMAND Tutorial 25)

接下来,让我们使用 PASS_REGULAR_EXPRESSION 测试属性来验证测试的输出是否包含某些字符串。在本例中,验证当提供的参数数量不正确时是否打印 usage 消息

# Add a test called Usage which runs the following command:$ Tutorial 25
add_test(NAME Usage COMMAND Tutorial)
# Make sure the expected output is displayed.
set_tests_properties(Usage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
  )

我们将添加的下一个测试验证计算值确实是平方根

add_test(NAME StandardUse COMMAND Tutorial 4)
set_tests_properties(StandardUse
  PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2"
  )

这一个测试还不足以让我们确信它对传入的所有值都有效。我们应该增加更多的测试来验证这一点。为了方便地添加更多测试,我们创建了一个名为 do_test 的函数,该函数运行应用程序并验证给定输入的计算平方根是否正确。对于 do_test 的每次调用,另一个带有名称、输入和基于传递的参数的预期结果的测试被添加到项目中。

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 a bunch of result based tests
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is (-nan|nan|0)")
do_test(Tutorial 0.0001 "0.0001 is 0.01")

CTest 运行方式

导航到构建目录并重新构建应用程序。然后,运行 ctest 可执行文件:ctest -Nctest -VV。对于多配置生成器(例如 Visual Studio),必须使用 -C 标志指定配置类型。例如,要在 Debug 模式下运行测试,请从构建目录(而不是 Debug 子目录!)使用 ctest -C Debug -VV。Release 模式将从相同的位置执行,但使用 -C Release。或者,从 IDE 构建 RUN_TESTS 目标。

  • CTest:
    • -N, --show-only[=]
      禁用测试的实际执行。
      这个选项告诉 CTest 列出将要运行但没有实际运行的测试。与 -R-E 选项一起使用。
    • -VV, --extra-verbose
      从测试中启用更详细的输出。
      测试输出通常被抑制,只显示摘要信息。这个选项将显示更多的测试输出。
ctest -N

使用 ctest -N 输出如下:
CMake 官方完整版_第4张图片

ctest -VV

使用 ctest -VV 输出如下(删减版):
CMake 官方完整版_第5张图片

6. Adding Support for a Testing Dashboard(Step 6)-添加测试仪表盘支持

讲道理这一步作用不是很清楚,不像很有用的样子。

添加将测试结果提交到 dashboard 的支持很简单。我们已经在上一节测试支持中为我们的项目定义了许多测试。现在我们只需要运行这些测试并将它们提交给 CDash。

我们很容易将测试结果提交到一个仪表盘上。我们已经为我们的项目定义了测试编号。现在,我们只需要运行那些测试并将它们提交到一个仪表盘。为了包含对仪表盘的支持,我们在顶层目录下的 CMakeLists.txt 中包含了 CTest 模块。

将:

# enable testing
enable_testing()

替换为:

# enable dashboard scripting
include(CTest)

CTest 模块将自动调用 enable_testing(),所以我们可以从 CMake 文件中移除它。

我们也需要在顶层目录下创建一个 CTestConfig.cmake 文件,在这个文件中指定项目名和要提交到的仪表盘。

ctest [-VV] -D Experimental

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

ctest [-VV] -C Debug -D Experimental

或者从 IDE 中构建 Experimental 目标。

CTest 将构建和测试该项目,然后提交测试结果到 Kitware 的公共仪表盘。

7. Adding System Introspection(Step 7)-添加系统自省

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

在这个练习中,我们将使用 CheckCXXSourceCompiles 模块中的函数,所以首先我们必须将它包含在 MathFunctions/CMakeLists.txt 中。

include(CheckCXXSourceCompiles)

然后使用 check_cxx_compiles_source 测试 logexp 的可用性。这个函数允许我们在真正的源代码编译之前尝试编译带有所需依赖项的简单代码。结果变量 HAVE_LOGHAVE_EXP 表示这些依赖项是否可用

 # Use check_cxx_source_compiles with simple C++ code to verify availability of:
 # * std::log
 # * std::exp
 # Store the results in HAVE_LOG and HAVE_EXP respectively.
check_cxx_source_compiles("
  #include 
  int main() {
    std::log(1.0);
    return 0;
  }
" HAVE_LOG)
check_cxx_source_compiles("
  #include 
  int main() {
    std::exp(1.0);
    return 0;
  }
" HAVE_EXP)

接下来,我们需要将这些 CMake 变量传递给我们的源代码。这样,我们的源代码就可以知道哪些资源是可用的。如果 logexp 都可用,使用 target_compile_definitions()HAVE_LOGHAVE_EXP 指定为 PRIVATE 编译定义。

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

因为我们可能会使用 logexp,所以我们需要修改 mysqrt.cxx 包含 cmath

#include 

如果系统上有 logexp,那么在 mysqrt 函数中使用它们来计算平方根。MathFunctions/mysqrt.cxx 中的 mysqrt 函数看起来如下:

#if defined(HAVE_LOG) && defined(HAVE_EXP)
  double result = std::exp(std::log(x) * 0.5);
  std::cout << "Computing sqrt of " << x << " to be " << result
            << " using log and exp" << std::endl;
#else
  double result = 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;
  }
#endif

8. Adding a Custom Command and Generated File(Step 8)-增加自定义命令并生成文件

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

首先,让我们删除 MathFunctions/CMakeLists.txt 中对 logexp 函数的检查。然后从 mysqrt.cxx 中删除对 HAVE_LOGHAVE_EXP 的检查。同时,我们可以删除 #include

MathFunctions 子目录中,有一个名为 MakeTable.cxx 的新源文件被提供来生成表格。

检查完文件后,我们可以看到表是作为有效的 C++ 代码生成的,并且输出文件名作为参数传入。

下一步是创建 MathFunctions/MakeTable.cmake。然后,将适当的命令添加到文件中以构建 MakeTable 可执行文件,然后将其作为构建过程的一部分运行。需要几个命令来完成这个任务。

首先,我们为 MakeTable 添加一个可执行文件。

add_executable(MakeTable MakeTable.cxx)

在创建可执行文件之后,我们使用 target_link_libraries()tutorial_compiler_flags 添加到可执行文件中。

target_link_libraries(MakeTable PRIVATE tutorial_compiler_flags)

然后添加自定义命令,它规定了在运行 MakeTable 时如何产生 Table.h

  • add_custom_command:向生成的构建系统添加自定义构建规则。
    1. add_custom_command(OUTPUT output1 [output2 ...] COMMAND command1 [ARGS] [args1...] [COMMAND command2 [ARGS] [args2...] ...] [MAIN_DEPENDENCY depend] [DEPENDS [depends...]] [BYPRODUCTS [files...]] [IMPLICIT_DEPENDS depend1 [ depend2] ...]
      • 这个签名用于添加自定义命令以产生输出。
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.hMathFunctions 库的源代码列表中去完成这一任务。(如果不添加这一步,仅有 include(MakeTable.cmake) 并不会生成 Table.h 文件)

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

我们还需要添加当前的 binary(就是 build 文件夹下的 MathFunctions)目录到 include 目录列表,以便 Table.h 可以被找到并被 mysqrt.cxx 包含。

  target_include_directories(SqrtLibrary PRIVATE
                             ${CMAKE_CURRENT_BINARY_DIR}
                             )

作为最后一步,我们需要包含 MakeTable。在 MathFunctions/CMakeLists.txt 的顶部。

  include(MakeTable.cmake)

现在让我们使用被生成的表。首先,修改 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。它包含了 Table.h 来生成 MathFunctions 库。

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

生成的 Table.h 如下所示:
CMake 官方完整版_第6张图片

9. Building an Installer(Step 9)-构建一个安装程序

(感觉这个功能就是方便打包给别人的 install,如果使用 cmake --install 的话,还需要设置 prefix 然后再打包,过于麻烦)

接下来假设我们想要将我们的项目分发给其他人,以便他们可以使用它。我们希望在各种平台上提供二进制和源代码发行版。这与我们之前在安装和测试中所做的安装略有不同,在安装中我们安装了从源代码构建的二进制文件。在本例中,我们将构建支持二进制安装和包管理特性的安装包。为此,我们将使用 CPack 来创建特定于平台的安装程序(还是当前平台而不是跨平台的意思,该功能主要用于帮助管理项目在不同平台上的构建和运行依赖关系,但它本身并不能保证可执行文件在不同平台上的通用性)。具体来说,我们需要在顶层 CMakeLists.txt 文件的底部添加几行。

  • InstallRequiredSystemLibraries:包含这个模块来搜索编译器提供的系统运行时库,并为它们添加安装规则。可以在包含模块之前设置一些可选变量来调整行为(这通常用于帮助确保项目在不同的系统上能够正常构建和运行):
    • CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS:指定可能未检测到的其他运行时库。在包含之后,任何检测到的库都将追加到此。
    • CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP:设置为 TRUE 将跳过调用 install(PROGRAMS) 命令以允许包含程序指定其自己的安装规则,使用 CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS 的值来获取库列表。
    • CMAKE_INSTALL_DEBUG_LIBRARIES:当 MSVC 工具可用时,设置为 TRUE 安装调试运行时库。
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}")
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)

这就是它的全部。我们从包含 InstallRequiredSystemLibraries 开始。此模块将包含当前平台上项目所需的任何运行时库。接下来,我们将一些 CPack 变量设置到存储此项目的许可证和版本信息的位置。本教程前面设置了版本信息,并且 License.txt 已包含在此步骤的顶层源代码目录中。CPACK_SOURCE_GENERATOR 变量为源包选择一种文件格式。(TGZ 即 Tarball 文件(通常是一个 .tar.gz 压缩文件))

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

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

cpack

结果如下,生成了压缩包 Tutorial-1.0-Linux.tar.gzTutorial-1.0-Linux.tar.Z
CMake 官方完整版_第7张图片

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

cpack -G ZIP -C Debug

结果如下,生成了功能包 Tutorial-1.0-Linux.zip,可以看到可执行程序的版本号:
CMake 官方完整版_第8张图片解压后的文件如下,可以看到与 Step 5 安装规则得到的相同。(但奇怪的一点是,License.txt 并没有加入到安装包内):
CMake 官方完整版_第9张图片

有关可用生成器的列表,请参阅 cpack-generators(7) 或调用 cpack --help。像 ZIP 这样的归档生成器创建所有已安装文件的压缩归档。

要创建完整源代码树的存档,您可以输入:

cpack --config CPackSourceConfig.cmake

或者,运行 make package 或右键单击 Package 目标并在 IDEBuild Project

运行二进制目录中的安装程序。然后运行已安装的可执行文件并验证它是否有效。

10. Selecting Static or Shared Libraries(Step 10)-混合静态库和动态库

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

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

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

接下来,我们需要为静态库和共享库指定输出目录。

  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY
    • 用于指定存放静态库(archive 文件,通常以.a或.lib为扩展名)的输出目录
    • 静态库是编译后的代码库,它包含了函数和符号,可以在链接时与其他可执行程序或动态库一起使用。
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY
    • 用于指定存放共享库(dynamic library,通常以.so、.dll或.dylib为扩展名)的输出目录
    • 共享库是一种可以在运行时被动态加载的库,可以被多个可执行程序或其他共享库使用。
    • LIBRARY_OUTPUT_PATH 是老版本的写法,现不再推荐使用。使用官方建议的变量可以提高项目的一致性,因为它们符合 CMake 的最新标准和最佳实践。
  • CMAKE_RUNTIME_OUTPUT_DIRECTORY
    • 用于指定存放可执行程序(runtime 可执行文件)的输出目录
    • 可执行程序是编译后的应用程序,它们是最终用户可以运行的文件。
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)

最后,更新 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);
}

此时,如果您构建了所有内容,您可能会注意到链接失败,因为我们正在将没有位置无关代码的静态库与具有位置无关代码的库组合在一起。解决这个问题的方法是在构建共享库时显式地将 SqrtLibraryPOSITION_INDEPENDENT_CODE 目标属性设置为 True

target_link_libraries(MathFunctions PUBLIC tutorial_compiler_flags)

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

11. Adding Export Configuration (Step 11)-添加导出配置

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

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

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

set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()
# 这里做了改动,添加了 EXPORT MathFunctionsTargets
install(TARGETS ${installable_libs}
        DESTINATION lib
        EXPORT MathFunctionsTargets)
install(FILES MathFunctions.h DESTINATION include)

现在我们已经导出了 MathFunctions(这时候已生成 MathFunctionsTargets.cmake,还需要将其添加至 install,便于本地安装和打包),我们还需要显式安装生成的 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 想说的是,在生成导出信息期间,它将导出一个与当前机器本质上绑定的路径,并且在其他机器上无效(绑定了一个绝对路径的意思?没想到在 Step 3 看到的问题在这里有解决方法)。这个问题的解决方案是更新 MathFunctionstarget_include_directories()以了解它在从构建目录和从一个 install/package 中使用时需要不同的 INTERFACE 位置。这意味着将 MathFunctionstarget_include_directories() 调用转换为如下样子(原来的是 target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})):

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

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

此时,我们已经让 CMake 正确地打包了所需的目标信息,但我们仍然需要生成一个 MathFunctionsConfig。这样cmake find_package() 命令可以找到我们的项目。因此,让我们继续并在项目的顶层添加一个名为 Config.cmake.in 的新文件,其内容如下:

@PACKAGE_INIT@

include ( "${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake" )

然后,正确配置并安装那个文件,添加下面的代码到顶层 CMakeLists.txt 的底部:

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)

include(CMakePackageConfigHelpers)

接下来,我们执行 configure_package_config_file()。该命令将配置所提供的文件,但与标准的 configure_file() 方式有一些具体的区别。为了正确地利用这个功能,除了所需的内容外,输入文件还应该有一行文本 @PACKAGE_INIT@。该变量将被替换为一个代码块,该代码块将集合值转换为相对路径。这些新值可以用相同的名称引用,但要加上 PACKAGE_ 前缀。

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)

include(CMakePackageConfigHelpers)
# generate the config file that 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
  )

write_basic_package_version_file() 是下一个。这个命令写一个文件供 find_package() 使用,记录所需包的版本和兼容性。在这里,我们使用 Tutorial_VERSION_* 变量,并说它与 AnyNewerVersion 兼容,这表示该版本或任何更高的版本与请求的版本兼容。

write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  COMPATIBILITY AnyNewerVersion
)

最后,设置两个生成的文件都要安装:

install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
  DESTINATION lib/cmake/MathFunctions
  )

至此,我们已经为我们的项目生成了一个可重新定位的 CMake 配置,它可以在项目安装或打包后使用。如果我们想让我们的项目也从构建目录中使用,我们只需要在顶层 CMakeLists.txt 的底部添加以下内容:

export(EXPORT MathFunctionsTargets
  FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

通过这个导出调用,我们现在生成一个 MathFunctionsTargets。允许配置 MathFunctionsConfig.Cmake 在构建目录中,以供其他项目使用,而无需安装它。

12. Packaging Debug and Release (Step 12)-打包调试和发布

注意:这个例子只能用于单配置生成器,不能工作于多配置生成器(例如 Visual Studio)。

默认形况下,CMake 的构建目录只能包含一个单独的配置,可以是 DebugReleaseMinSizeRelRelWithDebInfo。然而可以让 CPack 打包多个构建目录,并构造一个包含了当前项目的多个配置的包

首先,我们要确保 debugrelease 构建为可执行目标和库目标使用不同的名字。我们为调试的可执行目标和库目标添加后缀 d

在顶层 CMakeLists.txt 的开始设置 CMAKE_DEBUG_POSTFIX

set(CMAKE_DEBUG_POSTFIX d)

add_library(tutorial_compiler_flags INTERFACE)

以及教程内可执行文件的 DEBUG_POSTFIX 属性:

add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})

target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)

同时,添加版本号到 MathFunctions 库。在 MathFunctions/CMakeLists.txt 中,设置 VERSIONSOVERSION 属性:

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

在 Step12 目录下,创建 debugrelease 子目录。目录布局为:

- Step12
   - debug
   - release

现在,我们需要设置 debugrelease 构建。我们可以使用 CMAKE_BUILD_TYPE 去设置配置类型:

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

现在,debugrelease 构建都已经完成,区别如下,可以看到 debug 的可执行文件、动静态库名后多一个 d
CMake 官方完整版_第10张图片

我们可以使用一个自定义配置文件去统一发布这两个构建。在 Step12 目录中,创建一个名为 MultiCPackConfig.cmake 的文件。在这个文件中,先包含被 cmake 创建默认的配置文件(include("release/CPackConfig.cmake"))。

然后,使用 CPACK_INSTALL_CMAKE_PROJECTS 变量去指定要安装的项目。此时,我们希望同时安装 debugrelease

include("release/CPackConfig.cmake")

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

在 Step12 目录下,运行 cpack,通过 config 选项指定我们自定义的配置文件:

cpack --config MultiCPackConfig.cmake

打包的文件即对应路径如下:
CMake 官方完整版_第11张图片

你可能感兴趣的:(C/C++,linux,c语言,c++,cmake)