12. 3.4 属性系统深度解析

导语

在前面的章节中,我们已经熟练掌握了接口库(Interface Library)的封装技巧,也见识了生成器表达式(Generator Expressions)在条件处理上的强大威力。如果你曾好奇:这些 target_include_directoriestarget_compile_options 等命令,底层究竟是如何把配置信息存储并传递的? 答案就是本节的主角——属性系统(Property System)

属性是 CMake 构建系统的”基因”。目标(Target)有属性,目录(Directory)有属性,源文件(Source)有属性,甚至整个构建过程的全局状态也由属性控制。理解属性系统,不仅能让你明白之前学过的 Modern CMake 命令在幕后做了什么,更能让你在面对复杂项目时,精准地操控构建行为的每一个细节。

本节将带你从目标、目录、全局、源文件四个层面逐层剖析属性系统,并掌握 set_propertyget_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.exemy_libd.dll,并且它们被整齐地归类到了 binlib 目录中。

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_BUILDZERO_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_BUILDINSTALLZERO_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_propertyget_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)

特别地,SETDEFINEDBRIEF_DOCSFULL_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 属性系统的四个层级。在实际项目中,这些层级之间存在明确的优先级和推荐用法:

  1. 全局属性(Global):用于影响整个构建系统的开关(如 USE_FOLDERS)。应尽量少用,避免隐式副作用。
  2. 目录属性(Directory):用于子项目兼容或设置目录级默认值。Modern CMake 中应优先使用目标属性替代。
  3. 目标属性(Target)核心战场。 这是 Modern CMake 推荐的配置层级,所有编译、链接、包含路径都应尽量通过目标属性传递。
  4. 源文件属性(Source):用于极少数需要单文件特殊处理的场景(如对某个 legacy 文件关闭优化)。
  5. 缓存变量属性(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 的最佳实践。你会发现,当项目规模扩大时,今天学到的属性层级知识将帮助你更好地隔离模块、控制依赖传播范围。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……