第 4 步:深入了解 CMake 目标命令

在 CMake 中,我们可以使用多种目标命令来描述构建需求。提醒一下,目标命令是指用于修改其作用目标属性的命令。这些属性描述了构建软件所需的条件(如源文件、编译标志和输出名称),或使用该目标所需的属性(如头文件包含路径、库目录和链接规则)。

注意

正如 Step1 中所讨论的,构建目标所需的属性应使用 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() 设置;同样,ADDITIONAL_CLEAN_FILES(用于描述由 CMake clean 目标删除的额外文件)以及其他此类属性也是如此。

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 是标准化年份,例如 14, 17, 20 等。

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() 来传达语言标准和编译定义要求。

有用资源

要编辑的文件

  • Tutorial/CMakeLists.txt

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MathFunctions.cxx

  • CMakePresets.json

开始

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

构建并运行

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

cmake --preset tutorial
cmake --build build

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

解决方案

首先,我们在顶层 CMakeLists.txt 中添加一个新选项。

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