7、目标属性基础:目标的概念(可执行文件、库、自定义目标)、set_target_properties()与get_target_property()、常用目标属性一览

好,咱们今天聊聊 CMake 里最核心的概念——目标(Target)

说实话,我刚开始用 CMake 的时候,觉得它就是个高级的 Makefile 生成器。写几个 add_executabletarget_link_libraries 就完事了。但后来项目越来越大,我才发现,如果不理解「目标」这个抽象层,你写出来的 CMakeLists.txt 迟早会变成一团乱麻。

目标是什么?说白了,就是 CMake 世界里一切构建产物的抽象。可执行文件是目标,静态库是目标,动态库也是目标。甚至你还可以创建一些「不产生任何文件」的目标——比如跑测试、生成文档、打包发布。这些叫自定义目标。

嗯,这里要注意:CMake 里的目标,不是 Makefile 里的 .o 文件,也不是 Visual Studio 里的项目。它是一个更高层的概念,携带了所有构建该产物所需的属性。

7.1 三种基本目标类型

咱们先看最常用的三种:

  • 可执行文件目标:用 add_executable() 创建。比如 add_executable(myapp main.cpp utils.cpp)。这会在构建时生成一个可执行文件(Linux 下是 myapp,Windows 下是 myapp.exe)。
  • 库目标:用 add_library() 创建。可以指定 STATIC、SHARED、MODULE 或者不指定(默认根据 BUILD_SHARED_LIBS 决定)。比如 add_library(mylib STATIC src/lib.cpp)
  • 自定义目标:用 add_custom_target() 创建。它不产生任何文件,只是执行一些命令。比如 add_custom_target(run_tests COMMAND ${CMAKE_CTEST_COMMAND})

我个人习惯是:把项目里所有「需要被构建的东西」都定义成目标。哪怕只是一个简单的脚本,我也倾向于用 add_custom_target 包装一下。这样整个构建系统就统一了——所有操作都围绕目标展开。

核心思想:CMake 的构建系统,本质上是一个「目标依赖图」。你定义目标,然后告诉 CMake 它们之间的依赖关系。剩下的,CMake 帮你搞定。

为了让你更直观地理解,我画了一张图:

CMake 目标体系 可执行文件目标 add_executable() • 生成可执行二进制 • 可设置入口点 • 可链接库目标 • 支持 WIN32/MACOSX_BUNDLE • 示例:myapp, server, tool 库目标 add_library() • STATIC / SHARED / MODULE • 可被其他目标链接 • 可导出供外部使用 • 支持 INTERFACE 库 • 示例:core, math, network 自定义目标 add_custom_target() • 不产生文件 • 执行自定义命令 • 可设置依赖 • 常用于:测试、打包、文档 • 示例:run_tests, docs, clean 三者均可设置属性,通过 set_target_properties() 统一管理

7.2 操作目标属性的两把钥匙

目标创建好了,怎么给它「加料」?CMake 提供了两个核心函数:set_target_properties()get_target_property()

set_target_properties()

这个函数用来设置一个或多个目标的属性。语法很简单:

set_target_properties(target1 target2 ...
    PROPERTIES
    prop1 value1
    prop2 value2
)

举个例子:

add_executable(myapp main.cpp)
set_target_properties(myapp PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    WIN32_EXECUTABLE TRUE
)

这段代码告诉 CMake:编译 myapp 时用 C++17 标准,并且如果是在 Windows 上,把它标记为 GUI 应用程序(不弹出控制台窗口)。

小技巧:如果你只想设置单个目标的单个属性,也可以用 set_property(TARGET target PROPERTY name value)。但我个人更习惯用 set_target_properties,因为它可以一次设置多个属性,代码更紧凑。

get_target_property()

有设置就有读取。这个函数用来获取目标的某个属性值:

get_target_property(var target property)

# 示例
get_target_property(my_std myapp CXX_STANDARD)
message("myapp 的 C++ 标准是:${my_std}")

如果属性没有被设置过,var 会被赋值为 NOTFOUND。嗯,这里有个坑——我曾经在调试一个大型项目时,怎么查都查不到某个属性为什么没生效。后来才发现,是因为我在 add_executable 之前就调用了 set_target_properties,目标还没创建,属性自然没设上。所以记住:先创建目标,再设置属性

7.3 常用目标属性一览

CMake 支持的属性非常多,官方文档列了上百个。但实际项目中,常用的也就二三十个。我把最核心的整理成了一张表:

属性名 适用目标 说明
CXX_STANDARD 可执行文件、库 指定 C++ 标准版本(如 11、14、17、20)
CXX_STANDARD_REQUIRED 可执行文件、库 如果设为 ON,编译器必须支持指定的标准,否则报错
OUTPUT_NAME 可执行文件、库 指定生成的产物文件名(不含扩展名)
SUFFIX 可执行文件、库 给文件名加后缀,比如 _d 表示 debug 版本
FOLDER 所有目标 在 IDE(如 Visual Studio)中分组显示
WIN32_EXECUTABLE 可执行文件 Windows 下创建 GUI 应用,不弹出控制台
MACOSX_BUNDLE 可执行文件 macOS 下创建 .app bundle
POSITION_INDEPENDENT_CODE 生成位置无关代码(PIC),共享库必须
INTERFACE_INCLUDE_DIRECTORIES 链接该库时自动添加的包含目录
INTERFACE_LINK_LIBRARIES 链接该库时自动链接的其他库
COMPILE_DEFINITIONS 所有目标 编译时定义的宏(-D 选项)
COMPILE_OPTIONS 所有目标 编译时附加的选项(如 -Wall -O2)
LINK_FLAGS 可执行文件、库 链接时附加的选项

避坑指南:我曾经在一个跨平台项目里,用 set_target_properties 设置了 COMPILE_DEFINITIONS,结果在 Linux 上好好的,到了 Windows 上死活编译不过。查了半天才发现,COMPILE_DEFINITIONS 的值在 Windows 上会被自动加上 /D 前缀,而 Linux 是 -D。如果你需要跨平台,建议用 target_compile_definitions() 这个更高级的命令,它会自动处理平台差异。

7.4 属性的继承与作用域

这里有个重要的概念:属性不是全局的。每个目标都有自己的属性副本。你给 A 目标设置了 CXX_STANDARD 17,B 目标完全不受影响。

但是,库目标有一个特殊的机制——INTERFACE 属性。当你用 target_link_libraries() 把库链接到可执行文件时,库的 INTERFACE 属性会自动传播给可执行文件。比如:

add_library(mylib STATIC lib.cpp)
set_target_properties(mylib PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/include
)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# 现在 myapp 自动获得了 include 目录的包含路径

这个机制非常强大。你想想看,每个库只需要管好自己的头文件路径和依赖库,使用者只需要 target_link_libraries 一下,一切自动就绪。这就是现代 CMake 的「封装」思想。

核心原则:能用目标属性解决的问题,就不要用全局变量。全局变量(比如 include_directories())会让构建系统变得难以维护,而目标属性是自包含的、可组合的。

7.5 实战:一个完整示例

最后,咱们看一个完整的例子,把今天讲的东西串起来:

cmake_minimum_required(VERSION 3.16)
project(TargetDemo LANGUAGES CXX)

# 创建一个库
add_library(math STATIC math.cpp math.h)
set_target_properties(math PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE ON
    INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}
)

# 创建一个可执行文件
add_executable(calculator main.cpp)
set_target_properties(calculator PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    OUTPUT_NAME "calc"
    SUFFIX "_v1"
)

# 链接库
target_link_libraries(calculator PRIVATE math)

# 创建一个自定义目标,用于运行程序
add_custom_target(run_calculator
    COMMAND calculator
    DEPENDS calculator
)

# 验证属性
get_target_property(calc_output calculator OUTPUT_NAME)
message("计算器的输出名称是:${calc_output}")

这个例子展示了三种目标类型,以及如何用 set_target_propertiesget_target_property 操作它们。你可以看到,每个目标都独立管理自己的属性,互不干扰。

好了,目标属性的基础就讲到这里。记住一句话:在 CMake 里,一切皆目标,一切属性皆可查可设。掌握了这个思维,你写出来的 CMakeLists.txt 就会清晰、可维护、可扩展。

我的建议:刚开始接触 CMake 时,不要急着学那些花哨的语法。先把目标、属性、依赖这三个概念吃透。我见过太多人把 CMakeLists.txt 写成「面条代码」,就是因为没理解目标这个抽象层。你想想看,如果每个库都把自己的头文件路径、编译选项、依赖关系封装好,整个项目是不是清爽多了?


公众号:蓝海资料掘金营,微信deep3321