6、CMake目标与属性:目标概念、目标属性、set_target_properties、get_target_properties、目录属性

好,咱们今天聊点实在的。CMake 里有个核心概念,叫「目标」。说白了,目标就是你最终要生成的那个东西——可执行文件、静态库、动态库,甚至是一些自定义的构建动作。我刚开始用 CMake 时,总觉得这东西就是个脚本,写几个 add_executable 就完事了。后来项目一复杂,才发现不懂目标,根本玩不转。

什么是目标?

目标,英文叫 target。你可以把它理解成一个「构建单元」。每个目标都有自己的名字、类型,还有一堆属性。比如:

add_executable(my_app main.cpp utils.cpp)
add_library(my_lib STATIC math.cpp io.cpp)

这里 my_app 和 my_lib 就是两个目标。前者是可执行文件,后者是静态库。嗯,就这么简单。

但真正有意思的,是目标身上的那些属性。

目标属性:目标的「身份证」

每个目标都有一堆属性。这些属性控制着编译、链接、安装等各个环节的行为。比如:

  • INCLUDE_DIRECTORIES:头文件搜索路径
  • COMPILE_DEFINITIONS:编译宏定义
  • COMPILE_OPTIONS:编译选项
  • LINK_LIBRARIES:链接的库
  • OUTPUT_NAME:输出文件名
  • VERSIONSOVERSION:共享库版本号

我举个例子你就明白了。假设你有个库,想控制它的输出文件名:

add_library(my_math STATIC math.cpp)
set_target_properties(my_math PROPERTIES OUTPUT_NAME "math_core")

这样编译出来的库文件就叫 libmath_core.a,而不是 libmy_math.a。我在项目中遇到过类似需求——客户要求库名必须符合他们的命名规范,用 set_target_properties 一行就搞定了。

set_target_properties:批量设置属性

这个命令是咱们的常用工具。它的语法很直接:

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

你可以一次设置多个目标的多个属性。比如:

set_target_properties(my_app my_lib
    PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE ON)

这里我同时给两个目标设置了 C++17 标准,并要求位置无关代码。你想想看,如果不用 set_target_properties,你得在每个目标后面单独写 target_compile_features,多麻烦。

我的小技巧: 如果你只想设置一个目标的单个属性,用 set_property(TARGET target PROPERTY prop value) 也行。但 set_target_properties 更直观,我一般都用它。

get_target_property:把属性读出来

有设置就有读取。get_target_property 可以帮你拿到目标的某个属性值:

get_target_property(out_var target property)

举个例子:

add_executable(my_app main.cpp)
get_target_property(app_type my_app TYPE)
message("my_app 的类型是: ${app_type}")

输出会是:

my_app 的类型是: EXECUTABLE

这个命令在调试时特别有用。我记得有一次,一个库的链接总是失败,我怀疑是属性没传过去。用 get_target_property 一查,发现 LINK_LIBRARIES 是空的——原来我忘了 target_link_libraries。嗯,这种坑踩过一次就记住了。

注意: 如果目标没有设置某个属性,get_target_property 会返回一个空字符串,而不是报错。所以判断时要小心,别把空值和「没设置」搞混了。

目录属性:作用域的力量

除了目标属性,CMake 还有目录属性。目录属性作用于整个 CMakeLists.txt 所在的目录及其子目录。常用的有:

  • INCLUDE_DIRECTORIES:目录级别的头文件路径
  • COMPILE_DEFINITIONS:目录级别的编译宏
  • COMPILE_OPTIONS:目录级别的编译选项

设置方式类似:

set_directory_properties(PROPERTIES
    INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/include"
    COMPILE_DEFINITIONS "USE_OPENSSL")

但说实话,我个人不太推荐滥用目录属性。为什么呢?因为它的作用域太宽了。你在这个目录下定义了一个宏,子目录里所有目标都会受影响。万一子目录里有个目标不需要这个宏,你就得手动取消,很麻烦。

我更喜欢用 target_include_directories 和 target_compile_definitions 这种目标级别的命令。它们更精确,不会「误伤」其他目标。你想想看,一个大型项目里,不同模块的编译需求千差万别,用目录属性很容易造成「牵一发而动全身」的局面。

一张图看懂目标与属性

下面这张图总结了目标、属性、目录属性之间的关系:

CMake 目标与属性关系图 目标 (Target) 可执行文件 静态库 共享库 目标属性 INCLUDE_DIRECTORIES COMPILE_DEFINITIONS 目标属性 LINK_LIBRARIES OUTPUT_NAME 目录属性 (Directory Properties) 实线:直接关联 | 虚线:间接影响

实战:一个完整的例子

咱们来写一个完整的 CMakeLists.txt,把今天讲的东西串起来:

cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0)

# 添加一个库目标
add_library(math_ops STATIC
    src/add.cpp
    src/sub.cpp
    src/mul.cpp
)

# 设置库的属性
set_target_properties(math_ops PROPERTIES
    OUTPUT_NAME "math_core"
    VERSION 1.0.0
    SOVERSION 1
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
)

# 添加可执行文件目标
add_executable(my_app
    src/main.cpp
    src/calc.cpp
)

# 链接库
target_link_libraries(my_app PRIVATE math_ops)

# 设置可执行文件的属性
set_target_properties(my_app PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)

# 读取属性做检查
get_target_property(app_out_dir my_app RUNTIME_OUTPUT_DIRECTORY)
message(STATUS "可执行文件输出目录: ${app_out_dir}")

get_target_property(lib_name math_ops OUTPUT_NAME)
message(STATUS "库的输出名称: ${lib_name}")

这个例子涵盖了目标创建、属性设置、属性读取的全流程。我在实际项目中就是这么用的——先定义目标,再精细化控制每个目标的编译和链接行为。

核心要点:
  • 目标是 CMake 构建的基本单元,分为可执行文件、静态库、共享库等类型
  • 目标属性控制编译、链接、输出等行为,用 set_target_properties 设置
  • get_target_property 用于调试和检查属性值
  • 目录属性作用域更宽,适合全局设置,但容易造成意外影响
  • 优先使用目标级别的命令(如 target_include_directories),而不是目录属性

好了,关于目标和属性,今天就聊到这儿。这些东西看着简单,但用好了能让你的 CMake 脚本清晰又灵活。下次遇到复杂的构建需求,记得先想想:我能不能用目标属性来解决?


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