第 4 步:深入了解 CMake 目标命令¶
在 CMake 中,我们可以使用多种目标命令来描述构建需求。提醒一下,目标命令是指用于修改其作用目标属性的命令。这些属性描述了构建软件所需的条件(如源文件、编译标志和输出名称),或使用该目标所需的属性(如头文件包含路径、库目录和链接规则)。
注意
正如 Step1 中所讨论的,构建目标所需的属性应使用 PRIVATE 作用域关键字 来描述,使用该目标所需的属性应使用 INTERFACE 描述,而两者兼有的属性则使用 PUBLIC 描述。
在这一步中,我们将详细介绍 CMake 中所有可用的目标命令。并非所有的目标命令都同等重要。我们已经讨论了两个最重要的目标命令:target_sources() 和 target_link_libraries()。在剩余的命令中,有些命令几乎与这两个一样常用,有些具有更高级的应用场景,还有几个命令仅应在没有其他可用选项时作为最后手段使用。
背景¶
在进一步探讨之前,让我们列出所有 CMake 目标命令。我们将它们分为三组:推荐且通用的常用命令、高级且需谨慎使用的命令,以及除非必要否则应避免使用的“陷阱”命令。
注意
不存在所谓的“坏”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_LIBRARIES 和 INTERFACE_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() 使用与其他目标命令相同的接口属性和非接口属性样式。这意味着可以通过 INTERFACE 或 PUBLIC 作用域关键字来继承指定的语言标准要求。
如果语言特性仅在实现文件中使用,则相应的编译特性应设为 PRIVATE。如果目标的头文件使用了这些特性,则应使用 PUBLIC 或 INTERFACE。
对于 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.txtMathFunctions/CMakeLists.txtMathFunctions/MathFunctions.cxxCMakePresets.json
开始¶
Help/guide/tutorial/Step4 目录包含了 Step3 的完整推荐解决方案,以及本步骤的相关 TODO。完成 TODO 1 到 TODO 8。
构建并运行¶
我们可以使用 tutorial 预设运行 CMake,然后像往常一样进行构建。
cmake --preset tutorial
cmake --build build
验证 Tutorial 的输出是否符合我们对 std::sqrt 的预期。
解决方案¶
首先,我们在顶层 CMakeLists.txt 中添加一个新选项。
TODO 1:点击显示/隐藏答案
option(TUTORIAL_BUILD_UTILITIES "Build the Tutorial executable" ON)
option(TUTORIAL_USE_STD_SQRT "Use std::sqrt" OFF)
然后我们将编译特性和定义添加到 MathFunctions 中。
TODO 2-3: 点击显示/隐藏答案
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:点击显示/隐藏答案
target_compile_features(Tutorial PRIVATE cxx_std_20)
现在我们可以修改 MathFunctions 以利用这个新定义。
TODO 5-6: 点击显示/隐藏答案
#include <cmath>
#include <format>
#include <iostream>
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: 点击显示/隐藏答案
"cacheVariables": {
"TUTORIAL_USE_STD_SQRT": "ON"
}
练习 2 - 编译与链接选项¶
有时,我们需要对传递给编译和链接命令的具体选项进行精确控制。这种情况可以通过 target_compile_options() 和 target_link_options() 来处理。
target_compile_options(MyApp PRIVATE -Wall -Werror)
target_link_options(MyApp PRIVATE -T LinksScript.ld)
无条件调用 target_compile_options() 或 target_link_options() 会带来几个问题。主要问题是编译器标志是特定于所使用的编译器前端的。为了确保我们的项目支持多个编译器前端,我们必须只向编译器传递兼容的标志。
我们可以通过检查 CMAKE_<LANG>_COMPILER_FRONTEND_VARIANT 变量来实现这一点,该变量告诉我们编译器前端支持的标志样式。
注意
在 CMake 3.26 之前,CMAKE_<LANG>_COMPILER_FRONTEND_VARIANT 仅针对具有多个前端变体的编译器设置。在 CMake 3.26 之后的版本中,仅检查此变量就足够了。
然而,本教程针对的是 CMake 3.23。因此,逻辑比我们此处有时间介绍的更为复杂。本教程步骤已经包含了针对 CMake 3.23 上 MSVC、GCC、Clang 和 AppleClang 的编译器变体检查逻辑。
即使编译器接受了我们传递的标志,编译器标志的语义也会随着时间而改变。在警告方面尤其如此。项目不应默认开启“将警告视为错误”标志,因为这可能会在后续版本中出现无关紧要的编译器警告时导致构建失败。
注意
对于错误和警告,请考虑将标志放置在 CMAKE_<LANG>_FLAGS 中,用于本地开发构建和 CI 运行(通过预设或 -D 标志)。在这种上下文中,我们确切地知道使用了哪些编译器和工具链,因此可以精确地自定义行为,而不会冒在其他平台上破坏构建的风险。
目标¶
为 MSVC 和 GNU 风格的编译器前端向 Tutorial 可执行文件添加适当的警告标志。
有用资源¶
要编辑的文件¶
Tutorial/CMakeLists.txt
开始¶
继续编辑 Step4 目录中的文件。检查前端变体的条件逻辑已经编写好了。完成 TODO 9 和 TODO 10,为 Tutorial 添加警告标志。
构建并运行¶
由于我们已经配置过此步骤,我们可以使用通常的命令进行构建。
cmake --build build
这应该会揭示构建中的一个简单警告。你可以去修复它。
解决方案¶
我们需要为 Tutorial 添加两个编译选项:一个 MSVC 风格标志和一个 GNU 风格标志。
TODO 9-10: 点击显示/隐藏答案
if(
(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") OR
(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
)
target_compile_options(Tutorial PRIVATE /W3)
elseif(
(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
)
target_compile_options(Tutorial PRIVATE -Wall)
endif()
练习 3 - 包含路径与链接目录¶
注意
本练习要求直接在命令行上使用编译器构建一个归档文件。它在后续步骤中不会被使用,仅用于演示 target_include_directories() 和 target_link_directories() 的使用场景。
如果由于某种原因无法完成此练习,请随意将其视为仅供参考,或完全跳过它。
通常不需要直接描述包含路径和链接目录,因为当链接在 CMake 中生成的目标或从外部依赖项(通过稍后介绍的命令导入 CMake)中导入时,这些要求会自动继承。
如果我们恰好有一些未由 CMake 目标描述的库或头文件需要引入构建中(例如供应商提供的预编译二进制文件),我们可以使用 target_link_directories() 和 target_include_directories() 命令来将其整合。
target_link_directories(MyApp PRIVATE Vendor/lib)
target_include_directories(MyApp PRIVATE Vendor/include)
这些命令使用的属性映射到 -L 和 -I 编译器标志(或编译器用于链接和包含目录的任何标志)。
当然,传递链接目录并不会告诉编译器将任何东西链接到构建中。为此我们需要 target_link_libraries()。当 target_link_libraries() 接收到一个不映射到目标名称的参数时,它会将该字符串直接添加到链接行中,作为要链接到构建中的库(并预置任何适当的标志,例如 -l)。
目标¶
在项目中使用 target_link_directories() 和 target_include_directories() 描述一个预编译的第三方静态库及其头文件。
有用资源¶
要编辑的文件¶
Vendor/CMakeLists.txtTutorial/CMakeLists.txt
入门¶
你需要将第三方库构建为静态归档文件才能完成此练习。导航到 Help/guide/tutorial/Step4/Vendor/lib 目录,并根据你的平台构建代码。
类 Unix 系统上 GCC 工具链的典型命令是
g++ -c Vendor.cxx
ar rvs libVendor.a Vendor.o
同样,Windows 上 MSVC 工具链的示例命令是
cl -c Vendor.cxx
lib -out:Vendor.lib Vendor.obj
在此处,由于你直接调用了 cl 和 lib,请确保使用与此 CMake 项目所使用的目标架构相匹配的 Visual Studio 开发人员命令提示符。
然后完成 TODO 11 到 TODO 14。
注意
VendorLib 是一个 INTERFACE 库,这意味着它没有构建要求(因为它已经被构建了)。其所有属性也应该是接口属性。
我们将在下一步深入讨论 INTERFACE 库。
构建和运行¶
如果你已经成功构建了 libVendor,可以使用普通命令重新构建 Tutorial。
cmake --build build
运行 Tutorial 现在应该会输出一条关于结果对供应商的可接受性的消息。
解决方案¶
我们需要使用目标链接和包含命令,将归档文件及其头文件描述为 VendorLib 的 INTERFACE 要求。
TODO 11-13: 点击显示/隐藏答案
target_include_directories(VendorLib
INTERFACE
include
)
target_link_directories(VendorLib
INTERFACE
lib
)
target_link_libraries(VendorLib
INTERFACE
Vendor
)
然后我们可以将 VendorLib 添加到 Tutorial 的链接库中。
TODO 14: 点击显示/隐藏答案
target_link_libraries(Tutorial
PRIVATE
MathFunctions
VendorLib
)