一、CMAKE 到底是什么?
CMAKE 是“构建系统”的生成器
这里的“构建系统”是指不同平台上面负责驱动编译过程的工具,比如linux上面的构建系统是make,Ninja;windows上面的构建系统是MSBuild。“构建系统”的作用是主要是 负责管理编译顺序,检查那些文件需要重新编译。真正负责编译的是 gcc、clang、MSVC 等编译器。
“生成器”是指根据 Generator 生成对应工程文件的,linux上面是生成makefile,build.ninja文件,windows是生成sln工程文件或者 build.ninja之类的。
cmake -G "Unix Makefiles" ..
cmake -G "Ninja" ..
cmake -G "Visual Studio 17 2022" ..他们之间的关系是 CMake → 生成 Makefile/.sln → 构建系统(make/MSBuild)→ 调用编译器(gcc/cl.exe)→ 编译链接
根据上面的内容得出:cmake其实就是一个可以运行在多个平台的生成器软件,他为不同平台的“构建系统”提供了对应项的工程文件或构建脚本,在由构建系统调用编译器去完成编译链接。
单配置和多配置生成器
在深入单配置和多配置生成器之前,我们首先需要明确一个核心概念:构建配置(Build Configuration) 在 C/C++ 工程中,最常见的构建配置有以下四种:
- Debug:包含调试符号,关闭优化,便于断点调试。
- Release:去除调试符号,开启高度优化,追求极致的运行速度。
- RelWithDebInfo:开启优化的同时保留调试符号,兼顾运行效率与调试可能性。
- MinSizeRel:专门针对程序体积进行优化,常用于嵌入式或存储空间受限的场景** 还有的话,就是 构建类型(Build Type) :仅当特指 CMake 中的
CMAKE_BUILD_TYPE变量时使用(该变量用于单配置生成器)。
单配置生成器 生成工程时就定死一种类型(比如 Debug),编译时不能改。
想换 Release?必须重新跑 CMake 另生成一个工程。
指定方式:cmake -DCMAKE_BUILD_TYPE=Debug ..
常见:Unix Makefiles、Ninja、FASTBuild
多配置生成器
一个工程同时包含所有配置(Debug、Release 等),编译时再临时选。
换配置不用重生成,直接切就行。
指定方式:cmake --build . --config Release
常见:Visual Studio、Xcode、Ninja Multi-Config
二、开始使用CMake
基础命令与概念
- 任何CMake项目根目录下的 CMakeLists.txt 前两条命令必须是
cmake_minimum_required()(版本声明) 和projet()(申明项目名)。 围绕四个核心命令展开:
add_executable()|| 创建可执行文件add_library()|| 创建库;target_sources()|| 关联源文件;target_link_libraries()|| 描述目标间的依赖关系。
- 引入“目标(target)”概念:目标是属性的集合(产物类型、源文件、头文件、链接库等)。
cmake_minimum_required
声明项目需要的最低 CMake 版本
cmake_minimum_required(VERSION 3.10...3.31)
# 3.10 是项目所需最低CMake版本。
# 3.31 是项目已主动适配的最高策略版本。
# 3.31 如果当前实际使用CMake 版本高于 3.31 ,而是继续使用 3.31 的 Policy 行为进行配置.add_executable
使用指定的源文件向项目中添加一个可执行文件。
add_executable(<目标名> [选项] <源文件...>)
目标名:编译生成的可执行文件名(自动为所在平台添加后缀名,如.exe)。
源文件:通常为 .c、.cpp、.cc、.cxx、.rc 等。头文件(如 .h、.hpp)也可以写入目标中用于 IDE 展示,但不会参与编译。
选 项:[选项] 这个参数有点特别,他又多个参数类型,且参数代表的含义不同,具体如下
WIN32: 在 Windows 上创建带有WinMain()入口点,使程序成为GUI可执行文件,而不是控制台程序。MACOSX_BUNDLE:在 MacOS 下生成.app常用程序包。EXCLUDE_FROM_ALL:被添加了这个参数的可执行文件不会参与make all或cmake --build .,必须手动指定目标名才能编译。
示例
# 最简用法
add_executable(my_prog main.cpp)
# 别名示例 (ALIAS)
add_executable(real_main main.cpp)
add_executable(app ALIAS real_main) # 之后可用 `app` 引用
# 变量(指定方式,这样方便复用,但实际开发中,推荐使用 target_sources )
set(SOURCES main.cpp helper.cpp)
add_executable(app ${SOURCES})
#指定当权目录(CMakeLists.txt)下的路径
add_executable(app ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)
# 修改输出文件名(win系统输出:myapp.exe || linux系统输出:myapp)
set_target_properties(app PROPERTIES OUTPUT_NAME "myapp")
# 导入外部工具(IMPORTED)
add_executable(ffmpeg_tool IMPORTED)
set_property(TARGET ffmpeg_tool PROPERTY IMPORTED_LOCATION "ffmpeg.exe")注意事项
- 在 CMake 配置阶段中,我们通过
add_executable、add_library、add_custom_target定义的变量,统一称为目标(Target)。这些目标(Target)在项目中必须全局唯一,不能与库或者自定义目标重名。
# 报错例子:示例一(my_app)
add_executable(my_app main.cpp) # 可执行目标名叫 my_app
add_library(my_app STATIC util.cpp) # 执行cmake报错。因为库名重复
# 报错例子:示例二
add_executable(hello hello.cpp)
add_custom_target(hello COMMAND echo "Hi") # 执行cmake报错,因为前面已经定义过目标
# 通过 add_subdirectory 引入的 子项目/模块, ,一样是不能有重复的目标名。- 在实际开发工程中,并不推荐使用
*.cpp通配符来添加源文件。如果使用*.cpp来添加源文件,后续有新增加的源文件,这样不会直接修改CMakeLists.txt,让构建系统读取到的CMakeLists.txt文件时间戳并没有修改,则之前已有的构建系统文件(Makefile或.vcxproj) 中也不会包含新的源文件,导致不会参与编译。这时候必须手动跑一次CMake命令才行。且*.cpp也有CONFIGURE_DEPENDS来解决上述通配符的问题,但在大型项目中性能开销比较大。 IMPORTED是引用外部已有的(库或者可执行)文件。他只能用于被依赖关系,提供给其他目标使用,且不会被编译。当引用外部文件不存在时,或需要动态生成或存在性无法保证时,才会需要配合add_custom_command/add_dependencies来管理文件就绪顺序。
add_library()
add_library() 用于创建 库目标(Target)。# 语法结构
add_library(<name> [类型] [sources...])
# 创建后的 <name> 是 CMake 逻辑目标,不一定等于最终文件名。常见普通库
其中 [类型] 分为:STATIC: 静态库(STATIC)用于在链接其他目标时使用的目标文件归档。SHARED: 动态库(SHARED)可以被其他目标链接并在运行时加载的动态库。MODULE: 模块库(MODULE)一种插件,不能被其他目标链接,但可以使用类似 dlopen 的功能在运行时动态加载。
- 静态库的特点编译时复制代码进可执行文件,运行时不依赖库文件,文件较大,生成的是
.a/.lib文件。 - 动态库的特点是运行时加载,多个程序共享,需要处理动态库搜索路径,生成的是
.so/.dll/.dylib文件。 - 模块库的特点是不能被
target_link_libraries()链接,运行时动态加载。通常用于插件系统/Qt Plugin/dlopen 加载。 - 如果
add_library()不指定类型如add_library(MyLib a.cpp)那么默认类型可能是由BUILD_SHARED_LIBS来决定类型的。
对象库
对象库是将源代码编译成中间目标文件(.o/.obj)后立即停止的“半成品集合”,既不打包成静态库(.a)也不链接成动态库(.so)。它的唯一使命是让多个最终库(STATIC/SHARED)通过直接搬运这些中间文件来复用编译结果,从而避免同一份源码被反复编译——这是大型项目缩短构建时间、同时保持代码复用性的核心利器。并且,它依然拥有独立目标的属性,能通过 target_link_libraries 将自身的头文件路径和宏定义传递给使用者。
add_library(name OBJECT sources...)- 对象库只编译源码,不生成
.a/.so,只生成.o/.obj。 - 对象库一般用于多个库共享源码,避免重复编译。
# 基础用法。
# 1. 创建对象库(编译 a.cpp 和 b.cpp 生成 .o 文件)
add_library(core OBJECT a.cpp b.cpp)
# 2. 静态库 MyLib 直接"物理搬运"这些 .o 文件
add_library(MyLib STATIC $<TARGET_OBJECTS:core>)
# 3. 另一个动态库 YourLib 也搬运同一批 .o 文件(无需重新编译 a.cpp/b.cpp)
add_library(YourLib SHARED $<TARGET_OBJECTS:core>)需要注意的是:
- 不能与普通源文件混写,解决办法是把 新的源文件 也变成 某个对象库 在键入。
- 编译参数(头文件路径/宏)不会自动传递,这里的意思就是
$<TARGET_OBJECTS:core>只搬运了 编译好的二进制.o文件,但没有搬运core目标设置的include目录和宏定义。这里的解决办法,就不写了。感觉挺长的。
接口库
接口库(INTERFACE)是 CMake 中一个纯逻辑的“虚拟标签”,在磁盘上不生成任何库文件(不编译源码、不产.a或.so)。它的唯一价值是打包“使用要求”——即把头文件路径、宏定义、编译选项等捆绑成一个逻辑目标。当其他目标通过target_link_libraries链接它时,会自动继承这些配置,而不会链接任何实体库文件。最典型的场景是管理纯头文件库(如 Eigen、nlohmann/json)或统一项目通用的编译标准(如-std=c++17),让依赖传递变得干净利落。
add_library(name INTERFACE)- 接口库的特点是不编译源码,不生成文件,只保存使用要求。
- 接口库主要保存:
include路径、编译宏、编译选项、依赖库。 - 接口库的作用是:管理纯头文件库,传递编译信息,聚合依赖链。
举例:
# 1.创建一个接口库,添加 头文件搜索路径(include) 和 声明预处理器宏定义(DEBUG_MODE)
add_library(Config INTERFACE)
target_include_directories(Config INTERFACE include)
target_compile_definitions(Config INTERFACE DEBUG_MODE)
# 2.然后 App库 使用 target_link_libraries 链接接口库Config
target_link_libraries(App PRIVATE Config)
# 3.这样调用以后,就避免了Config生成文件。但又能把Config 和App连接起来。APP 内也有了 include和 DEBUG_MODE 编译规则。
导入库
导入库是一个指向磁盘上现有二进制文件的“目标占位符”,让当前项目能像链接自己编译的库已有target_link_libraries它,且不会再构建时重新编译它。它本质上只是一个拥有INTERFACE属性的空壳。
一般导入的第三方库还需要提供 头文件,不然没办法用。
add_library(<name> <type> IMPORTED [GLOBAL])<type>:必须指定STATIC、SHARED、MODULE、UNKNOWN、OBJECT、INTERFACE。IMPORTED:固定关键字,声明这是一个导入目标。GLOBAL:可选。不加时目标仅在当前目录及子目录可见;加上GLOBAL后,整个项目均可引用该目标。
路径属性
IMPORTED_LOCATION:指定主库文件的绝对路径。配置变体:
IMPORTED_LOCATION_DEBUG、IMPORTED_LOCATION_RELEASE等,允许不同构建配置使用不同路径。# 基本 set_target_properties(bar PROPERTIES IMPORTED_LOCATION "/absolute/path/to/libbar.so" ) set_target_properties(bar PROPERTIES IMPORTED_LOCATION_DEBUG "/path/to/libbar_debug.so" IMPORTED_LOCATION_RELEASE "/path/to/libbar_release.so" ) # 这样 编译时会自动根据编译类型 选择 Debug 或 release 的第三方二进制库。
Windows 动态库
Windows 上使用一个动态库通常包含2个文件:
.dll:程序运行时真正需要的文件,里面是可执行代码。.lib:"导入库",只包含"怎么找到 dll 里函数"的跳转表,链接器在编译时需要这个文件。set_target_properties(MyDll PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/MyDll.lib" # 给链接器看的 IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/bin/MyDll.dll" # 运行时真正用的 )
OBJECT 类型
一般库是 .a、.so、.lib、.dll,而“对象文件”就是 .o 或 .obj,是编译产生的中间文件,还没被链接成库。
add_library(MyObj OBJECT IMPORTED)
set_target_properties(MyObj PROPERTIES
IMPORTED_OBJECTS "/path/a.o" "/path/b.o"
)Unix(Linux/macOS)动态库补充
SONAME 是写在动态库文件内部的 ABI 兼容主版本标识。运行时系统通过它来识别和加载库,而不是靠文件名。- 若库文件有
SONAME(或 macOS 的LC_ID_DYLIB),应设置IMPORTED_SONAME。 - 若平台支持
SONAME但库文件无此信息,设置IMPORTED_NO_SONAME为TRUE。 SONAME可以通过 Linux 的readelf命令查询 。
举例
set_target_properties(foo PROPERTIES
IMPORTED_LOCATION /path/to/libfoo.so.1.0.0
IMPORTED_SONAME libfoo.so.1 # 告诉 CMake 他的内部版本之类的。
)
# 如果库没有SONAME
set_target_properties(foo PROPERTIES
IMPORTED_LOCATION /path/to/libcustom.so
IMPORTED_NO_SONAME TRUE # 这是个没 SONAME 的库,别乱猜
)属性的传递
虽然导入库不编译源码,但仍可通过设置 INTERFACE_* 属性来携带依赖信息:cmake_minimum_required(VERSION 3.10)
project(MyApp)
# 创建导入库目标
add_library(MathLib SHARED IMPORTED)
# 指定库文件位置
set_target_properties(MathLib PROPERTIES
IMPORTED_LOCATION "/opt/math/lib/libmath.so"
)
# 写“使用说明书”
target_include_directories(MathLib INTERFACE /opt/math/include)
target_compile_definitions(MathLib INTERFACE USE_MATHLIB)
target_compile_options(MathLib INTERFACE -ffast-math)
target_link_libraries(MathLib INTERFACE pthread)
# 其他使用者导入方式
add_executable(calculator main.cpp)
target_link_libraries(calculator MathLib)
# calculator 获得 USE_MATHLIB、-ffast-math、pthread 属性别名库
创建一个 别名目标,以便在后续命令中使用<name>来引用<target>。<name>不会作为 make 目标出现在生成的构建系统中。<target>不能是 ALIAS。
add_library(<name> ALIAS <target>)target_sources()
概述
target_sources()的作用是向已经创建的 target 目标(如add_library、add_executable) 追加源码文件和修改属性,并通过PRIVATE、PUBLIC、INTERFACE指定这些文件的作用域。相比于直接在
add_library()或add_executable()中一次性列出私有源码文件路径,target_sources()更加适合将项目模块化拆分,可以在多个CMakeLists.txt中分阶段追加源码文件。
target_sources()还可以结合现代 CMake 的FILE_SET功能统一管理头文件,并配合install(TARGETS ... FILE_SET ...)安装,无需重复维护头文件列表。因为FILE_SET本身不会自动安装,还需要install()显示指定。
命令结构
# 示例
target_sources(MyLibrary
PRIVATE
library_implementation.cxx
# 这里的源码如果不需要给他模块用,直接在这里定义为Private 最省事。
PUBLIC
FILE_SET myHeaders
TYPE HEADERS
BASE_DIRS
include
FILES
include/library_header.h
)
# 命令结构
target_sources(<target>
<PRIVATE|PUBLIC|INTERFACE> [普通源码文件列表...]
[<PRIVATE|PUBLIC|INTERFACE> [普通源码文件列表...] ...]
[<PRIVATE|PUBLIC|INTERFACE>
FILE_SET <文件集名称>
TYPE <类型>
BASE_DIRS <目录...>
FILES <文件...>
]
...
)<>:是必须填写的参数。[]:是可选项。target:是目标昵称,必须已经存在;由add_library或者add_executable创建。FILE_SET <文件集名称>:是给 target 添加一个有名字的文件集合,该参数用于后续install命令 和 追加文件。TYPE <类型>:- 类型
HEADERS表示这些文件是头文件,不会被编译。(++常用++) - 类型
CXX_MODULES表示这些文件是作为 Cpp 模块源文件 编译,CMake会自动处理模块之间的依赖扫描。 (++了解即可++) - 类型
CXX_MODULE_HEADER_UNITS表示 C++20 Header Unit(头文件单元),用于将传统头文件作为 Header Unit 管理,目前实际项目中使用较少。(了解即可)
- 类型
BASE_DIRS <目录...>:表示定义文件集的“根目录”,CMake 会基于 BASE_DIRS 计算每个文件的相对路径。- 安装时自动保留目录结构。
- 在 IDE 中按目录分组显示文件。
- 所有的
FILES必须位于某个BASE_DIRS目录下,否则报错。 - 若有多个基础目录,CMake自动使用“ 最长前缀匹配”选择最匹配的路径,作为基础目录。
FILES <文件...>- 这个参数用于列出那些文件属于这个文件集的。
- 具体文件路径是相当于当前
CMakeLists.txt。 - 该路径必须位于
BASE_DIRS的某个目录下,否则配置报错。 - 可以写多个文件路径,用空格或者换行分割。
- 通常用于头文件(
TYPE HEADERS),不要与普通源文件(.cpp、.cxx)列表混淆了。 - 安装后,其他项目使用
#include包含头文件时,所写的路径与该文件相对于BASE_DIRS的路径完全一致。
target_link_libraries()
该命令主要是告知构建系统,某个目标(可执行文件或者库)需要链接那些库。
基本语法
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...] ...
)<target>:由add_executable()和add_library()创建的目标昵称。<item>:可以是另一个CMake目标、库文件名、完整路径、链接标识符、生成表达式等。CMake目标 可以理解为 add_executable 、 add_library 、导入目标 创建的目标名称。
示例
# 一、链接另一个CMake目标 add_library(bar STATIC bar.cpp) ... target_link_libraries(my_app PRIVATE bar) # 二、按库文件名 / 路径 target_link_libraries(my_app PRIVATE /path/to/libfoo.a) target_link_libraries(my_app PRIVATE foo) # 会交给链接器查找 (-lfoo) # 三、链接标志 target_link_libraries(my_app PRIVATE "-framework Cocoa") # macOS 框架 target_link_libraries(my_app PRIVATE -pthread) # 三、更现代的命令方式 find_package(Threads REQUIRED) target_link_libraries(my_app PRIVATE Threads::Threads) # 四、生成器表达式 target_link_libraries(my_app PRIVATE $<$<CONFIG:Debug>:debug_lib> $<$<CONFIG:Release>:optimized_lib> )
注意事项
- 重复调用 target_link_libraries 是追加。然后每次调用是按顺序累积。链接器要放在依赖库的后面,复杂情况下可能还需要手动调整。
- 不推荐使用
link_libraries()它是全局命令,会影响之后创建的所有目标,缺乏范围控制,容易污染工程。 - 需要区分“引入头文件”和“链接库”。一般链接库用
target_link_libraries,引入头文件是用target_include_directories。 - 应避免两个库互相 PUBLIC 链接,会导致 CMake 错误或链接顺序混乱。
add_subdirectory()
在构建中添加子目录。
基本语法
# 示例
add_subdirectory(thirdparty/libfoo libs EXCLUDE_FROM_ALL SYSTEM)
# 语法结构
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])参数说明
source_dir:子目录路径,必须包含CMakeLists.txt。可以使用相对路径/绝对路径。binary_dir:指定该子目录在构建树中的输出位置。不指定时,默认在构建目录下创建一个与 source_dir 原名相同的目录。相对路径基于当前构建目录解析。EXCLUDE_FROM_ALL:使子目录内定义的目标不参与默认构建。(这参数没有其他变量)SYSTEM:告知编译器,忽略这个第三方库的报错。(这个参数没有其他变量)
三、CMake 语言基础
宏、函数和列表
macro(宏)
macro() 宏命令用于定义一个简单的,参数化的代码块。macro 有点类似于C语言的宏定义,只是将要执行的内容进行一个文本替换。执行调用 Macro() ,CMake只会将宏内容复制到调用位置,进行字符串替换。
语法
macro(<name> [<arg1> ...])
<commands>
endmacro([<name>]) # 可选的 name 必须完全一致- 定义时不会立即执行宏体,仅在调用时执行。
- 习惯上宏名全小写。
- 大小写不敏感:foo() Foo() FOO() 均可,但推荐与定义一致。
- 形式参数:
${arg1}直接做字符串替换 - 需要特别注意,不建议使用 macro 在日常开发中,优先考虑用 function 实现。
function()
function()的功能和其他编程语言的function有点类似,function()用于录制一段 CMake 命令,将其封装为一个自定义函数命令。录制后不会立即执行,等待后续通过函数名调用才会运行。它可以让 CMake实现模块化,减少重复。
语法结构
function(<name> [<arg1> ...])
<commands>
endfunction()
# 调用方式
<name>([<arg1> ...])
# 3.18 新增的通用调用方式
cmake_language(CALL foo)<name>:函数名,调用时对大小写不敏感,但日常使用中最好保持于定义一致的大小写风格。<arg1> ...:形参参数列表,参数可选,不强制必须填写。<commands>:函数体,在调用时才会被执行的CMake 命令集。(这里写自定义CMake命令)endfunction():这里表述函数体结束。
函数提供的变量
ARGC:获取函数传递了多少个参数。(只有在if()里才不需要加${},其他地方message、set必须用${ARGC})ARGV0,ARGV1,ARGV2, …:分别代表第0、1、2...个参数。ARGV:所有参数打包成一个列表。ARGN:超出形参数量的那部分参数列表。CMake的function()形参在定义时可以不用写出来,调用处一样可以写入实参,函数内用ARGC、ARGV0、ARGV、ARGN一样可以处理实参数据。
注意事项
- CMake 的
fcuntion()一样存在作用域。函数内部定义的变量,外部读取不到。 - 想要在函数内修改外边的变量得使用
PARENT_SCOPE,例如:set(变量 值 PARENT_SCOPE)。 - CMake 的
function()是没有return 值这种说法的。都是通过设置一个变量带PARENT_SCOPE,将结果传递出去。 - CMake 函数内想要中途推出 可以使用
return()。 使用场景:
- 优化重复命令代码块
- 自定义构建规则,统一项目风格
- 批量设置属性、安装规则
- 测试用例、文件复制等
- 将复杂的
if-else封装到函数内
完整示例
# 定义一个函数,封装 add_executable + 自动链接 pthread
function(add_threaded_app name)
add_executable(${name} ${ARGV})
target_link_libraries(${name} PRIVATE pthread)
endfunction()
# 使用,和直接写 add_executable 几乎一样短
add_threaded_app(server server.cpp utils.cpp)
add_threaded_app(client client.cpp)List()
在CMake 中,列表就是分号分隔的字符串。
概述
# 读取
list(LENGTH <列表> <输出变量>)
# 获取列表长度。
list(GET <列表> <元素索引> [<索引>...] <输出变量>)
# 根据一个或多个索引,获取列表中对应的元素。
list(JOIN <列表> <连接符> <输出变量>)
# 根据一个或多个索引,获取列表中对应的元素。
list(SUBLIST <列表> <起始索引> <长度> <输出变量>)
# 截取列表的一部分。
# 搜索
list(FIND <列表> <要查找的值> <输出变量>)
# 在列表中查找指定值,返回其索引位置;若未找到则返回 -1
# 修改
list(APPEND <列表> [<元素>...])
# 在列表末尾追加一个或多个元素。
list(FILTER <list> <INCLUDE|EXCLUDE> <MODE>)
# 根据 正则表达式 模式过滤列表,INCLUDE 保留匹配项,EXCLUDE 移除匹配项。
list(INSERT <列表> <索引> [<元素>...])
# 在指定的索引位置插入一个或多个元素。
list(POP_BACK <列表> [<输出变量>...])
# 移除并弹出列表末尾的一个或多个元素(可将弹出的值存入变量)。
list(POP_FRONT <列表> [<输出变量>...])
# 移除并弹出列表开头的一个或多个元素(可将弹出的值存入变量)。
list(PREPEND <列表> [<元素>...])
# 在列表开头插入一个或多个元素
list(REMOVE_ITEM <列表> <值>...)
# 从列表中移除所有与指定值相等的元素。
list(REMOVE_AT <列表> <索引>...)
# 移除列表中指定索引位置的一个或多个元素。
list(REMOVE_DUPLICATES <列表>)
# 移除列表中重复的元素(只保留第一次出现的那一项)。
list(TRANSFORM <列表> <操作> [...])
# 列表中的每个元素进行某种转换操作(例如转为大写、添加前后缀等)。
# 排序
list(REVERSE <列表>)
#将列表中的元素顺序完全反转。
list(SORT <列表> [...])
# 对列表中的元素进行排序(可附加参数指定升序/降序或自定义比较规则)。示例
cmake_minimum_required(VERSION 3.15)
project(ShortListDemo)
# 初始列表:水果
set(FRUITS "apple" "banana" "apple" "orange")
# 1. 读取:查长度
list(LENGTH FRUITS len)
message(STATUS "长度: ${len}")
# 2. 搜索:找"orange"在哪
list(FIND FRUITS "orange" idx)
message(STATUS "'orange' 的索引: ${idx}")
# 3. 修改:追加、移除所有"apple"、去重
list(APPEND FRUITS "grape") # 末尾加个葡萄
list(REMOVE_ITEM FRUITS "apple") # 删掉所有苹果
list(REMOVE_DUPLICATES FRUITS) # 去掉重复项
message(STATUS "修改后: ${FRUITS}") # 结果:banana;orange;grape
# 4. 排序:按字母升序排一下
list(SORT FRUITS)
message(STATUS "排序后: ${FRUITS}") # 结果:banana;grape;orange
# 输出结果
长度: 4
'orange' 的索引: 3
修改后: banana;orange;grape
排序后: banana;grape;orangeset()
基本语法
set(<var> <value>... [PARENT_SCOPE])
# 普通变量 , <var> 可以理解为变量
set(CACHE{<variable>} [TYPE <type>] [HELP <helpstring>...] [FORCE] VALUE [<value>...])
# 缓存变量(≥4.2,推荐)
set(ENV{<var>} [<value>])
# 环境变量普通变量
- 多个
<value>表示这是一个列表,内部元素用分隔符隔开。 set(VAR)表示没有值,unset(VAR)表示清除变量。- 每个函数、
add_subdirectory()、block()都会新建作用域。 PARENT_SCOPE将变量设置到上一层,当前作用域保持不变。
缓存变量
TYPE <type>:指定缓存类型,<type>必须是以下类型之一。BOOL:布尔值ON/OFFFILEPATH:磁盘上的文件路径。PATH:磁盘上的目录路径。STRING:一行文本,GUI 中显示为文本框(或下拉列表),供用户配置。INTERNAL:一行文本,隐藏的缓存类型,自带FORCE,用于存储 CMake 内部计算结果,不出现在 GUI 中。- 如果未指定
TYPE,且如果缓存变量已存在且其类型不是UNINITIALIZED(未初始化),则保留之前指定的类型;否则使用 STRING。
[HELP <helpstring>...]:给 CMake-GUI 提供帮助文本信息。如果未指定 HELP,则使用空字符串。[FORCE]:这个参数表述强制覆盖缓存变量中的值。哪怕该变量的值已存在,也会强制覆盖为新值。VALUE <value>...:该参数用于设置 缓存变量具体的值(可以是多个,构成列表),但必须放在命令最后。该命令经典使用场景包括:
- 提供构建选项。
- 指定外部依赖路径。(比如:设置安装后目录路径)
- 存储内部计算中间结果。
环境变量
- 用于在 CMake 进程内部设置或清除环境变量,仅对当前 CMake 运行有效,不影响外部系统环境及后续构建进程。
- 了解即可,不算特别常用。
条件判断和循环
if()
语法结构
if(<condition>)
<commands>
elseif(<condition>) # 可选,可重复
<commands>
else() # 可选
<commands>
endif()真值判断
if(<常数 constant>):常量为1,ON,YES,TRUE,Y, 或非零数字。if(<variable>):变量已定义且其值不是假常量(如OFF,NO,FALSE,0, 空字符串,-NOTFOUND结尾等)。if(<string>):字符串是带引号的字面量,通常为假。除非字符串内容本身就是真常量。if():永远为假。
逻辑运算符
NOT:条件为假时为真AND:两者都为真时为真OR:任一为真时为真():圆括号优先计算
存在性检查
if(COMMAND cmd):cmd是否一个可调用的命令/宏/函数if(TARGET tgt):tgt是否由add_executable、add_library等创建的目标if(TEST t):t是否由add_test创建的测试if(DEFINED name):变量name是否已被定义(无论值是什么)if(DEFINED CACHE{name}):缓存变量name是否存在if(DEFINED ENV{name}):环境变量name是否存在if(POLICY CMP<NNNN>):该策略是否存在。if(<el> IN_LIST <list_var>):元素<el>在列表变量<list_var>中
文件系统检查
if(EXISTS path)路径存在且可读if(IS_READABLE path)路径可读if(IS_WRITABLE path)路径可写if(IS_EXECUTABLE path)路径可执行if(IS_DIRECTORY path)路径是目录if(IS_SYMLINK path)路径是符号链接if(IS_ABSOLUTE path)是绝对路径if(<file1> IS_NEWER_THAN <file2>)file1 是否比 file2 新,或任一文件不存在
数值比较
LESS:小于GREATER:大于EQUAL:等于LESS_EQUAL:小于等于GREATER_EQUAL:大于等于
举例:if(<var|string> LESS <var|string>) # 作为实数比较
字符串比较
STRLESS:字符串小于STRGREATER:字符串大于STREQUAL:字符串等于STRLESS_EQUAL:字符串小于等于STRGREATER_EQUAL:字符串大于等于
距离:if(<var|string> STREQUAL <var|string>)
版本比较
VERSION_LESS:版本号小于VERSION_GREATER:版本号大于VERSION_EQUAL:版本号等于VERSION_LESS_EQUAL:版本号小于等于VERSION_GREATER_EQUAL:版本号大于等于
路径比较
if(<path1> PATH_EQUAL <path2>)注意:
- Windows 下的路径比较默认不区分大小写,Linux 下区分大小写
- cmake 里面的相对路径是以 cmake 命令执行的目录路径为基准。
- 比较的适合会拼接为完整的路径,然后按路径组件逐项比较,在比较过程中自动处理多余的分隔符。
其他事项
- 正则匹配:
if(<var|string> MATCHES <regex>) - 避免直接写
if(${SOME_VAR}),推荐 使用if(SOME_VAR)或者if(“\${SOME_VAR}”)。这里没有\,因为MD语法限制了才写的。 - 在CMake 中,if 判断会先把左右两边的条件都算出来,在做逻辑判断,所以有些时候得用嵌套if 来保证先决条件的检查。
foreach()
基本语法
foreach(<loop_var> <items>)
<commands>
endforeach()<items>是由分号或空格分隔的列表。在IN LISTS语法下,可以是指向多个列表变量的名称。<loop_var>是循环变量,在每次迭代中代表拷贝当前列表元素的值,修改它不会影响到原始列表。break()跳出整个循环,continue()跳过本次迭代。foreach没有作用域,循环变量loop_var仅在循环内部有效,结束循环后自动删除。
范围迭代
foreach(i RANGE 3) # 0 1 2 3 :输出结果
# 这里的3, 表示从0开始,循环到3结束,一共4次循环
foreach(i RANGE 1 5 2) # 1 3 5 :输出结果
# 这里表示 i从 1开始,直接到5,每次递增i的值加2参数必须非负整数。
遍历列表变量
foreach(<loop_var> IN [LISTS <lists>] [ITEMS [<items>]]) # 示例 set(A "x;y") set(B "1 2") foreach(v IN LISTS A B ITEMS "hello" world) message("${v}") endforeach() # 输出:x y 1 2 hello worldLISTS后跟列表变量名(列表变量名会被展开,遍历每个变量的每一个项)ITEMS后直接写项,等同于第一种基本形式。- 可以
LISTS和ITEMS组合使用,也可只写一种。 - 若
LISTS和ITEMS都没给项,循环不执行。
并行遍历
foreach(var IN ZIP_LISTS A B) # 生成 var_0, var_1
foreach(x y IN ZIP_LISTS A B) # 分别对应两个列表
# 示例
set(L1 "a;b")
set(L2 "1;2;3")
foreach(x IN ZIP_LISTS L1 L2)
message("x_0=${x_0}, x_1=${x_1}")
endforeach()
# 输出 a 1, b 2, 3 (最后一次 x_0 为空)- 按最长列表次数循环,短列表空缺用空字符串补齐。
使用 Include 进行组织
语法
include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>])OPTIONAL:文件不存在时不报错,避免配置中断。RESULT_VARIABLE <var>:判断文件是否加载成功,加载成功后将文件路径存入,失败则为NOTFOUND。
作用:将独立的 .cmake 文件内容展开到 include 所需的地方执行,相当于在当前位置执行。
使用场景:通常.cmake 文件内存放着自定义函数、宏、变量定义(放在根目录下的 cmake/ 文件夹中)
模块搜索顺序:若指定的是模块名(无路径、无扩展名),则依次搜索 CMAKE_MODULE_PATH,在搜索 CMake 内置模块目录。若指定的是文件路径,则直接按路径查找。
使用对比:只是引入工具函数用 include,需要构建子项目用 add_subdirectory
简单示例
# 项目结构
project/
├── CMakeLists.txt
└── cmake/
└── helpers.cmake
# cmake/helpers.cmake
function(print_info target)
message(STATUS "Building: ${target}")
endfunction()
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(Demo)
# 设置模块路径,然后像引入模块一样加载
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(helpers) # 加载 helpers.cmake
print_info(my_app) # 直接使用宏
add_executable(my_app main.cpp)四、配置和缓存变量
使用选项 (Options)
提供可有用户选择的布尔开关(ON/OFF),通常用于控制构建行为的可配置项。
#语法结构
option(<variable> "<help_text>" [value])
#示例
option(ENABLE_TESTS "Build unit tests" ON)
option(USE_OPENGL "Use OpenGL rendering" OFF) # 默认 OFF
# 命令覆盖形式
cmake -DENABLE_TESTS=ON ..
cmake -DUSE_OPENGL=OFF ..<variable>:变量名,GUI 中显示该名称。<help_text>:帮助信息,给GUI用户看的提示文字。[value]:可选,默认值为 OFF。如不指定则默认关闭。该变量是缓存变量,用户修改后会持久化,下次构建保持上次设置的值。- 如果变量已经存在(普通变量或者缓存变量),命令都不执行任何操作。
CMake 变量与语言标准
在CMake中变量类型分为两种类型:普通变量 和 缓存变量
- 普通变量 通常是在
CMakeLists.txt用set()定义的。 作用域局限。 - 缓存变量 通常存在于
CMakeCache.txt,通过-D传入,也可以使用set(... CACHE ...)创建,在同一个构建目录中跨多次配置保持。 cmake -L/cmake -LA或直接看build/CMakeCache.txt- CMake 变量前缀
CMAKE_ - 项目自定义变量推荐前缀
项目名_(如TUTORIAL_BUILD_UTILITIES)
全局语言标准变量
CMAKE_CXX_STANDARD:控制 C++ 标准(如 17、20、23)。不同标准可能导致与某些库的兼容性问题,但并不意味着一定会改变 ABI。CMAKE_C_STANDARD:控制 C 标准。- 打包时 应该在配置阶段统一指定语言标准,例如:
cmake -DCMAKE_CXX_STANDARD=20。 - 库项目 一般不要强行
set(CMAKE_CXX_STANDARD 20),应尊重用户或上层项目的配置; - 如果使用
set(CMAKE_CXX_STANDARD ...),会影响之后创建目标的默认语言标准,并可能覆盖用户在配置时指定的值。如果与依赖库要求的标准不一致,可能导致编译或兼容性问题。
用 target 属性设置目标CPP标准
set_target_properties(mylib PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)- 仅影响该目标,不污染全局,允许用户通过
CMAKE_CXX_STANDARD整体提升。 CMAKE_CXX_STANDARD_REQUIRED: 是否强制该标准(ON 时若编译器不支持则报错)CMAKE_CXX_EXTENSIONS: 是否使用编译器特有扩展(如 GNU 扩展)
CMake Presets
- CMake Presets:官方提供的配置管理机制,用于保存 CMake 配置,避免每次手动输入大量
-D参数。 - 作用:统一管理项目配置,提高本地开发和 CI 的一致性。
配置文件:
CMakePresets.json:项目级配置,应加入版本控制(Git)。CMakeUserPresets.json:用户本地配置,不应加入版本控制。
configurePresets:定义一个或多个配置预设,每个预设都有唯一的name。cacheVariables:设置 CMake Cache 变量,等价于命令行的-D参数。binaryDir:指定构建目录,等价于命令行-B。${sourceDir}:内置宏,表示项目根目录(顶层CMakeLists.txt所在目录)。使用方式:
cmake --preset <preset-name>- 优先级:命令行参数 高于 Preset 中的配置。
- 扩展能力:Presets 不仅支持 Configure,还支持 Build、Test、Install、Package 等整个 CMake 工作流.
示例
# CMakePresets.json
{
"version": 4,
"configurePresets": [
{
"name": "tutorial",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "20"
}
}
]
}五、CMake 目标命令
Target Command 是用于修改目标(Target)属性的命令。
这些属性分为两类:
- 构建属性:源码、编译选项、输出名称等。
- 使用属性:头文件、链接库、编译要求等。
特性和定义
限制 CMake 编写过程中,不推荐全局设置语言标准(set(CMAKE_CXX_STANDARD 20)),因为会覆盖使用者的语言标准选择,降低项目灵活性,不同目标可能需要不同的 C++ 标准。
target_compile_features
目标_编译_特性:向目标(Target)申明所需的 C++ 特性或最低语言标准。
target_compile_features(MyApp PRIVATE cxx_std_20)
- 这里表示 MyApp 至少需要C++ 20 标准。
- cxx_std_标准年份:11、14、17、20、23
target_compile_features的 作用域分为:PRIVATE、PUBLIC、INTERFACE
target_compile_definitions
用于为指定目标(Target)添加编译宏(预处理器宏)。这些宏会在编译时传递给编译器,相当于自动添加-D参数,可用于控制源码中的条件编译。在现代 CMake 中 通常 配合option()和CMakePresets.json和target_compile_definitions()在一起使用。
基本语法
target_compile_definitions(
<target>
<PRIVATE|PUBLIC|INTERFACE>
<宏定义...>
)
# 例如
target_compile_definitions(MyLib PRIVATE ENABLE_LOG)
# MyLib 和所有链接 MyLib 的目标都能看到 ENABLE_LOG作用域:
PRIVATE:当前目标使用。PUBLIC:当前目标和依赖它的目标。INTERFACE:仅依赖当前的目标的目标使用,自己不用
举例
// 条件编译代码
void run()
{
#ifdef ENABLE_LOG
std::cout << "[LOG] Application started\n";
#endif
std::cout << "Hello CMake\n";
}
// CMakeLists.txt
option(ENABLE_LOG "Enable log output" OFF)
// 根据开关决定是否添加编译宏
if(ENABLE_LOG)
target_compile_definitions(MyApp PRIVATE ENABLE_LOG )
endif()
// CMakePresets.json
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_LOG": "ON"
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_LOG": "OFF"
}
}
]
}
// cmake 构建命令
// Debug 构建
cmake --preset debug
cmake --build build/debug
// Release 构建
cmake --preset release
cmake --build build/release编译和链接选项
有事,我们需要对传递给编译和链接行精确选项进行特定控制。这些情况由 target_compile_options() 和 target_link_options() 处理。target_link_options() 在实际开发中不算特别常用,就详细介绍了。
target_compile_options()
target_compile_options() 用于向指定 target 添加编译器选项。基本语法
target_compile_options(<target> [BEFORE]
{INTERFACE|PUBLIC|PRIVATE} <item>...
[{INTERFACE|PUBLIC|PRIVATE} <item>...]...)<target>:必须由add_executable()或add_library()创建,不能是别名目标。BEFORE:将本次调用添加的选项,放到该目标已有编译选项列表的最前面。[ -O0, -O2, -Wall, -DDEBUG ]比如 -O0 就是 用 BEFORE 放到已有列表最前面的。原本应该在 DDEBUG 后面。- 作用域:
PRIVATE,PUBLIC,INTERFACE - 重复调用会按顺序追加选项。
常见编译选项
# 常用编译选项及功能注释
-Wall # 开启绝大多数常见的编译警告
-Wextra # 在 -Wall 基础上启用额外的警告
-Werror # 将所有警告视为错误,强制代码零警告通过
-O2 # 常规发布优化级别,平衡性能与编译时间
-O0 # 关闭所有优化,保留完整的调试信息
-g # 生成默认调试信息(等价于 -g2),支持源代码级调试
-std=c++20 # 指定采用 C++20 语言标准
-fsanitize=address # 启用 AddressSanitizer,运行时检测内存越界、use-after-free 等错误
-march=native # 生成针对当前机器 CPU 的优化指令(可能导致可移植性下降)
-fPIC # 生成位置无关代码,编译共享库时必备
-fsanitize=undefined # 检测 未定义行为,整数溢出,非法转换包含和链接目录
太懒了,不知道怎么写了,顺便写写吧。
假设一个第三库提供这样的资源。
Vendor/ <-- 第三方库根目录
├── include/ <-- 头文件目录
│ └── Vendor.h <-- 库的接口头文件
└── lib/ <-- 库文件目录
├── libVendor.a <-- Linux 静态库
├── Vendor.lib <-- Windows 静态库(或导入库)
└── Vendor.dll <-- 可能的 Windows 动态库然后在Cmake 里面 用这三条命令导入 这个第三方库
# 1. 头文件:指向 include 目录
target_include_directories(VendorLib INTERFACE include)
# 2. 库搜索路径:指向 lib 目录
target_link_directories(VendorLib INTERFACE lib)
# 3. 具体库名:去掉前缀 lib 和后缀 .a/.lib 的“基本名”
target_link_libraries(VendorLib INTERFACE Vendor)但是,这并不是唯一的形式。有时候第三方库的目录结构可能是:
Library/
├── headers/ <-- 可能不叫 include
├── x64/release/ <-- 不同平台/配置的库分开存放
└── ...那么你只需要让三个命令的路径指向对应位置即可。核心是:
- 道头文件在哪个文件夹 → 用
target_include_directories - 知道库文件在哪个文件夹 → 用
target_link_directories - 知道库文件叫什么名字(通常是去掉前后缀的核心名)→ 用
target_link_libraries
六、CMake 库概念
静态库与共享库
对于大多数通用库,推荐在 add_library() 中省略库类型参数,即不显式指定 STATIC 或 SHARED。这样,实际生成的库类型将由 BUILD_SHARED_LIBS 变量统一控制:当该变量为真时生成共享库,否则生成静态库。CMake 默认不定义此变量,因此不作任何设置时,库将默认以静态方式构建。这种设计让包管理器或下游用户在构建时只需通过一个开关即可灵活选择静态或共享版本,无需修改项目源码,极大提升了库的可移植性与分发灵活性。
BUILD_SHARED_LIBS
BUILD_SHARED_LIBS是一个全局布尔变量,用于控制add_library()在未明确指定库类型时的默认行为。
- 当
BUILD_SHARED_LIBS的值为真(ON、TRUE、1),则add_library(<name> ...)默认生成动态库(SHARED)。 - 当
BUILD_SHARED_LIBS的值为假(OFF、FALSE、0、未定义),则默认生成 静态库(STATIC)。 - CMake 默认不定义该变量(BUILD_SHARED_LIBS),因此在没有任何设置时,库默认为静态构建。
# 等价于 if(BUILD_SHARED_LIBS)
add_library(example ${sources})
# 等价于 add_library(example ${sources})
if(BUILD_SHARED_LIBS)
add_library(example SHARED ${sources})
else()
add_library(example STATIC ${sources})
endif()
# 项目通常会在顶层 CMakeLists.txt 中通过 option() 命令为该变量创建一个缓存选项,方便用户控制:
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)注意点:如果项目通过 FetchContent 或 add_subdirectory() 直接引入外部依赖,而该依赖内部也包含 option(BUILD_SHARED_LIBS ...),则必须确保顶层项目在引入依赖之前已调用 option(BUILD_SHARED_LIBS ...)
接口库
接口库是指仅为其他目标传达使用要求,自身不构建或产生任何制品的库。因此,接口库的所有属性本身必须是接口属性,需使用 INTERFACE 作用域关键字来指定。
add_library(MyInterface INTERFACE)
target_compile_definitions(MyInterface INTERFACE MYINTERFACE_COMPILE_DEF)C++ 开发中最常见的接口库是仅头文件库。此类库不构建任何东西,仅提供发现其头文件所需的标志。
举例
cmake_minimum_required(VERSION 3.12)
project(ObjectLibraryExample)
# 定义两个对象库,表示内部子模块(例如来自不同团队的代码)
add_library(math_utils OBJECT math_utils.cpp math_utils.h)
add_library(string_utils OBJECT string_utils.cpp string_utils.h)
# 为对象库设置包含路径(如果需要),通常用 PRIVATE 即可,因为对象库不应传递链接对象
target_include_directories(math_utils PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(string_utils PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 也可以给对象库设置编译定义或选项
target_compile_definitions(math_utils PRIVATE EXTRA_PRECISION)
# 创建一个最终的静态库,将两个对象库的目标文件合并到一起
add_library(my_combined_lib STATIC)
target_link_libraries(my_combined_lib
PRIVATE math_utils
PRIVATE string_utils
)
# 如果需要,最终库仍然可以对外暴露公共头文件路径
target_include_directories(my_combined_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 创建可执行文件,链接合并后的静态库
add_executable(consumer main.cpp)
target_link_libraries(consumer PRIVATE my_combined_lib)对象库
对象库通过 add_library(<name> OBJECT) 创建,它只会将源文件编译为目标文件(.o / .obj),而不会将其链接成静态库或动态库。因此,对象库本身不能直接用于链接,也不能作为独立的库产物分发。
关键限制
链接不可传递
- 对象库中的目标文件无法通过依赖链传递给其他目标。如果将一个对象库放在某个目标的
INTERFACE_LINK_LIBRARIES属性中,依赖该目标的下游目标将“看不到”这些对象文件——此时对象库的行为类似于 INTERFACE 库(只传递使用要求,不传递链接对象)。 - 这意味着对象库通常只能通过
target_link_libraries()的 PRIVATE 或 PUBLIC 方式使用,而不应期望它通过 INTERFACE 传递链接实体。
典型用例:合并多个库目标
对象库最常见的用途是将多个独立维护的库目标合并为一个最终的静态库或动态库。在大型项目中,不同的团队可能分别开发并维护各自的库目标(出于组织、职责分离等原因)。但对外发布时,项目可能希望将这些内部子库整合成单一的面向用户的二进制文件(例如单个 .a 或 .so)。对象库正好提供了这种“仅编译、不链接”的中间形态:
- 每个子库定义为对象库,独立编译源文件。
- 最后创建一个最终的静态库或共享库目标,通过
target_link_libraries()将这些对象库以 PRIVATE 方式链接进去,将所有目标文件合并到一个产物中。
这样既保留了模块化的内部结构,又为最终消费者提供了简洁统一的链接目标。
举例
cmake_minimum_required(VERSION 3.14)
project(InterfaceLibraryExample)
# 定义一个接口库,用于提供通用编译选项和头文件路径
add_library(project_options INTERFACE)
# 为目标追加现代 C++ 标准和警告选项(这些会传递给使用该库的目标)
target_compile_features(project_options INTERFACE cxx_std_17)
target_compile_options(project_options INTERFACE -Wall -Wextra)
# 如果还有其他通用的包含路径,也可以通过接口库统一管理
target_include_directories(project_options INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 创建一个可执行目标,并链接接口库以获取编译选项和头文件路径
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE project_options) 