导语
在前面的章节中,我们已经熟练掌握了接口库(Interface Library)的封装技巧,也见识了生成器表达式(Generator Expressions)在条件处理上的强大威力。如果你曾好奇:这些 target_include_directories、target_compile_options 等命令,底层究竟是如何把配置信息存储并传递的? 答案就是本节的主角——属性系统(Property System)。
属性是 CMake 构建系统的”基因”。目标(Target)有属性,目录(Directory)有属性,源文件(Source)有属性,甚至整个构建过程的全局状态也由属性控制。理解属性系统,不仅能让你明白之前学过的 Modern CMake 命令在幕后做了什么,更能让你在面对复杂项目时,精准地操控构建行为的每一个细节。
本节将带你从目标、目录、全局、源文件四个层面逐层剖析属性系统,并掌握 set_property、get_property 等底层操作命令。准备好了吗?让我们打开 CMake 的”引擎盖”。
一、目标属性(Target Properties)
目标属性是我们最常用的一类属性。之前学过的几乎所有 target_* 命令,本质上都是在为目标设置特定的属性。除了用封装好的命令外,CMake 也允许我们直接通过属性系统对目标进行精细化配置。
1.1 查看所有可用属性
CMake 内置了数百个目标属性。你可以在命令行中运行以下指令查看完整列表:
cmake --help-property-list | grep "^TARGET_"
不过,你通常不需要记住所有属性。下面我们按场景分类,讲解最实用的那些。
1.2 输出控制类属性
这类属性决定了目标产物(可执行文件、库文件)的名称和存放路径。
OUTPUT_NAME:最终输出的文件名(不含前缀和扩展名)。RUNTIME_OUTPUT_DIRECTORY:可执行文件/DLL 的输出目录。LIBRARY_OUTPUT_DIRECTORY:共享库(.so/.dylib)的输出目录。ARCHIVE_OUTPUT_DIRECTORY:静态库(.a/.lib)的输出目录。DEBUG_POSTFIX:Debug 构建时附加到文件名尾的后缀。
来看一个典型的项目输出目录规范化示例:
add_executable(my_app main.cpp)
add_library(my_lib SHARED lib.cpp)
set_target_properties(my_app PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
DEBUG_POSTFIX "d"
)
set_target_properties(my_lib PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
DEBUG_POSTFIX "d"
)
编译后,Debug 模式下你会得到 my_appd.exe 和 my_libd.dll,并且它们被整齐地归类到了 bin 和 lib 目录中。
1.3 语言标准与编译特性
虽然我们在 2.3 节学过用 target_compile_features 来要求 C++ 标准,但直接设置目标属性也是一种常见做法:
set_property(TARGET my_app PROPERTY CXX_STANDARD 17)
set_property(TARGET my_app PROPERTY CXX_STANDARD_REQUIRED ON)
set_property(TARGET my_app PROPERTY CXX_EXTENSIONS OFF)
这三行分别表示:要求 C++17、必须满足该标准(不满足则报错)、关闭编译器特有扩展(如 GNU++17,严格使用标准 -std=c++17)。
1.4 链接与位置无关代码
POSITION_INDEPENDENT_CODE:是否生成位置无关代码(PIC/PIE),对于共享库通常需要设为ON。LINK_DEPENDS:指定链接阶段的额外文件依赖。INTERPROCEDURAL_OPTIMIZATION:是否启用链接时优化(LTO/IPO)。
set_property(TARGET my_lib PROPERTY POSITION_INDEPENDENT_CODE ON)
# 启用跨模块优化(需编译器支持)
set_property(TARGET my_app PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
1.5 IDE 组织与可见性
在使用 Visual Studio、Xcode 或 CLion 等多配置 IDE 时,以下属性能让你的工程结构更清晰:
FOLDER:在 IDE 中将目标归入指定文件夹(需配合全局属性USE_FOLDERS使用,见后文)。CXX_VISIBILITY_PRESET:控制符号可见性,如hidden。VISIBILITY_INLINES_HIDDEN:将内联函数的符号也设为隐藏。
set_target_properties(my_app my_lib PROPERTIES
FOLDER "CoreModules"
)
二、目录属性(Directory Properties)
目录属性的作用域是当前 CMakeLists.txt 及其子目录(直到被显式覆盖)。在早期 CMake 中,目录属性是配置的主要手段;在 Modern CMake 中,虽然更推荐目标级控制,但目录属性在管理全局默认值、子项目兼容等方面依然不可替代。
2.1 常用目录属性
INCLUDE_DIRECTORIES:当前目录下所有目标默认的头文件搜索路径。COMPILE_DEFINITIONS:当前目录下所有目标默认的编译宏定义。COMPILE_OPTIONS:当前目录下所有目标默认的编译选项。LINK_DIRECTORIES:当前目录下所有目标默认的库文件搜索路径。LINK_OPTIONS:当前目录下所有目标默认的链接选项。TEST_INCLUDE_FILE:CTest 测试时要包含的配置文件。
2.2 目录属性的设置与影响范围
我们使用 set_property(DIRECTORY ...) 或简化的目录级命令来设置。以下示例演示了如何为当前目录及其子目录设置默认包含路径:
# 当前 CMakeLists.txt 所在目录
set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/third_party/include")
# 等价于旧写法(不推荐在新项目中使用):
# include_directories("${CMAKE_SOURCE_DIR}/third_party/include")
add_executable(app1 main.cpp) # 会自动继承上述 include 路径
add_subdirectory(src) # src/CMakeLists.txt 中的目标也会继承,除非被覆盖
注意: 目录属性具有”传染性”,子目录会继承父目录的属性。这正是 Modern CMake 强调避免目录级 include_directories 的原因——它太容易在不经意间把路径传递给不该继承的目标了。
2.3 获取父目录属性
你可以显式指定目录路径来获取或设置特定目录的属性:
# 获取当前源码目录的属性
get_property(current_includes DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
message(STATUS "Current dir includes: ${current_includes}")
# 获取父目录的属性
get_property(parent_includes DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES PARENT_SCOPE)
三、全局属性(Global Properties)
全局属性影响整个 CMake 构建系统的行为。它们没有”目标”或”目录”的局限,一旦设置,对所有范围生效。
3.1 实用全局属性一览
USE_FOLDERS:启用 IDE 文件夹组织功能。配合目标的FOLDER属性使用。PREDEFINED_TARGETS_FOLDER:将 CMake 自动生成的目标(如ALL_BUILD、ZERO_CHECK)放入指定文件夹,避免污染你的工程视图。AUTOGEN_SOURCE_GROUP:将 Qt 的 MOC、UIC 生成的文件归入 IDE 中的特定组。TARGET_SUPPORTS_SHARED_LIBS:查询当前平台是否支持共享库(通常为只读属性)。RULE_MESSAGES:控制是否显示编译规则的输出信息。GLOBAL_DEPENDS_DEBUG_MODE:开启全局依赖调试模式,输出详细的依赖分析日志。
3.2 实战:整理 IDE 工程视图
对于大型项目,CMake 默认会在 IDE 中生成许多辅助目标(如 Visual Studio 中的 ALL_BUILD、INSTALL、ZERO_CHECK)。通过全局属性,我们可以将它们”收纳”起来:
# 必须在顶层 CMakeLists.txt 中设置
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMakePredefinedTargets")
set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "GeneratedFiles")
# 然后为你的实际目标分组
add_executable(my_app main.cpp)
add_library(core core.cpp)
add_library(util util.cpp)
set_target_properties(my_app PROPERTIES FOLDER "Applications")
set_target_properties(core util PROPERTIES FOLDER "Libraries/Core")
打开 Visual Studio 或 Xcode 后,你会发现工程视图变得层次分明,再也不用在一堆目标里翻找自己的模块了。
四、源文件属性(Source Properties)
有时,我们需要对单个源文件进行特殊处理,例如关闭某个文件的优化(便于调试第三方库)、给特定文件添加宏定义,或者标记文件是由代码生成器自动产生的。
4.1 常用源文件属性
COMPILE_FLAGS:给单个文件添加编译标志(注意:不支持生成器表达式,已逐渐被COMPILE_OPTIONS取代)。COMPILE_OPTIONS:更现代的单个文件编译选项设置。INCLUDE_DIRECTORIES:仅针对该源文件的额外头文件搜索路径。COMPILE_DEFINITIONS:仅针对该源文件的宏定义。GENERATED:标记该文件是在构建过程中生成的,避免在配置阶段报错找不到文件。OBJECT_DEPENDS:指定该文件编译时额外依赖的文件。HEADER_FILE_ONLY:告知 CMake 该文件只作为头文件展示,不参与编译(常用于 IDE 中显示 .h 文件)。SKIP_AUTOGEN/SKIP_AUTOMOC/SKIP_AUTOUIC/SKIP_AUTORCC:在 Qt 项目中跳过特定文件的自动处理。
4.2 实战:对单个文件禁用优化
假设你引入了一个第三方源文件 legacy.c,它在你项目的高优化级别下会出现异常。你可以单独为它关闭优化:
add_executable(my_app main.cpp legacy.c)
# GCC/Clang: -O0 关闭优化;MSVC: /Od 关闭优化
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
set_source_files_properties(legacy.c PROPERTIES COMPILE_OPTIONS "-O0")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set_source_files_properties(legacy.c PROPERTIES COMPILE_OPTIONS "/Od")
endif()
这样,即使整个项目使用 Release(-O3)构建,legacy.c 也会以无优化模式编译。
4.3 标记生成文件
当你使用代码生成工具(如 Protocol Buffers、Lex/Yacc)时,生成的源文件在首次配置时还不存在。必须标记为 GENERATED:
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
COMMAND python ${CMAKE_SOURCE_DIR}/tools/gen.py > ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
COMMENT "Generating source file..."
)
add_executable(my_app main.cpp ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp)
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
PROPERTIES GENERATED TRUE
)
五、缓存变量属性
缓存变量(Cache Variables)不仅存储在 CMakeCache.txt 中,它们自身也带有一系列元数据属性,控制着在 ccmake、CMake GUI 或 VS Code 中如何展示和编辑。
5.1 缓存变量的类型(TYPE)
当你使用 set(... CACHE ...) 时,指定的类型决定了配置界面的交互方式:
BOOL:布尔值,在 GUI 中显示为复选框。STRING:字符串,显示为文本框。FILEPATH:文件路径,显示为带文件选择按钮的文本框。PATH:目录路径,显示为带目录选择按钮的文本框。INTERNAL:内部变量,不对外展示,用于持久化存储 CMake 内部状态。STATIC:静态变量,极少使用。
5.2 缓存变量属性详解
FORCE:强制覆盖缓存中已有的值,即使 CMakeCache.txt 中已存在。DOCSTRING(帮助字符串):变量的说明文档,在 GUI 中悬停时显示。STRINGS:为 STRING 类型变量指定一组可选值,GUI 会渲染为下拉框。ADVANCED:标记为高级变量,在 GUI 中默认隐藏,需点击”Advanced”才显示。
5.3 实战:创建用户友好的构建选项
# 基础布尔选项
set(BUILD_TESTS ON CACHE BOOL "Build the test suite" FORCE)
# 带下拉框的选择项
set(BUILD_MODE "Release" CACHE STRING "Choose build mode")
set_property(CACHE BUILD_MODE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
# 路径选择
set(THIRD_PARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty" CACHE PATH "Path to third-party libraries")
# 文件选择
set(SIGNING_KEY "${CMAKE_SOURCE_DIR}/default.key" CACHE FILEPATH "Code signing key")
# 标记为高级(普通用户不需要看到)
mark_as_advanced(FORCE SIGNING_KEY)
在 ccmake 或 CMake GUI 中,BUILD_MODE 会呈现为一个下拉菜单,而 SIGNING_KEY 默认会被折叠到高级选项中,大大提升了项目的专业感。
六、属性操作命令:set_property / get_property / define_property
前面我们在各小节中已经零散使用了 set_property 和 get_property。现在让我们系统性地掌握这三个底层命令,它们是操作属性系统的”万能钥匙”。
6.1 set_property:通用属性设置
set_property 可以操作所有作用域的属性,其完整语法为:
set_property(
<GLOBAL
| DIRECTORY [dir]
| TARGET [target1 ...]
| SOURCE [src1 ...]
| INSTALL [file1 ...]
| TEST [test1 ...]
| CACHE [var1 ...]
| VARIABLE>
[APPEND] [APPEND_STRING]
PROPERTY <name> [value ...]
)
参数说明:
APPEND:将值追加到属性列表中(如追加编译选项)。APPEND_STRING:将值作为字符串追加(不加分号分隔符,常用于拼接链接标志)。
对比直接命令与底层属性命令:
# 这两行是等价的:
target_compile_options(my_app PRIVATE -Wall -Wextra)
set_property(TARGET my_app APPEND PROPERTY COMPILE_OPTIONS -Wall -Wextra)
# 这两行也是等价的:
target_include_directories(my_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
set_property(TARGET my_lib APPEND PROPERTY INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/include)
6.2 get_property:属性读取与状态查询
get_property 不仅能读取属性值,还能查询属性是否存在。
# 读取目标属性
get_property(std_val TARGET my_app PROPERTY CXX_STANDARD)
message(STATUS "my_app uses C++${std_val}")
# 查询属性是否被设置过
get_property(is_set TARGET my_app PROPERTY MY_CUSTOM_PROP SET)
if(is_set)
message(STATUS "MY_CUSTOM_PROP is set")
else()
message(STATUS "MY_CUSTOM_PROP is not set")
endif()
# 读取目录属性
get_property(dir_defs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY COMPILE_DEFINITIONS)
# 读取全局属性
get_property(use_folders GLOBAL PROPERTY USE_FOLDERS)
# 读取缓存变量属性(注意使用 CACHE 范围)
get_property(cache_help CACHE BUILD_TESTS PROPERTY HELPSTRING)
特别地,SET、DEFINED、BRIEF_DOCS、FULL_DOCS 这几个关键字用于查询元信息而非取值:
SET:该属性是否被赋过值(即使是空值)。DEFINED:该属性名是否存在于 CMake 的定义中(包括未赋值的)。BRIEF_DOCS/FULL_DOCS:获取属性的简短/完整文档(需通过define_property定义过)。
6.3 define_property:自定义属性
当内置属性无法满足需求时,你可以为特定范围定义全新的属性。这在编写大型 CMake 模块或框架时非常有用。
# 为目标定义一个新属性
define_property(
TARGET
PROPERTY MY_LIBRARY_CATEGORY
BRIEF_DOCS "Category of the library module"
FULL_DOCS
"This property is used by our build system to group libraries "
"into logical categories such as 'Networking', 'Storage', 'UI'. "
"It is consumed by the custom documentation generator."
)
# 使用自定义属性
set_property(TARGET my_lib PROPERTY MY_LIBRARY_CATEGORY "Networking")
# 读取并消费该属性
get_property(cat TARGET my_lib PROPERTY MY_LIBRARY_CATEGORY)
if(cat STREQUAL "Networking")
message(STATUS "my_lib belongs to Networking category")
endif()
注意: define_property 只是”注册”属性名并提供文档说明,它并不赋值。赋值仍需通过 set_property 完成。
6.4 各作用域操作速查表
为了便于查阅,下面给出各作用域的操作示例:
# GLOBAL
set_property(GLOBAL PROPERTY MY_GLOBAL_FLAG 1)
get_property(gflag GLOBAL PROPERTY MY_GLOBAL_FLAG)
# DIRECTORY(当前目录)
set_property(DIRECTORY PROPERTY ADDITIONAL_MAKE_CLEAN_FILES "temp.log")
get_property(clean_files DIRECTORY PROPERTY ADDITIONAL_MAKE_CLEAN_FILES)
# DIRECTORY(指定目录)
set_property(DIRECTORY "${CMAKE_SOURCE_DIR}/src" PROPERTY INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/include")
get_property(incs DIRECTORY "${CMAKE_SOURCE_DIR}/src" PROPERTY INCLUDE_DIRECTORIES)
# TARGET
set_property(TARGET my_app PROPERTY RUNTIME_OUTPUT_NAME "launcher")
get_property(out_name TARGET my_app PROPERTY RUNTIME_OUTPUT_NAME)
# SOURCE
set_property(SOURCE main.cpp PROPERTY COMPILE_DEFINITIONS "DEBUG_MAIN=1")
get_property(main_defs SOURCE main.cpp PROPERTY COMPILE_DEFINITIONS)
# INSTALL(较少直接使用)
set_property(INSTALL "my_app" PROPERTY CPACK_START_MENU_SHORTCUTS "MyApp")
# TEST(配合 CTest 使用)
add_test(NAME my_test COMMAND my_app --test)
set_property(TEST my_test PROPERTY TIMEOUT 60)
get_property(tout TEST my_test PROPERTY TIMEOUT)
# CACHE
set(MY_CACHE_VAR "default" CACHE STRING "My var")
set_property(CACHE MY_CACHE_VAR PROPERTY STRINGS "default" "custom" "none")
get_property(vtype CACHE MY_CACHE_VAR PROPERTY TYPE)
# VARIABLE(普通变量,非缓存)
set(my_local_var "hello")
set_property(VARIABLE my_local_var PROPERTY TYPE STRING)
get_property(lval VARIABLE my_local_var PROPERTY VALUE)
七、属性系统的层级与最佳实践
至此,我们已经领略了 CMake 属性系统的四个层级。在实际项目中,这些层级之间存在明确的优先级和推荐用法:
- 全局属性(Global):用于影响整个构建系统的开关(如
USE_FOLDERS)。应尽量少用,避免隐式副作用。 - 目录属性(Directory):用于子项目兼容或设置目录级默认值。Modern CMake 中应优先使用目标属性替代。
- 目标属性(Target):核心战场。 这是 Modern CMake 推荐的配置层级,所有编译、链接、包含路径都应尽量通过目标属性传递。
- 源文件属性(Source):用于极少数需要单文件特殊处理的场景(如对某个 legacy 文件关闭优化)。
- 缓存变量属性(Cache):用于暴露给最终用户的可配置项(如构建选项、路径选择)。
7.1 一个综合示例
让我们用一个贴近实战的片段来收尾,展示如何协同使用各层属性:
cmake_minimum_required(VERSION 3.20)
project(PropertyDemo CXX)
# ===== 全局属性:整理 IDE 视图 =====
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMake")
# ===== 缓存变量:暴露用户选项 =====
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries")
set(ENABLE_SANITIZER OFF CACHE BOOL "Enable AddressSanitizer")
set_property(CACHE ENABLE_SANITIZER PROPERTY HELPSTRING "Turn on ASan for debug builds")
# ===== 目录属性:子目录默认包含 =====
set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/common")
# ===== 目标属性:精细化控制 =====
add_executable(demo_app
main.cpp
core/engine.cpp
legacy/compat.cpp
)
set_target_properties(demo_app PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
FOLDER "Apps"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
# 接口库封装现代编译警告(见 3.2 节)
add_library(project_warnings INTERFACE)
target_compile_options(project_warnings INTERFACE
$<$:-Wall -Wextra -Wpedantic>
$<$:/W4>
)
target_link_libraries(demo_app PRIVATE project_warnings)
# 对 legacy 文件单独关闭优化
set_source_files_properties(legacy/compat.cpp
PROPERTIES
COMPILE_OPTIONS "$<$:-O0>"
)
# 读取并验证属性
get_property(out_dir TARGET demo_app PROPERTY RUNTIME_OUTPUT_DIRECTORY)
message(STATUS "App will be output to: ${out_dir}")
get_property(asan CACHE ENABLE_SANITIZER PROPERTY VALUE)
if(asan)
message(STATUS "AddressSanitizer enabled by user")
endif()
总结
属性系统是 CMake 的”幕后操盘手”。target_include_directories 背后修改的是 INCLUDE_DIRECTORIES 目标属性,set(... CACHE ...) 背后操作的是缓存属性,add_test 生成的测试对象也携带着 TIMEOUT 等测试属性。
掌握属性系统后,你不再只是”调用 CMake 命令”,而是真正理解了构建系统的数据模型。这为后续学习安装(Install)、打包(CPack)、导出(Export)以及 CTest 测试框架打下了坚实的底层基础。
在下一节中,我们将进入第四章,学习如何通过多目录结构组织大型项目,以及 add_subdirectory 的最佳实践。你会发现,当项目规模扩大时,今天学到的属性层级知识将帮助你更好地隔离模块、控制依赖传播范围。


没有回复内容