5、目标与属性:理解add_executable与add_library、目标属性(Target Properties)的查看与设置、set_target_properties实战
这一章,我们聊聊CMake里最核心的两个概念:目标和属性。
说白了,CMake的一切工作,最终都是为了生成一个“目标”——要么是一个可执行文件,要么是一个库。而属性,就是附着在目标上的各种配置信息。你想想看,一个目标需要知道它的源文件是谁、头文件路径在哪、链接什么库、编译选项是什么……这些信息,全都存在属性里。
我个人习惯把目标想象成一个“对象”,属性就是它的成员变量。嗯,这个比喻虽然不严谨,但很实用。
5.1 创建目标:add_executable 与 add_library
这是CMake里最基础的两个命令。你几乎每个项目都会用到它们。
5.1.1 add_executable
用来生成一个可执行文件。语法很简单:
add_executable(<目标名> [源文件列表...])
举个例子:
add_executable(my_app main.cpp utils.cpp)
这行代码会创建一个名为 my_app 的目标,它由 main.cpp 和 utils.cpp 编译链接而成。最终生成的可执行文件,在Linux下叫 my_app,在Windows下叫 my_app.exe。
5.1.2 add_library
用来生成一个库。语法:
add_library(<目标名> [STATIC | SHARED | MODULE] [源文件列表...])
三种库类型:
- STATIC:静态库。在Linux下是
.a,Windows下是.lib。链接时会把代码直接复制到可执行文件里。 - SHARED:动态库。Linux下是
.so,Windows下是.dll(同时会生成一个.lib导入库)。运行时加载。 - MODULE:插件库。跟SHARED类似,但不参与链接,通常用
dlopen或LoadLibrary手动加载。
举个例子:
add_library(my_lib STATIC math.cpp io.cpp)
这会生成一个静态库 libmy_lib.a(Linux)或 my_lib.lib(Windows)。
BUILD_SHARED_LIBS 变量来控制。我曾经在一个跨平台项目里忘了指定类型,结果Linux下编译正常,Windows下却生成了DLL,导致链接错误。嗯,从那以后我每次都写清楚类型。
5.2 目标属性:查看与设置
目标创建好了,但它只是一个空壳。真正让目标“活”起来的,是属性。
5.2.1 什么是目标属性?
每个目标都有一堆属性,控制着它的编译、链接、安装等行为。常见的属性有:
| 属性名 | 作用 |
|---|---|
INCLUDE_DIRECTORIES |
头文件搜索路径 |
COMPILE_DEFINITIONS |
编译宏定义 |
COMPILE_OPTIONS |
编译选项(如 -Wall) |
LINK_LIBRARIES |
链接的库 |
OUTPUT_NAME |
最终生成的文件名 |
VERSION / SOVERSION |
库的版本号(仅SHARED库有效) |
你可以用 get_target_property 来查看属性值:
get_target_property(my_include_dirs my_app INCLUDE_DIRECTORIES)
message("my_app 的头文件路径: ${my_include_dirs}")
如果属性没被设置过,会返回 NOTFOUND。嗯,这个细节容易踩坑——我建议你总是先检查一下。
5.2.2 设置属性:set_target_properties
这是最常用的设置属性的命令。语法:
set_target_properties(<目标1> [<目标2> ...]
PROPERTIES
<属性名1> <值1>
[<属性名2> <值2> ...])
可以同时设置多个目标、多个属性。看个实战例子:
add_library(my_math STATIC math.cpp)
set_target_properties(my_math
PROPERTIES
OUTPUT_NAME "math_core"
VERSION "1.2.0"
SOVERSION "1"
COMPILE_DEFINITIONS "USE_FAST_MATH"
)
这段代码做了几件事:
- 把库的输出文件名改为
math_core(而不是默认的my_math) - 设置版本号为1.2.0,SO版本为1(Linux下会生成
libmath_core.so.1.2.0和软链接) - 添加了一个编译宏
USE_FAST_MATH
set_target_properties 是“覆盖式”赋值。如果你多次设置同一个属性,后面的值会覆盖前面的。但有些属性是列表型的(比如 INCLUDE_DIRECTORIES),你需要用 target_include_directories 来追加,而不是直接覆盖。
5.3 实战:用属性控制一个库的方方面面
光说不练假把式。我们来看一个完整的例子,把今天讲的东西串起来。
假设我们要构建一个图像处理库 imgproc,它需要:
- 以动态库形式构建
- 输出文件名改为
imgproc_core - 版本号 2.0.1,SO版本 2
- 添加一个宏
IMGPROC_ENABLE_SSE - 链接一个第三方库
png
CMakeLists.txt 可以这样写:
cmake_minimum_required(VERSION 3.16)
project(ImageProcessor)
add_library(imgproc SHARED
filter.cpp
transform.cpp
io.cpp
)
# 设置各种属性
set_target_properties(imgproc
PROPERTIES
OUTPUT_NAME "imgproc_core"
VERSION "2.0.1"
SOVERSION "2"
COMPILE_DEFINITIONS "IMGPROC_ENABLE_SSE"
)
# 链接png库(假设已经用find_package找到了)
target_link_libraries(imgproc PRIVATE png)
# 查看一下最终输出文件名
get_target_property(out_name imgproc OUTPUT_NAME)
message("库的输出文件名: ${out_name}")
编译后,在Linux下你会得到 libimgproc_core.so.2.0.1,同时有 libimgproc_core.so.2 和 libimgproc_core.so 两个软链接。Windows下则是 imgproc_core.dll 和 imgproc_core.lib。
VERSION 和 SOVERSION 分开设置。很多新手只设 VERSION,结果发现动态库的SO版本没变,导致运行时加载了错误的库。记住:SOVERSION 控制的是二进制兼容性,VERSION 只是完整版本号。
5.4 知识体系:目标与属性核心逻辑
下面这张图,帮你理清本章的核心脉络:
这张图展示了:目标分为可执行文件和库两大类,库又有三种类型。每个目标都挂载着若干属性,而 set_target_properties 就是用来配置这些属性的核心命令。
5.5 避坑指南
最后,分享几个我踩过的坑:
- 属性名拼写错误: CMake不会报错,只会默默忽略。我曾经把
OUTPUT_NAME写成OUTPUT_NAME(少了个T),结果折腾了半小时才发现。建议用cmake --help-property-list查看所有合法属性名。 - 列表属性用等号覆盖: 比如
set_target_properties(... PROPERTIES INCLUDE_DIRECTORIES "/my/path")会清空之前所有的头文件路径。正确做法是用target_include_directories追加。 - 忘记设置
SOVERSION: 动态库的SO版本不设置,默认是0。如果你的库做了不兼容的修改,但忘了改SO版本,用户更新后程序会直接崩溃。嗯,这个坑我帮别人排查过很多次。
好了,这一章的内容就到这里。目标与属性是CMake的基石,理解了它们,后面的构建配置就会顺畅很多。