第四步:深入 CMake 目标命令

CMake 中有几个目标命令可用于描述需求。作为提醒,目标命令是指修改其应用的目标的属性的命令。这些属性描述了构建软件所需的需求,例如源文件、编译标志和输出名称;或者消耗目标所必需的属性,例如头文件包含、库目录和链接规则。

注意

正如在 第一步 中讨论的,构建目标所需属性应使用 PRIVATE 作用域关键字 描述,消耗目标所需属性使用 INTERFACE 描述,而两者都需要的属性则使用 PUBLIC 描述。

在本步中,我们将回顾 CMake 中所有可用的目标命令。并非所有目标命令都是相同的。我们已经讨论了两个最重要的目标命令:target_sources()target_link_libraries()。在剩余的命令中,有些几乎和这两个一样常用,有些则有更高级的应用,还有几个只有在其他选项不可用时才应作为最后的手段使用。

背景

在继续之前,让我们列出所有 CMake 目标命令。我们将它们分为三类:推荐的且普遍有用的命令,高级且需要谨慎的命令,以及“陷阱”命令,除非必要应避免使用。

常用/推荐

高级/谨慎

晦涩/陷阱

target_compile_definitions() target_compile_features() target_link_libraries() target_sources()

get_target_property() set_target_properties() target_compile_options() target_link_options() target_precompile_headers()

target_include_directories() target_link_directories()

注意

“坏”的 CMake 目标命令是不存在的。它们都有有效的用例。这种分类是为了让新手对在解决问题时首先应考虑哪些命令有一个简单的直观认识。

我们将在接下来的练习中演示其中大部分。我们不会使用的三个命令是 get_target_property()set_target_properties()target_precompile_headers(),因此我们在此简要讨论它们的用途。

get_target_property()set_target_properties() 命令可以通过名称直接访问目标的属性。它们甚至可以用于将任意属性名称附加到目标。

add_library(Example)
set_target_properties(Example
  PROPERTIES
    Key Value
    Hello World
)

get_target_property(KeyVar Example Key)
get_target_property(HelloVar Example Hello)

message("Key: ${KeyVar}")
message("Hello: ${HelloVar}")
$ cmake -B build
...
Key: Value
Hello: World

CMake 语义上有意义的目标属性的完整列表已记录在 cmake-properties(7) 中,但是其中大多数应该使用其专用命令进行修改。例如,直接操作 LINK_LIBRARIESINTERFACE_LINK_LIBRARIES 是不必要的,因为这些由 target_link_libraries() 处理。

相反,一些不太常用的属性只能通过这些命令访问。用于将弃用通知附加到目标的 DEPRECATION 属性只能通过 set_target_properties() 设置;用于描述 CMake 的 clean 目标要移除的附加文件的 ADDITIONAL_CLEAN_FILES 属性也可以;以及其他类似的属性。

target_precompile_headers() 命令接受一个头文件列表,类似于 target_sources(),并从中创建一个预编译头。然后,此预编译头将被强制包含到目标的所有翻译单元中。这对于构建性能可能很有用。

练习 1 - 特性和定义

在之前的步骤中,我们曾警告不要全局设置 CMAKE_<LANG>_STANDARD 并覆盖打包者关于使用哪种语言标准的选择。另一方面,许多库在构建时需要一组最少的必需特性,对于这些库,使用 target_compile_features() 命令来传达这些需求是合适的。

target_compile_features(MyApp PRIVATE cxx_std_20)

target_compile_features() 命令将最低语言标准描述为目标属性。如果 CMAKE_<LANG>_STANDARD 高于此版本,或者编译器默认已提供此语言标准,则不执行任何操作。如果需要额外的标志来启用该标准,CMake 将会添加它们。

注意

target_compile_features() 操作的接口和非接口属性与其它目标命令相同。这意味着可以通过 INTERFACEPUBLIC 作用域关键字指定的语言标准要求进行继承

如果语言特性仅在实现文件中使用,则相应的编译特性应为 PRIVATE。如果目标的头文件使用这些特性,则应使用 PUBLICINTERFACE

对于 C++,编译特性的形式为 cxx_std_YY,其中 YY 是标准化年份,例如 141720 等。

target_compile_definitions() 命令将编译定义描述为目标属性。它是将构建配置信息传达给源代码本身的机制。与所有属性一样,作用域关键字按照我们所讨论的方式适用。

target_compile_definitions(MyLibrary
  PRIVATE
    MYLIBRARY_USE_EXPERIMENTAL_IMPLEMENTATION

  PUBLIC
    MYLIBRARY_EXCLUDE_DEPRECATED_FUNCTIONS
)

使用 target_compile_definitions() 描述的编译定义,既不需要也不希望添加 -D 前缀。CMake 将为当前编译器确定正确的标志。

目标

使用 target_compile_features()target_compile_definitions() 来传达语言标准和编译定义的需求。

有用资源

要编辑的文件

  • 教程/CMakeLists.txt

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MathFunctions.cxx

  • CMakePresets.json

开始

目录 Help/guide/tutorial/Step4 包含 第三步 的完整推荐解决方案以及本步相关的 TODO。完成 TODO 1TODO 8

构建并运行

我们可以使用我们的 tutorial 预设运行 CMake,然后像平常一样构建。

cmake --preset tutorial
cmake --build build

验证 Tutorial 的输出是否符合我们对 std::sqrt 的预期。

解决方案

首先,我们在顶层 CML 中添加一个新的选项。

TODO 1:点击显示/隐藏答案
TODO 1: CMakeLists.txt
option(TUTORIAL_BUILD_UTILITIES "Build the Tutorial executable" ON)
option(TUTORIAL_USE_STD_SQRT "Use std::sqrt" OFF)

然后,我们将编译特性和定义添加到 MathFunctions

TODO 2-3:点击显示/隐藏答案
TODO 2-3: MathFunctions/CMakeLists.txt
target_compile_features(MathFunctions PRIVATE cxx_std_20)

if(TUTORIAL_USE_STD_SQRT)
  target_compile_definitions(MathFunctions PRIVATE TUTORIAL_USE_STD_SQRT)
endif()

以及 Tutorial 的编译特性。

TODO 4:点击显示/隐藏答案
TODO 4: Tutorial/CMakeLists.txt
target_compile_features(Tutorial PRIVATE cxx_std_20)

现在我们可以修改 MathFunctions 来利用新的定义。

TODO 5-6:点击显示/隐藏答案
TODO 5: MathFunctions/MathFunctions.cxx
#include <cmath>
#include <format>
#include <iostream>
TODO 6: MathFunctions/MathFunctions.cxx
double sqrt(double x)
{
#ifdef TUTORIAL_USE_STD_SQRT
  return std::sqrt(x);
#else
  return mysqrt(x);
#endif
}

最后,我们可以更新我们的 CMakePresets.json。我们不再需要设置 CMAKE_CXX_STANDARD,但我们确实想尝试我们新的编译定义。

TODO 7-8:点击显示/隐藏答案
TODO 7-8: CMakePresets.json
"cacheVariables": {
  "TUTORIAL_USE_STD_SQRT": "ON"
}