一、 项目的心脏:project() 命令详解
在第二章,我们使用了最简单的project(HelloCMake LANGUAGES CXX)。现在,让我们解锁它的全部潜力。
-
基础语法与核心作用:
project(<PROJECT-NAME> [VERSION <min>[.<patch>[.<tweak>[.<suffix>]]] [DESCRIPTION <project-description-string>] [HOMEPAGE_URL <url-string>] [LANGUAGES <language-name>...])-
<PROJECT-NAME>(必填): 给你的项目起个名字!这个名字会设置一系列重要的内置变量(稍后详解)。 -
VERSION(可选): 为项目指定版本号(例如VERSION 1.0.0)。CMake 3.12+ 支持。设置后,会定义相关版本变量。 -
DESCRIPTION(可选): 提供项目的简短描述(例如DESCRIPTION "My Awesome Application")。CMake 3.9+ 支持。 -
HOMEPAGE_URL(可选): 设置项目的主页URL(例如HOMEPAGE_URL "https://github.com/me/myproject")。CMake 3.12+ 支持。 -
LANGUAGES(可选但强烈推荐): 明确项目使用的编程语言。常用值:C: C语言项目CXX: C++语言项目 (最常见)CUDA: CUDA项目Fortran,ASM等。可以指定多个,如LANGUAGES C CXX。如果不指定,CMake默认会启用C和CXX。 显式指定更清晰。
-
-
project()设置的关键内置变量:
执行project()命令后,CMake 会自动定义一组非常有用的变量,这些变量在整个CMakeLists.txt及其子目录中都可用(作用域后续讨论):PROJECT_NAME: 项目的名称(即你传入的<PROJECT-NAME>)。<PROJECT-NAME>_SOURCE_DIR/PROJECT_SOURCE_DIR: 项目源代码的根目录。 即包含顶层CMakeLists.txt的目录。这是最常用的变量之一。例如:${PROJECT_SOURCE_DIR}/include<PROJECT-NAME>_BINARY_DIR/PROJECT_BINARY_DIR: 项目构建的根目录。 即你运行cmake命令时所在的目录(通常是build目录)。这是最常用的变量之一。PROJECT_VERSION,<PROJECT-NAME>_VERSION: 项目的完整版本号(如果指定了VERSION)。PROJECT_VERSION_MAJOR,<PROJECT-NAME>_VERSION_MAJOR: 主版本号。PROJECT_VERSION_MINOR,<PROJECT-NAME>_VERSION_MINOR: 次版本号。PROJECT_VERSION_PATCH,<PROJECT-NAME>_VERSION_PATCH: 修订号。PROJECT_VERSION_TWEAK,<PROJECT-NAME>_VERSION_TWEAK: 微调号(较少用)。PROJECT_DESCRIPTION,<PROJECT-NAME>_DESCRIPTION: 项目描述(如果指定了DESCRIPTION)。PROJECT_HOMEPAGE_URL,<PROJECT-NAME>_HOMEPAGE_URL: 项目主页URL(如果指定了HOMEPAGE_URL)。CMAKE_PROJECT_NAME: 当前正在处理的最顶层项目的名称(在多项目配置中与PROJECT_NAME可能不同)。- 编译器相关变量 (检测后设置):
CMAKE_C_COMPILER,CMAKE_CXX_COMPILER,CMAKE_C_FLAGS,CMAKE_CXX_FLAGS等。这些通常在project()调用时或之后被检测和设置。
-
实践示例:
cmake_minimum_required(VERSION 3.12) # 需要支持 VERSION/DESCRIPTION project(MySuperApp VERSION 2.1.3 DESCRIPTION "A revolutionary application that does amazing things" HOMEPAGE_URL "https://github.com/awesomecoder/mysuperapp" LANGUAGES CXX ) message(STATUS "Project Name: ${PROJECT_NAME}") message(STATUS "Project Version: ${PROJECT_VERSION}") message(STATUS "Project Description: ${PROJECT_DESCRIPTION}") message(STATUS "Source Dir: ${PROJECT_SOURCE_DIR}") message(STATUS "Build Dir: ${PROJECT_BINARY_DIR}")运行
cmake时,你会看到这些变量的值被打印出来。这些变量在后续配置头文件、安装路径、打包信息等方面极其有用。
二、 构建模块:创建库 (add_library())
除了可执行文件 (add_executable),库是代码复用和项目组织的基石。add_library() 让你轻松创建各种类型的库。
-
基础语法与库类型:
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [<source>...])-
<name>(必填): 你要创建的库的目标名称 (Target Name) 。这个名字将在后续的target_link_libraries()等命令中使用。建议保持唯一性! -
库类型 (可选,但强烈建议指定):
STATIC: 创建静态库 (Static Library) 。在Linux/macOS上通常生成.a文件,在Windows上生成.lib文件。特点: 代码在链接时被直接复制到最终的可执行文件或库中。运行时不再需要静态库文件。生成的文件较大,但发布简单(不需要附带库文件)。SHARED: 创建动态库/共享库 (Shared Library / Dynamic Link Library - DLL) 。在Linux/macOS上生成.so(Shared Object) 文件,在Windows上生成.dll(及配套的.lib导入库)。特点: 代码不被复制到最终程序。程序运行时动态加载共享库。生成的可执行文件较小,但发布时需要附带相应的共享库文件。支持运行时更新(热插拔)。MODULE: 一种特殊的动态库,通常不作为其他目标的直接依赖,而是在运行时被动态加载(例如插件系统)。在Linux/macOS上类似.so,Windows上类似.dll。使用相对较少。- 不指定类型: CMake会根据
BUILD_SHARED_LIBS变量的值决定默认类型。如果BUILD_SHARED_LIBS为ON,默认创建SHARED库;如果为OFF(默认值),创建STATIC库。最佳实践:总是显式指定STATIC或SHARED! 避免歧义。
-
EXCLUDE_FROM_ALL(可选): 如果指定,该库不会被默认构建(即运行cmake --build .或make时不会构建)。需要显式指定目标构建(如cmake --build . --target MyLib或make MyLib)。用于构建可选的或测试用的库。 -
<source>...(必填): 构建这个库所需的源文件列表(.cpp,.c,.cu等)。可以列出多个文件,也可以使用变量或生成器表达式(后续介绍)。
-
-
创建库的示例:
场景: 假设我们的项目MySuperApp包含一个数学工具库MathUtils和一个主程序。
目录结构 (简化):MySuperApp/ ├── CMakeLists.txt # 顶层 ├── app/ │ ├── CMakeLists.txt # 子目录 (可选,演示作用域) │ └── main.cpp └── math/ ├── CMakeLists.txt # 子目录 (可选) ├── math_utils.h └── math_utils.cpp顶层
CMakeLists.txt(创建静态库示例):cmake_minimum_required(VERSION 3.12) project(MySuperApp VERSION 1.0 LANGUAGES CXX) # 添加 math 子目录。CMake会进入 math/ 并处理其 CMakeLists.txt add_subdirectory(math) # 添加 app 子目录 add_subdirectory(app)math/CMakeLists.txt(创建MathUtils静态库):# 在 math 目录下创建一个名为 MathUtils 的 STATIC 库 # 源文件:当前目录下的 math_utils.cpp # 头文件 math_utils.h 不需要在这里列出(用于编译),但它的位置需要告知使用者(后续章节) add_library(MathUtils STATIC math_utils.cpp) # (可选) 设置库目标属性,例如包含路径(后续章节详解) # target_include_directories(MathUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})app/CMakeLists.txt(创建链接库的可执行文件):# 创建一个名为 MySuperApp 的可执行文件,源文件是 main.cpp add_executable(MySuperApp main.cpp) # 告诉 CMake:MySuperApp 可执行文件需要链接到 MathUtils 库 # 这是初步写法,现代CMake更推荐使用 target_link_libraries 的 PUBLIC/PRIVATE/INTERFACE (第四章详解) target_link_libraries(MySuperApp PRIVATE MathUtils)构建过程:
-
mkdir build && cd build -
cmake .. -
cmake --build .
结果:
- 在
build/math/(或类似路径) 下生成libMathUtils.a(Linux/macOS) 或MathUtils.lib(Windows)。 - 在
build/app/下生成MySuperApp可执行文件,该文件内部包含了MathUtils库的代码(因为是静态链接)。
-
-
静态库 vs 动态库的选择:
-
使用静态库 (
STATIC):- 优点: 最终程序独立发布,不依赖外部库文件;程序启动可能稍快(无加载开销)。
- 缺点: 可执行文件体积大;如果多个程序使用同一个库,内存中会有多份库代码拷贝;库更新需要重新编译链接整个程序。
- 适用场景: 小型工具、嵌入式系统、要求独立发布的程序、不希望用户管理依赖。
-
使用动态库 (
SHARED):- 优点: 可执行文件体积小;多个程序可共享内存中的同一份库代码,节省内存;库可以独立更新(需注意ABI兼容性)。
- 缺点: 发布程序时需要附带相应的动态库文件(或确保目标系统已安装);运行时加载有轻微开销;存在“DLL Hell”(依赖冲突)风险(需管理好版本)。
- 适用场景: 大型应用、公共库(如系统API)、插件系统、需要热更新的场景。
-
三、 CMake的神经:变量 (set()) 与作用域
变量是CMake脚本中存储信息、控制流程的核心机制。理解变量的设置和作用域至关重要。
-
设置变量:
set()命令set(<variable> <value>... [CACHE <type> <docstring> [FORCE]] [PARENT_SCOPE])-
基本形式 (设置普通变量):
set(MY_VARIABLE "Hello, CMake Variables!") set(SOURCE_FILES main.cpp utils.cpp helper.cpp)<variable>: 变量名。CMake变量名区分大小写! 通常使用大写字母和下划线命名(如MY_VAR,PROJECT_SOURCES)。<value>...: 变量的值。可以是一个值,也可以是多个值组成的列表。如果包含空格,值需要用双引号括起来 ("value with space")。多个值会组成一个分号(;)分隔的列表。
-
访问变量:
${<variable>}message(STATUS "MY_VARIABLE is: ${MY_VARIABLE}") add_executable(MyApp ${SOURCE_FILES}) # 展开SOURCE_FILES列表 -
取消设置变量:
unset()unset(MY_VARIABLE) # 删除变量 MY_VARIABLE
-
-
变量作用域:代码执行的“领地”
CMake变量的可见性由其被定义的位置决定,这就是作用域。主要作用域类型:-
1. 目录作用域 (Directory Scope - 最常见):
-
变量在定义它的
CMakeLists.txt文件及其下级子目录的CMakeLists.txt文件中可见。 -
add_subdirectory()的影响: 当父目录使用add_subdirectory(subdir)进入子目录subdir时:- 父目录定义的普通变量会复制一份到子目录作用域(子目录的修改不会影响父目录的副本)。
- 子目录定义的普通变量在其自身作用域可见,但不会自动向上传递到父目录。
-
示例 (
project()设置的内置变量如PROJECT_SOURCE_DIR也是目录作用域):# 顶层 CMakeLists.txt set(TOP_LEVEL_VAR "I'm at the top") message("Top (Before subdir): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: I'm at the top add_subdirectory(subdir) message("Top (After subdir): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: I'm at the top (未被子目录修改)# subdir/CMakeLists.txt message("Subdir (Start): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: I'm at the top (父目录变量的副本) set(TOP_LEVEL_VAR "Modified in subdir") # 修改的是子目录作用域内的副本 set(SUB_VAR "I'm only in subdir") message("Subdir (End): TOP_LEVEL_VAR=${TOP_LEVEL_VAR}") # 输出: Modified in subdir# 回到顶层 CMakeLists.txt message("Top (After subdir): SUB_VAR=${SUB_VAR}") # 输出: SUB_VAR= (空! 子目录变量不可见)
-
-
2. 函数作用域 (Function Scope):
-
在
function() ... endfunction()中定义的变量,默认只在该函数内部可见。 -
函数参数 (
ARGV,ARGN) 也属于函数作用域。 -
PARENT_SCOPE关键字: 如果需要在函数内部修改调用者作用域(通常是定义函数的目录作用域)的变量,必须使用set(... PARENT_SCOPE)。function(my_function) set(INSIDE_FUNC "Local Value") # 只在函数内可见 set(OUTSIDE_VAR "Changed Inside" PARENT_SCOPE) # 修改调用者的 OUTSIDE_VAR endfunction() set(OUTSIDE_VAR "Original") my_function() message("OUTSIDE_VAR=${OUTSIDE_VAR}") # 输出: Changed Inside message("INSIDE_FUNC=${INSIDE_FUNC}") # 输出: (空! 函数内变量不可见)
-
-
3. 持久缓存作用域 (Cache Scope):
-
使用
set(... CACHE ...)设置的变量称为缓存变量 (Cache Variables) 。 -
它们存储在
CMakeCache.txt文件中(位于构建目录)。即使你修改了CMakeLists.txt并重新运行cmake,这些变量的值默认会保留(除非你显式删除缓存或使用FORCE覆盖)。 -
缓存变量在整个项目(所有目录、函数)中全局可见。
-
语法:
set(<variable> <value> CACHE <type> <docstring> [FORCE])-
<type>: 变量类型,主要影响GUI工具中的显示。常用类型:BOOL(ON/OFF): 布尔值 (复选框)FILEPATH: 文件路径 (文件选择对话框)PATH: 目录路径 (目录选择对话框)STRING: 字符串 (文本输入框)INTERNAL: 内部变量 (通常不在GUI中显示)
-
<docstring>: 变量的描述文本,会在GUI工具和cmake -L等命令中显示。 -
FORCE: 强制覆盖缓存中已存在的值。慎用!
-
-
示例 (定义一个开关选项):
option(ENABLE_FEATURE_X "Enable the experimental Feature X" OFF) # option() 本质是 set(... CACHE BOOL ...) set(INSTALL_PREFIX "/usr/local" CACHE PATH "Where to install the software") -
访问缓存变量: 同样使用
${<variable>}。如果存在同名的普通变量,普通变量优先。要强制访问缓存变量,使用$CACHE{<variable>}(CMake 3.13+)。
-
-
-
环境变量:
- 访问系统环境变量使用
$ENV{<VARNAME>}。 - 设置环境变量只在当前CMake进程及其生成的子进程(如构建命令)中有效,使用
set(ENV{<VARNAME>} <value>)。
message("PATH is: $ENV{PATH}") set(ENV{TEMP_DIR} "/tmp/my_temp") # 仅影响CMake进程及后续构建命令 - 访问系统环境变量使用
四、 本章小结
本章我们深入探索了 CMakeLists.txt 的三大核心构件:
-
project()命令:- 定义了项目名称、版本、描述、主页和语言。
- 设置了关键的内置目录变量
PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR和版本变量。 - 是CMake配置的起点。
-
add_library()命令:- 用于创建静态库 (
STATIC)、共享库 (SHARED) 或模块库 (MODULE)。 - 定义了库目标 (
<name>) 及其源文件。 - 静态库 vs 动态库的选择取决于项目需求(大小、独立性、更新策略)。
- 库目标是现代CMake管理依赖的基础。
- 用于创建静态库 (
-
变量系统:
-
set()用于设置变量,unset()用于取消设置,${}用于访问。 -
作用域是核心概念:
- 目录作用域: 变量在定义目录及其子目录可见(
add_subdirectory会复制父变量到子作用域)。 - 函数作用域: 变量只在函数内部可见,修改父作用域需
PARENT_SCOPE。 - 缓存作用域:
set(... CACHE ...)定义的变量全局可见,持久存储在CMakeCache.txt中。
- 目录作用域: 变量在定义目录及其子目录可见(
-
环境变量通过
$ENV{...}访问和设置。
-