- 构建工具
- 开发工具
- CLI
【免费下载链接】CMake
Mirror of CMake upstream repository
本指南以 CMake 官方教程的收官章节 Help/guide/tutorial/Miscellaneous Features.rst(Step 11)为骨架,系统讲解两类在常规教程中不易展开、却在真实项目中举足轻重的特性:目标别名(Target Aliases)与生成器表达式(Generator Expressions)。读完本文,你将掌握如何用add_library(... ALIAS ...)让add_subdirectory与find_package两条消费路径共享同一套命名空间接口,以及如何借助$<CONFIG>这类延迟求值表达式,在编译定义、依赖注入等场景中让构建配置"注入"到目标内部,并配合仓库中 Step11 的完整示例代码完成动手练习。
一、为什么主教程之外还需要"杂项特性"
官方教程在 Miscellaneous Features.rst 开头就点明了这一章的定位:有些特性"不适合在主教程中占据篇幅,或重要程度不足以被单独讲解,但它们值得被提及",因此被归为"bonuses"(加分项)。文档同时强调了一个容易被忽略的事实:
CMake 有大量未在教程中覆盖的特性,其中一些被使用它们的项目视为必不可少(essential);另一些则被**打包者(packagers)**广泛使用,但在软件开发者日常讨论中鲜少出现。
也就是说,这一章收集的两类练习,恰恰是"本地构建者不关心、但面向包分发与依赖治理时绕不开"的能力:
- 目标别名(Target Aliases)——统一
add_subdirectory与find_package两种依赖消费方式的接口; - 生成器表达式(Generator Expressions)——将"配置阶段未知、生成阶段才可知"的信息注入构建系统。
文档也声明这份清单并非 CMake 全部能力的穷举,会随时代与相关性增长或收缩——因此这两讲本质上是"打开一扇窗",帮助读者意识到教程之外的广阔空间。
二、Step 11 工程全景:SimpleTest 与 TutorialProject 的分工
动手之前,先看清 Step 11 目录里两套工程的关系。仓库中该章节的完整布局位于 Help/guide/tutorial/Step11:
SimpleTest/:一个极简的测试框架工程(project(SimpleTest VERSION 0.0.1),见 SimpleTest/CMakeLists.txt)。它定义INTERFACE库SimpleTest,通过install(EXPORT ... NAMESPACE SimpleTest::)导出为SimpleTest::SimpleTest(见 SimpleTest/CMakeLists.txt),并连同SimpleTestConfig.cmake等文件一起安装到lib/cmake/SimpleTest目录。TutorialProject/:教程的主工程。其测试目标TestMathFunctions通过find_package(SimpleTest REQUIRED)找到已安装的 SimpleTest,并链接SimpleTest::SimpleTest(见 Tests/CMakeLists.txt),测试用例则直接使用TEST/REQUIRE宏断言数学函数(见 Tests/TestMathFunctions.cxx)。
两套工程都通过CMakePresets.json提供tutorial预设(如 TutorialProject/CMakePresets.json 所示,预设将CMAKE_PREFIX_PATH指向install目录并关闭了 IPO),因此后续所有构建命令都基于cmake --preset tutorial执行。
Step 11 的两个练习分别落在两个工程中:练习一改TutorialProject/MathFunctions/CMakeLists.txt,练习二改SimpleTest/CMakeLists.txt。
三、练习一:目标别名(Target Aliases)
3.1 动机:add_subdirectory与find_package的命名空间鸿沟
教程主体(Step 10 之前的章节)聚焦"安装依赖并在安装树中消费它",并推荐借助包管理器完成这一过程。但正如 Miscellaneous Features.rst 所指出的,由于历史与现实的多重原因,CMake 项目并非总是这样被消费的:
- 当依赖的源码被**整体嵌入(vendor)**父项目,通过
add_subdirectory引入时,暴露出来的目标名是依赖项目内部使用的名字; - 这些名字没有
install(EXPORT)会加上的命名空间前缀(如Tutorial::、SimpleTest::)。
于是同一个库,在"源码内嵌"与"包管理器安装"两种场景下出现了两套目标名。为了让两种工作流对使用者呈现一致的接口,CMake 提供了别名机制::command:add_library(ALIAS)与:command:add_executable(ALIAS)。
文档给出了别名的最小示例:
add_library(MyLib INTERFACE) add_library(MyProject::MyLib ALIAS MyLib)3.2add_library(... ALIAS ...)的语法与规则
在 Help/command/add_library.rst 的 "Alias Libraries" 一节,官方对别名目标的规则有精确定义:
add_library(<name> ALIAS <target>)- 别名
<name>可以在后续命令中代表<target>使用,但不会作为 make 目标出现在生成的构建系统中; - 被引用的
<target>本身不能是另一个 ALIAS 目标; - 别名目标可以被链接、可以读取其属性,也可以用常规的
if(TARGET)子命令检测其存在; - 版本 3.11 起,ALIAS 可以指向
GLOBAL导入目标(Imported Target);3.18 起可以指向非 GLOBAL 导入目标(此时别名仅在其创建目录及其以下作用域有效),并可通过目标属性ALIAS_GLOBAL判断别名是否为全局; - 版本 4.5 起,别名
<name>还可作为set_property、set_target_properties、target_link_libraries等命令的操作数来修改<target>的属性;若将 ALIAS 目标传给install或export命令,实际安装/导出的是其引用的目标。
3.3 实操:为 MathFunctions 添加别名(TODO 1)
练习目标一句话概括:为MathFunctions库添加一个与导出目标一致的库别名。需要编辑的文件是 Step11/TutorialProject/MathFunctions/CMakeLists.txt,其中第 2 行留有# TODO1:注释。
对照仓库中已完成全部练习的答案工程 Complete/TutorialProject/MathFunctions/CMakeLists.txt,TODO 1 的完整解只需一行:
add_library(MathFunctions) add_library(Tutorial::MathFunctions ALIAS MathFunctions)为什么别名要命名为Tutorial::MathFunctions?因为 TutorialProject 在 Step11/TutorialProject/CMakeLists.txt 中通过install(EXPORT TutorialTargets ... NAMESPACE Tutorial::)导出目标,find_package消费者看到的名字正是Tutorial::MathFunctions。别名让add_subdirectory的消费者也能以完全相同的方式引用它——这就是文档所说的"与 find_package 消费者所见接口保持一致"。
3.4 构建与运行
文档给出的构建流程分两步:先配置并安装SimpleTest,再构建TutorialProject。
# 在 Help/guide/Step11/SimpleTest 目录下 cmake --preset tutorial cmake --install build # 在 Help/guide/Step11/TutorialProject 目录下 cmake --preset tutorial cmake --build build文档特别提醒:添加别名后,行为不应有任何可观察的变化——这正是别名设计的精髓:它不改变构建产物与依赖图,只统一命名空间接口。
四、练习二:生成器表达式(Generator Expressions)
4.1 概念:延迟求值的条件表达式
cmake-generator-expressions(7) 是 CMake 支持的一种复杂领域特定语言(DSL),官方文档将其概括为:
Generator expressions are evaluated during build system generation to produce information for each specific build configuration.
即:生成器表达式在构建系统生成阶段才被求值,以产出"针对每个具体构建配置"的信息。原教程文档对它的通俗解释是——最容易将其理解为"延迟求值的条件表达式"(deferred-evaluation conditionals):它表达的需求,其输入在 CMake 配置(configure)阶段尚不可知。也正因如此,这类表达式被称为"生成器"表达式:它们是在底层构建系统被生成时才求值的。
历史上,生成器表达式常与target_include_directories配合,用于表达"构建树与安装树之间"的包含目录需求;文档指出,这一用途已被文件集(FILE SET)取代(Step 11 的 CMakeLists 中大量使用的FILE_SET HEADERS正是例证)。如今,生成器表达式最常见的应用场景是多配置生成器(multi-config generators)与复杂的依赖注入系统(dependency injection systems)。
文档给出的入门示例:
target_compile_definitions(MyApp PRIVATE "MYAPP_BUILD_CONFIG=$<CONFIG>")4.2 核心表达式$<CONFIG>深入解读
练习二使用的$<CONFIG>是生成器表达式中最常用也最直观的一个。在 cmake-generator-expressions.7.rst 中,其定义为:
$<CONFIG>:配置名(Configuration name),官方明确建议用它取代已弃用的CONFIGURATION表达式;$<CONFIG:cfgs>:若当前配置是逗号分隔列表cfgs中的任意一项,则求值为1,否则为0。比较不区分大小写;当该表达式作用于IMPORTED目标的属性时,还会考虑MAP_IMPORTED_CONFIG_<CONFIG>的映射。自 3.19 起cfgs支持同时指定多个配置。
理解$<CONFIG>的关键在于"两个配置阶段可能不同":在多配置生成器(如 Visual Studio、Xcode、Ninja Multi-Config)下,同一个构建目录可产出 Debug / Release / RelWithDebInfo 等多个配置的产物,而配置(configure)阶段根本不存在单一"当前配置",只有到生成(generate)阶段、为每个具体配置生成构建规则时,$<CONFIG>才被替换为对应的配置名。
4.3 实操:为 SimpleTest 添加$<CONFIG>编译定义(TODO 2)
练习二的目标是:在SimpleTest中添加一个生成器表达式,把构建配置写进一条编译定义里。需要编辑 Step11/SimpleTest/CMakeLists.txt,其中第 17-18 行留有# TODO2:注释。
答案工程的 Complete/SimpleTest/CMakeLists.txt 给出的完整解同样是一行,但与文档示例不同之处在于使用了INTERFACE关键字:
target_compile_definitions(SimpleTest INTERFACE "SIMPLETEST_CONFIG=$<CONFIG>")SimpleTest本身是INTERFACE库(只传递接口、不产生编译产物),因此编译定义必须通过INTERFACE传播给消费者;$<CONFIG>会按"构建该可执行文件所用的配置"求值——正如文档所强调的,这个配置未必等于配置 SimpleTest 时的配置。
该宏的消费端在 Complete/SimpleTest/SimpleTest.h 与 同文件 L132-L135:运行测试时,若定义了SIMPLETEST_CONFIG,会通过SIMPLETEST_XSTRINGIFY(SIMPLETEST_CONFIG)(字符串化宏)打印一行SimpleTest built with config: <配置名>。于是运行时即可直观验证"编译定义中的配置"究竟来自哪个构建配置。
4.4 构建、运行与CMAKE_BUILD_TYPE的影响
构建命令与练习一相同:
# 在 Help/guide/Step11/SimpleTest 目录下 cmake --preset tutorial cmake --install build # 在 Help/guide/Step11/TutorialProject 目录下 cmake --preset tutorial cmake --build build然后直接运行TestMathFunctions二进制,应当看到一条指名"构建该可执行文件所用的配置"的消息。文档特别指出验证技巧:
在单配置生成器(single-configuration generators)上,可以通过设置
CMAKE_BUILD_TYPE变量来改变构建配置。
对应到仓库中的教程说明(Before You Begin.rst):CMAKE_BUILD_TYPE可通过环境变量或cmake -DCMAKE_BUILD_TYPE=<config>直接指定。修改CMAKE_BUILD_TYPE后重新构建,TestMathFunctions打印的配置名应随之变化;而在多配置生成器下,同一构建目录内不同配置的构建会产生不同的$<CONFIG>求值结果——这正是生成器表达式在多配置场景中的价值所在。
五、组合视角:两个"杂项特性"如何协同支撑依赖治理
把两个练习放在一起看,能更完整地理解它们服务于同一个目标——让一个库在"源码内嵌"与"包分发"两条消费路径下呈现一致的接口与行为:
- 目标别名解决"接口命名"问题:
add_subdirectory消费者通过Tutorial::MathFunctions引用目标,与find_package消费者所见完全一致; - 生成器表达式解决"行为差异化"问题:无论库以何种方式被消费,
SIMPLETEST_CONFIG=$<CONFIG>都能在生成阶段把"真实构建配置"注入编译定义,让测试框架报告与最终二进制一致的配置信息,而不是配置阶段的静态值。
这两点恰是打包者(packager)日常最关心的两类细节,也呼应了 Miscellaneous Features.rst 开篇的论断:教程未覆盖的特性,往往是使用它们的项目眼中"essential"的能力。后续深入学习时,可继续查阅 Help/command/add_library.rst(目标命令全量文档)、Help/manual/cmake-generator-expressions.7.rst(生成器表达式手册,涵盖$<CONFIG>、$<LINK_ONLY>、$<TARGET_FILE>等数百个表达式),以及教程目录 Help/guide/tutorial 中的其他章节,从"杂项"走向系统化掌握。
- 构建工具
- 开发工具
- CLI
【免费下载链接】CMake
Mirror of CMake upstream repository
相关推荐
CMake高级特性:生成器表达式与策略系统
CMake高级特性:生成器表达式与策略系统 本文深入探讨CMake的两个核心高级特性:生成器表达式和策略系统。生成器表达式是CMake的元编程工具,允许在生成构
构建工具开发工具CLI社区贡献指南:如何参与Qwen3.5-9B-GLM5.1-Distill-v1的改进与优化
社区贡献指南:如何参与Qwen3.5 9B GLM5.1 Distill v1的改进与优化 Qwen3.5 9B GLM5.1 Distill v1是基于Qwe
MMTextFieldEffects之Nao特效终极解析:贝塞尔曲线路径变形动画的完整实现原理
MMTextFieldEffects之Nao特效终极解析:贝塞尔曲线路径变形动画的完整实现原理 如何让 iOS 的 UITextField 输入框拥有"波浪线"
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考