第四步:深入 CMake 目标命令¶
CMake 中有几个目标命令可用于描述需求。作为提醒,目标命令是指修改其应用的目标的属性的命令。这些属性描述了构建软件所需的需求,例如源文件、编译标志和输出名称;或者消耗目标所必需的属性,例如头文件包含、库目录和链接规则。
注意
正如在 第一步 中讨论的,构建目标所需属性应使用 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() 设置;用于描述 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() 操作的接口和非接口属性与其它目标命令相同。这意味着可以通过 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() 来传达语言标准和编译定义的需求。
有用资源¶
要编辑的文件¶
教程/CMakeLists.txtMathFunctions/CMakeLists.txtMathFunctions/MathFunctions.cxxCMakePresets.json
开始¶
目录 Help/guide/tutorial/Step4 包含 第三步 的完整推荐解决方案以及本步相关的 TODO。完成 TODO 1 到 TODO 8。
构建并运行¶
我们可以使用我们的 tutorial 预设运行 CMake,然后像平常一样构建。
cmake --preset tutorial
cmake --build build
验证 Tutorial 的输出是否符合我们对 std::sqrt 的预期。
解决方案¶
首先,我们在顶层 CML 中添加一个新的选项。
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 可执行文件添加适当的警告标志。
有用资源¶
要编辑的文件¶
教程/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 中生成的 Target 之间进行链接或在稍后将介绍的命令中导入的外部依赖项时,这些需求会自动继承。
如果我们碰巧有一些未由 CMake Target 描述的、我们需要引入构建的库或头文件,例如供应商提供的预编译二进制文件,我们可以使用 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() 接收到一个不映射到 Target 名称的参数时,它会将该字符串直接添加到链接行,作为要链接到构建中的库(在前面加上任何适当的标志,例如 -l)。
目标¶
使用 target_link_directories() 和 target_include_directories() 来描述项目内的预编译供应商静态库及其头文件。
有用资源¶
要编辑的文件¶
Vendor/CMakeLists.txt教程/CMakeLists.txt
入门¶
您需要将供应商库构建为静态归档文件才能完成此练习。导航到 Help/guide/tutorial/Step4/Vendor/lib 目录,并根据您的平台适当构建代码。在类 Unix 系统上,通常的命令是
g++ -c Vendors.cxx
ar rvs libVendor.a Vendor.o
然后完成 TODO 11 到 TODO 14。
注意
VendorLib 是一个 INTERFACE 库,这意味着它没有构建需求(因为它已经构建好了)。它的所有属性也应该是接口属性。
我们将在下一步中更详细地讨论 INTERFACE 库。
构建和运行¶
如果您已成功构建 libVendor,则可以使用常规命令重新构建 Tutorial。
cmake --build build
运行 Tutorial 现在应该会输出一条关于供应商可以接受结果的消息。
解决方案¶
我们需要使用 Target Link 和 Include 命令将归档文件及其头文件描述为 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
)