强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

CMake 从入门到精通:完整教程 / 第 5 章:目标与属性

第 5 章:目标与属性

5.1 目标(Target)概念

目标是现代 CMake 的核心概念。每个目标代表一个构建产物,并携带一组属性。

5.1.1 目标类型

类型命令产物示例
可执行文件add_executable二进制文件应用程序、测试程序
静态库add_library(... STATIC).a / .lib底层工具库
动态库add_library(... SHARED).so / .dll插件、共享库
对象库add_library(... OBJECT).o 文件集合避免重复编译
接口库add_library(... INTERFACE)无物理产物纯头文件库、配置聚合
模块库add_library(... MODULE).so / .dll运行时加载的插件
导入目标add_executable(... IMPORTED)已存在的二进制外部预编译工具
别名目标add_executable(... ALIAS)指向另一个目标命名空间化

5.1.2 目标的职责

一个目标携带以下信息:

Target "mylib"
├── 源文件        → SOURCES: src/a.cpp, src/b.cpp
├── 包含目录      → INCLUDE_DIRECTORIES: include/
├── 编译定义      → COMPILE_DEFINITIONS: MY_DEFINE=1
├── 编译选项      → COMPILE_OPTIONS: -Wall
├── 编译特性      → COMPILE_FEATURES: cxx_std_17
├── 链接库        → LINK_LIBRARIES: fmt::fmt, Threads::Threads
├── 链接选项      → LINK_OPTIONS: -fsanitize=address
├── 输出名称      → OUTPUT_NAME: mylib_v2
└── 位置属性      → BINARY_DIR, SOURCE_DIR

5.2 传播规则:PUBLIC / PRIVATE / INTERFACE

这是现代 CMake 中最关键的概念

5.2.1 三种关键字

关键字自己使用传递给依赖者
PRIVATE
PUBLIC
INTERFACE
app → mylib → zlib (PUBLIC)
            → fmt  (PRIVATE)
            → range-v3 (INTERFACE)
# mylib 的定义
add_library(mylib src/mylib.cpp)
target_link_libraries(mylib
    PUBLIC zlib::zlib         # mylib 和使用 mylib 的人都能用 zlib
    PRIVATE fmt::fmt          # 仅 mylib 内部使用 fmt
    INTERFACE range-v3::range-v3  # 仅使用 mylib 的人能用 range-v3
)

# app 链接 mylib
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
# app 自动获得:zlib(PUBLIC 传递)和 range-v3(INTERFACE 传递)
# app 不获得:fmt(PRIVATE,不传递)

5.2.3 target_include_directories 传播

add_library(mylib src/mylib.cpp)

# public 目录:mylib 和依赖者都需要
target_include_directories(mylib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# private 目录:仅 mylib 内部需要
target_include_directories(mylib PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src   # 内部头文件
)

5.2.4 target_compile_definitions 传播

add_library(mylib src/mylib.cpp)

# PUBLIC 定义:mylib 和依赖者都会定义
target_compile_definitions(mylib PUBLIC HAS_FEATURE_X)

# PRIVATE 定义:仅 mylib 内部定义
target_compile_definitions(mylib PRIVATE MYLIB_INTERNAL_DEBUG)

5.2.5 target_compile_options 传播

add_library(mylib src/mylib.cpp)

# PRIVATE 选项:仅 mylib 编译时使用
target_compile_options(mylib PRIVATE -Wall -Wextra)

# PUBLIC 选项:mylib 和依赖者都使用
target_compile_options(mylib PUBLIC -fPIC)

5.2.6 实际传播效果

# 项目结构
add_library(core src/core.cpp)
target_include_directories(core PUBLIC include/core)
target_compile_definitions(core PUBLIC ENABLE_CORE)
target_link_libraries(core PUBLIC base)
target_compile_options(core PRIVATE -Wall)

add_library(app_lib src/app.cpp)
target_link_libraries(app_lib PUBLIC core)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE app_lib)

传播效果表:

属性coreapp_libmyapp
include/core 目录✅ (PUBLIC)✅ (传递)
ENABLE_CORE 定义✅ (PUBLIC)✅ (传递)
base✅ (PUBLIC)✅ (传递)
-Wall 选项❌ (PRIVATE)

5.3 目标属性(Properties)

5.3.1 设置属性

add_library(mylib src/mylib.cpp)

# 使用 target_* 命令(推荐)
target_include_directories(mylib PUBLIC include/)
target_compile_definitions(mylib PUBLIC MY_DEFINE)
target_compile_options(mylib PRIVATE -Wall)
target_compile_features(mylib PUBLIC cxx_std_17)

# 使用通用 set_target_properties
set_target_properties(mylib PROPERTIES
    OUTPUT_NAME "mylib_v2"
    VERSION "2.0.0"
    SOVERSION "2"
    POSITION_INDEPENDENT_CODE ON
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    CXX_EXTENSIONS OFF
)

# 读取属性
get_target_property(lib_name mylib OUTPUT_NAME)
message("库输出名称: ${lib_name}")  # mylib_v2

5.3.2 常用目标属性

属性说明示例
OUTPUT_NAME输出文件名mylib_v2
VERSION版本号2.0.0
SOVERSIONSO 版本号2
POSITION_INDEPENDENT_CODE生成 PIC 代码ON
CXX_STANDARDC++ 标准17
CXX_STANDARD_REQUIRED是否强制要求标准ON
CXX_EXTENSIONS是否使用编译器扩展OFF
PREFIX输出文件前缀lib
SUFFIX输出文件后缀.so
DEBUG_POSTFIXDebug 版后缀_d
RUNTIME_OUTPUT_DIRECTORY可执行文件输出目录${CMAKE_BINARY_DIR}/bin
LIBRARY_OUTPUT_DIRECTORY库文件输出目录${CMAKE_BINARY_DIR}/lib
ARCHIVE_OUTPUT_DIRECTORY静态库输出目录${CMAKE_BINARY_DIR}/lib

5.3.3 目标属性分类

属性
├── 编译属性
│   ├── INCLUDE_DIRECTORIES
│   ├── COMPILE_DEFINITIONS
│   ├── COMPILE_OPTIONS
│   ├── COMPILE_FEATURES
│   └── COMPILE_FLAGS (已弃用)
├── 链接属性
│   ├── LINK_LIBRARIES
│   ├── LINK_DIRECTORIES
│   ├── LINK_OPTIONS
│   └── LINK_FLAGS (已弃用)
├── 输出属性
│   ├── OUTPUT_NAME
│   ├── PREFIX / SUFFIX
│   ├── VERSION / SOVERSION
│   └── *_OUTPUT_DIRECTORY
├── 语言属性
│   ├── CXX_STANDARD
│   ├── CXX_STANDARD_REQUIRED
│   └── CXX_EXTENSIONS
└── 位置属性
    ├── SOURCE_DIR
    ├── BINARY_DIR
    └── SOURCES

5.4 别名目标(ALIAS)

5.4.1 基本用法

# 定义库
add_library(mylib src/mylib.cpp)

# 创建别名(注意命名空间风格)
add_library(MyLib::mylib ALIAS mylib)

# 使用别名
add_executable(app main.cpp)
target_link_libraries(app PRIVATE MyLib::mylib)

5.4.2 命名空间的好处

# 项目内部使用命名空间前缀
add_library(core src/core.cpp)
add_library(MyProject::core ALIAS core)
# 后续都用 MyProject::core 引用

# 下游项目使用时
target_link_libraries(app PRIVATE MyProject::core)

# 如果 MyProject::core 不存在,CMake 会尝试 find_package(MyProject)
# 这实现了"内部构建"和"系统安装"的无缝切换

5.4.3 别名限制

  • 不能对 IMPORTED 目标创建别名(CMake 3.18 之前)
  • 别名目标和原目标共享所有属性
  • 不能对别名目标设置额外属性
  • 可执行文件也可以创建别名
# 可执行文件别名
add_executable(myapp main.cpp)
add_executable(MyApp::myapp ALIAS myapp)

5.5 导入目标(IMPORTED)

5.5.1 概念

导入目标代表一个已经存在的外部文件,不是由当前项目构建的:

# 导入一个已存在的可执行文件
add_executable(MyTool::mytool IMPORTED)
set_target_properties(MyTool::mytool PROPERTIES
    IMPORTED_LOCATION "/usr/local/bin/mytool"
)

# 导入一个已存在的库
add_library(ExternalLib::external SHARED IMPORTED)
set_target_properties(ExternalLib::external PROPERTIES
    IMPORTED_LOCATION "/usr/lib/libexternal.so"
    IMPORTED_SONAME "libexternal.so.2"
    INTERFACE_INCLUDE_DIRECTORIES "/usr/include/external"
)

5.5.2 IMPORTED 与 GLOBAL

# 默认 IMPORTED 目标仅在当前目录可见
add_library(Ext::lib SHARED IMPORTED)

# 设置为 GLOBAL 使其在所有目录可见
add_library(Ext::lib SHARED IMPORTED GLOBAL)

5.5.3 find_package 创建导入目标

# find_package 通常会创建导入目标
find_package(OpenSSL REQUIRED)

# 检查目标是否存在
if(TARGET OpenSSL::SSL)
    target_link_libraries(myapp PRIVATE OpenSSL::SSL)
endif()

5.6 接口库详解

5.6.1 纯头文件库

# 创建一个仅包含头文件的库
add_library(header_only_lib INTERFACE)

# 接口库只能使用 INTERFACE 关键字
target_include_directories(header_only_lib INTERFACE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(header_only_lib INTERFACE cxx_std_17)

# 使用
add_executable(app main.cpp)
target_link_libraries(app PRIVATE header_only_lib)

5.6.2 配置聚合

# 创建一个接口库来聚合编译选项
add_library(project_warnings INTERFACE)
target_compile_options(project_warnings INTERFACE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

# 创建一个接口库来聚合特性
add_library(project_features INTERFACE)
target_compile_features(project_features INTERFACE cxx_std_17)
target_compile_definitions(project_features INTERFACE
    $<$<CONFIG:Debug>:DEBUG_MODE>
)

# 所有目标都可以方便地使用
add_executable(app main.cpp)
target_link_libraries(app PRIVATE
    project_warnings
    project_features
)

5.6.3 头文件集合(FILE_SET,CMake 3.23+)

add_library(mylib src/mylib.cpp)

# 使用 FILE_SET 管理头文件
target_sources(mylib PUBLIC
    FILE_SET HEADERS
    BASE_DIRS include
    FILES
        include/mylib/core.h
        include/mylib/utils.h
        include/mylib/types.h
)

# 安装时自动处理头文件
install(TARGETS mylib EXPORT MyTargets
    FILE_SET HEADERS DESTINATION include
)

5.7 对象库详解

5.7.1 基本用法

# 对象库编译源文件为目标文件(.o/.obj)
add_library(mylib_objects OBJECT
    src/a.cpp
    src/b.cpp
    src/c.cpp
)

# 设置公共属性
target_include_directories(mylib_objects PUBLIC include/)
target_compile_features(mylib_objects PUBLIC cxx_std_17)

# 使用对象文件创建静态库
add_library(mylib STATIC $<TARGET_OBJECTS:mylib_objects>)
target_link_libraries(mylib PUBLIC mylib_objects)

# 使用对象文件创建动态库
add_library(mylib_shared SHARED $<TARGET_OBJECTS:mylib_objects>)

5.7.2 对象库的优势

传统方式:
src/a.cpp ──编译──> a.o ──┐
src/b.cpp ──编译──> b.o ──┼──> libstatic.a  (链接)
src/c.cpp ──编译──> c.o ──┘
src/a.cpp ──编译──> a.o ──┐
src/b.cpp ──编译──> b.o ──┼──> libshared.so (再编译一次!)
src/c.cpp ──编译──> c.o ──┘

使用对象库:
src/a.cpp ──编译──> a.o ──┐
src/b.cpp ──编译──> b.o ──┼──> OBJECT ──┬──> libstatic.a
src/c.cpp ──编译──> c.o ──┘            └──> libshared.so
(每个文件只编译一次!)

5.7.3 CMake 3.12+ 对象库直接链接

add_library(mylib_objects OBJECT src/a.cpp src/b.cpp)
target_include_directories(mylib_objects PUBLIC include)

# 3.12+ 可以直接链接对象库
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib_objects)

5.8 业务场景

场景一:大型项目的模块化

# 核心库
add_library(core OBJECT src/core/*.cpp)
target_include_directories(core PUBLIC include/core)
target_compile_features(core PUBLIC cxx_std_17)

# 网络模块
add_library(network OBJECT src/network/*.cpp)
target_include_directories(network PUBLIC include/network)
target_link_libraries(network PUBLIC core OpenSSL::SSL)

# 数据库模块
add_library(database OBJECT src/database/*.cpp)
target_include_directories(database PUBLIC include/database)
target_link_libraries(database PUBLIC core PostgreSQL::PostgreSQL)

# 聚合为单一共享库
add_library(myapp SHARED
    $<TARGET_OBJECTS:core>
    $<TARGET_OBJECTS:network>
    $<TARGET_OBJECTS:database>
)

# 应用程序
add_executable(app main.cpp)
target_link_libraries(app PRIVATE myapp)

场景二:编译选项管理

# 创建项目级别的警告配置
add_library(project_warnings INTERFACE)
target_compile_options(project_warnings INTERFACE
    $<$<CXX_COMPILER_ID:GNU>:
        -Wall -Wextra -Wpedantic -Wshadow -Wnon-virtual-dtor
        -Wold-style-cast -Wcast-align -Woverloaded-virtual
    >
    $<$<CXX_COMPILER_ID:Clang>:
        -Wall -Wextra -Wpedantic -Wshadow -Wnon-virtual-dtor
    >
    $<$<CXX_COMPILER_ID:MSVC>:
        /W4 /WX /wd4251 /wd4275
    >
)

# 每个目标都可以使用
add_library(mylib src/mylib.cpp)
target_link_libraries(mylib PRIVATE project_warnings)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE project_warnings mylib)

5.9 注意事项

问题说明
混用新旧风格避免使用 include_directories() 等全局命令
过度使用 PUBLIC只有真正需要传递的属性才用 PUBLIC
忘记 PRIVATE大多数情况下,可执行文件应使用 PRIVATE 链接
对象库的 INCLUDE 目录CMake 3.12 前对象库不传递属性
接口库不能设 PRIVATE/PUBLIC接口库只能用 INTERFACE

5.10 扩展阅读


上一章:第 4 章 — 变量系统 | 下一章:第 6 章 — 库的构建与使用 →