4、项目配置与目标管理:project()命令、cmake_minimum_required、add_executable与add_library、目标属性(target properties)、静态库与动态库的创建
CMake 的核心价值在于将“构建配置”从“构建实现”中剥离出来,让开发者用声明式语法描述项目结构、编译选项和依赖关系。本章聚焦于 CMakeLists.txt 中最基础也最关键的部分:项目声明、最低版本约束、目标(Target)的创建与属性管理,以及静态库与动态库的构建差异。理解这些内容,是后续掌握 install、export、find_package 等高级特性的前提。我们将从命令语义出发,结合目标属性的作用域规则,通过一个多目标项目的实战案例,帮助你建立“目标为中心”的 CMake 心智模型。
4.1 项目声明:cmake_minimum_required 与 project()
核心原理:cmake_minimum_required 定义了构建此项目所需的最低 CMake 版本,它不仅是版本检查,更会激活对应版本的策略(Policies)兼容模式。project() 则设置项目名称、版本号、支持的语言,并隐式定义多个变量(如 PROJECT_NAME、PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR)。这两个命令必须在 CMakeLists.txt 的最顶部、任何 add_executable 或 add_library 之前调用。
判断方法:选择最低版本时,应参考你实际使用的 CMake 特性。例如,若使用 target_link_libraries 的带关键字语法(PRIVATE/PUBLIC),建议最低版本设为 3.0+;若使用 target_sources 或 C++20 模块,则需要 3.13+。project() 的 LANGUAGES 参数应明确声明,避免 CMake 自动检测 C 和 CXX 带来的额外开销。
cmake_minimum_required(VERSION 3.16)
project(GeometryEngine VERSION 2.1.0 LANGUAGES CXX)
# 此时 CMake 已定义:
# PROJECT_NAME = GeometryEngine
# PROJECT_VERSION = 2.1.0
# PROJECT_SOURCE_DIR = /path/to/source
# PROJECT_BINARY_DIR = /path/to/build
PROJECT_VERSION_MAJOR、PROJECT_VERSION_MINOR 等变量,可用于后续生成版本头文件或打包配置,无需手动解析字符串。
4.2 目标创建:add_executable 与 add_library
核心原理:CMake 的构建单元是“目标”(Target)。add_executable 创建一个可执行文件目标,add_library 创建一个库目标。库类型由第二个参数决定:STATIC(静态库)、SHARED(动态库)、MODULE(插件模块,不参与链接,仅运行时加载)、OBJECT(仅编译不打包)、INTERFACE(纯头文件库)。目标名在目录及其子目录中必须唯一,但不同目录可以重名。
操作流程:创建目标时,应遵循“先声明,后配置”的顺序。先列出源文件,再通过后续命令(如 target_include_directories)补充属性。对于源文件较多的情况,推荐使用 target_sources() 分批次添加,而不是在 add_library 中一次性列出所有文件,这样便于条件编译和文件分组。
# 可执行文件
add_executable(app
src/main.cpp
src/application.cpp
)
# 静态库
add_library(core STATIC
src/core/algorithm.cpp
src/core/geometry.cpp
)
# 动态库
add_library(render SHARED
src/render/opengl_renderer.cpp
)
# 纯头文件库(无需源文件)
add_library(interfaces INTERFACE)
target_include_directories(interfaces INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
案例拆解:假设一个图像处理项目,需要同时提供静态库和动态库版本供不同客户使用。直接创建两个同名目标会冲突,此时可采用“对象库 + 最终打包”策略:
# 先编译为对象库,避免重复编译
add_library(imgproc_objects OBJECT
src/filter.cpp
src/transform.cpp
)
set_target_properties(imgproc_objects PROPERTIES POSITION_INDEPENDENT_CODE ON)
# 分别打包为静态库和动态库
add_library(imgproc_static STATIC $<TARGET_OBJECTS:imgproc_objects>)
add_library(imgproc_shared SHARED $<TARGET_OBJECTS:imgproc_objects>)
set_target_properties(imgproc_shared PROPERTIES OUTPUT_NAME "imgproc")
set_target_properties(imgproc_static PROPERTIES OUTPUT_NAME "imgproc")
这样既避免了源码编译两次,又保证了静态库和动态库输出文件名一致(通过 OUTPUT_NAME 属性),便于客户链接时使用 -limgproc。
4.3 目标属性:精细控制编译与链接
核心原理:目标属性(Target Properties)是附着在目标上的键值对,控制从编译选项到链接库的方方面面。常用属性包括:INCLUDE_DIRECTORIES(头文件搜索路径)、COMPILE_DEFINITIONS(预处理器宏)、COMPILE_OPTIONS(编译器标志)、LINK_LIBRARIES(链接的库)、POSITION_INDEPENDENT_CODE(位置无关代码,动态库必需)。属性可通过 set_target_properties 直接设置,但更推荐使用 target_include_directories、target_compile_definitions 等专用命令,它们会自动处理属性传递。
判断方法:属性传递的关键字 PUBLIC、PRIVATE、INTERFACE 决定了依赖关系如何传播。PUBLIC 表示该属性既用于当前目标编译,也传递给链接此目标的其他目标;PRIVATE 仅用于当前目标;INTERFACE 仅传递给依赖者,自身编译时不使用。选择错误会导致头文件找不到或宏定义泄漏。
| 关键字 | 自身编译 | 依赖者编译 | 典型场景 |
|---|---|---|---|
| PRIVATE | ✔ 使用 | ✘ 不使用 | 内部实现细节,如私有头文件路径 |
| PUBLIC | ✔ 使用 | ✔ 使用 | 公共 API 头文件路径、导出宏 |
| INTERFACE | ✘ 不使用 | ✔ 使用 | 纯头文件库、接口定义 |
add_library(geometry STATIC src/geometry.cpp)
target_include_directories(geometry
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include # 外部需要引用
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src # 仅内部使用
)
target_compile_definitions(geometry PRIVATE GEOMETRY_BUILD_LIB)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE geometry)
# app 会自动获得 geometry 的 PUBLIC 头文件路径,但不会获得 PRIVATE 路径
include_directories() 和 add_definitions() 这类目录级命令,它们会污染整个目录及其子目录的所有目标,导致难以排查的编译冲突。始终使用目标级命令(target_*)来保持依赖关系清晰。
4.4 静态库与动态库的创建与选择
核心原理:静态库(.a/.lib)在链接时被完整复制到可执行文件中,运行时无需额外文件;动态库(.so/.dll/.dylib)在运行时由系统加载,可被多个进程共享。创建动态库时,必须确保编译目标文件时使用 -fPIC(位置无关代码),CMake 中可通过设置 POSITION_INDEPENDENT_CODE 属性实现。此外,Windows 上动态库需要处理 __declspec(dllexport) 导出符号,Linux 则需关注符号可见性。
操作流程:创建动态库的推荐流程:
- 使用
add_library(mylib SHARED ...)声明目标。 - 设置
POSITION_INDEPENDENT_CODE ON(CMake 3.0+ 对 SHARED 默认开启,但显式声明更安全)。 - 定义导出宏,在头文件中使用
#ifdef _WIN32区分__declspec(dllexport/import)。 - 设置
VERSION和SOVERSION属性(Linux/macOS)以管理 ABI 兼容性。
add_library(render SHARED src/render.cpp)
set_target_properties(render PROPERTIES
VERSION 1.2.0
SOVERSION 1
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)
# 头文件中的导出宏示例
# #ifdef _WIN32
# #ifdef render_EXPORTS
# #define RENDER_API __declspec(dllexport)
# #else
# #define RENDER_API __declspec(dllimport)
# #endif
# #else
# #define RENDER_API __attribute__((visibility("default")))
# #endif
案例拆解:某 SDK 项目需要向客户提供动态库,但内部工具链使用静态库。项目结构如下:
# 内部核心算法,静态链接,不对外
add_library(core_algo STATIC src/algo.cpp)
target_include_directories(core_algo PRIVATE src)
# 对外 SDK 动态库,链接 core_algo
add_library(sdk SHARED src/sdk_api.cpp)
target_link_libraries(sdk PRIVATE core_algo)
target_include_directories(sdk PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
set_target_properties(sdk PROPERTIES
VERSION 3.0.0
SOVERSION 3
DEFINE_SYMBOL sdk_EXPORTS # 自动定义导出宏
)
这里使用 $<BUILD_INTERFACE> 和 $<INSTALL_INTERFACE> 生成器表达式,区分构建时和安装后的头文件路径,避免将本地路径泄漏到安装包中。DEFINE_SYMBOL 属性会自动为编译 sdk 目标时添加 -Dsdk_EXPORTS,配合头文件中的 #ifdef sdk_EXPORTS 实现正确的导出/导入切换。
4.5 常见误区与总结复盘
误区一:在 project() 之前使用 add_executable。这会导致 CMake 报错,因为项目语言和编译器尚未确定。
误区二:混淆目录级命令与目标级命令。例如在子目录中使用 include_directories() 会影响所有兄弟目标,造成隐式依赖。
误区三:动态库不设置 SOVERSION。在 Linux 上,没有 SOVERSION 的动态库升级时可能导致已链接的二进制程序崩溃,因为符号版本不匹配。
误区四:静态库编译时未开启 -fPIC。当静态库被链接进动态库时,链接器会报错“relocation R_X86_64_PC32 against symbol ... can not be used when making a shared object”。
总结复盘:本章的核心是建立“目标”思维。CMake 的一切操作都应围绕目标展开:创建目标、配置目标属性、管理目标间依赖。项目声明是地基,目标创建是骨架,属性配置是血肉,静态/动态库选择是架构决策。建议在项目初期就明确哪些目标是 PUBLIC 接口、哪些是 PRIVATE 实现,并严格使用 target_* 命令,避免目录级命令的滥用。下一阶段学习 install 和 export 时,你会发现清晰的目标属性定义是生成正确 CMake 包配置文件的必要条件。