7、目标属性基础:目标的概念(可执行文件、库、自定义目标)、set_target_properties()与get_target_property()、常用目标属性一览
好,咱们今天聊聊 CMake 里最核心的概念——目标(Target)。
说实话,我刚开始用 CMake 的时候,觉得它就是个高级的 Makefile 生成器。写几个 add_executable、target_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 帮你搞定。
为了让你更直观地理解,我画了一张图:
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_properties 和 get_target_property 操作它们。你可以看到,每个目标都独立管理自己的属性,互不干扰。
好了,目标属性的基础就讲到这里。记住一句话:在 CMake 里,一切皆目标,一切属性皆可查可设。掌握了这个思维,你写出来的 CMakeLists.txt 就会清晰、可维护、可扩展。
我的建议:刚开始接触 CMake 时,不要急着学那些花哨的语法。先把目标、属性、依赖这三个概念吃透。我见过太多人把 CMakeLists.txt 写成「面条代码」,就是因为没理解目标这个抽象层。你想想看,如果每个库都把自己的头文件路径、编译选项、依赖关系封装好,整个项目是不是清爽多了?