CMake入门教程
简介
CMake 是一个元构建系统(Meta-Build System)。作为一个适配器为使用不同平台环境和工具链的开发者提供了一套统一的构建规范,并且为C++项目的依赖管理和模块化带来了极大的便利。为了更熟练的构建C++工程,掌握CMake的基本用法是必须的,本文章主要介绍在Windows平台下使用Visual Studio工具链进行项目构建的方法。
元构建系统(Meta-Build System)
元构建系统是一种用于自动化生成构建文件(用于Makefile,ninja等build-system)的工具。
元构建系统(Meta-Build System)和构建系统(Build System)以及构建文件之间的关系:
- 元构建系统:生成其他构建系统的工具。本身不执行构建工作,而是从更高层次的抽象描述构建依赖关系,并转换为make等更底层的构建系统。元构建系统会屏蔽掉平台相关依赖,具有很好的跨平台特性。
- 构建系统:从源代码生成用户可以使用的目标文件的自动化工具。目标可以是库文件、可执行文件或者脚本等。构建系统通过构建文件定义编译规则,并调用工具链来编译代码。Make就是一个常见的构建系统。
- 构建文件:用于定义构建系统编译规则的脚本,包含如何编译和链接程序的规则以及文件之间的依赖关系。
本教程假定你已经对C++的基础用法以及编译模型有一定了解,并非为C++初学者准备。
开发环境
教程中使用的开发环境。
- Win11
- Visual Studio 2022
- CMake 3.27.6
- Ninja
- Clang-cl/llvm
- C++ standard 17
基础用法
1. 构建可执行文件
先从VS2022创建一个空的CMake项目,简单修改目录格式如下:
1 | │ CMakeLists.txt |
CMakeLists.txt和CMakePresets.json均为CMake项目模板自动生成,暂时先关注CMakeLists.txt中的内容,CMakePresets.json用于配置更顶层的环境参数,我们放到后面解释。
现在我们开始编写简单的工程代码,假设此时我们想要设计一个数据结构,它的接口如下:dsu.h:
1 |
|
它的实现在dsu.cpp中:dsu.cpp:
1 |
|
我们在main.cpp中使用这个工具:main.cpp:
1 |
|
目前的文件结构如下:
1 | │ CMakeLists.txt |
如何让main.cpp和dsu.cpp找到dsu.h并且生成一个可执行程序?对应的CMake代码如下:
1 | cmake_minimum_required(VERSION 3.27.6) # 指定CMake版本下限 |
接下来详细解释涉及到的命令:
cmake_minimum_required():用于指定CMake版本下限,是必须被调用的指令(cmake_minimum_required()必须在project()之前被调用)。project():用于定义工程属性,是必须被调用的指令。完整格式为:1
2
3
4
5project(<工程名称>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <描述>]
[HOMEPAGE_URL <主页URL>]
[LANGUAGES <支持语言>...])set():用于定义或修改CMake变量(包括用户创建变量和内置变量),完整格式为:1
set(<variable> <value>... [PARENT_SCOPE] [CACHE <type> <docstring> [FORCE]])
<variable>:要设置的变量名。<value>:变量的值,可以是单个值或多个值(列表)。PARENT_SCOPE:将变量值设置到父作用域(通常是调用add_subdirectory()的上层CMakeLists.txt)。CACHE <type> <docstring> [FORCE]:将变量存入 CMake 缓存,使其可在ccmake或cmake-gui中修改。
include_directories():用于指定头文件搜索路径的命令,这一命令会影响后续构建的所有目标(针对单个目标的版本为target_include_directories())。它会告诉编译器在哪些目录中查找#include指令引用的头文件。完整格式为:1
include_directories([AFTER|BEFORE] [SYSTEM] <目录>...)
- <目录>:头文件所在的路径,可以是绝对路径或相对路径。
- AFTER(默认):把路径添加到系统默认搜索路径之后。
- BEFORE:把路径添加到系统默认搜索路径之前,优先查找。
- SYSTEM:标记目录为系统头文件目录,这样编译时不会针对这些目录的头文件生成警告。
file(GLOB) / file(GLOB_RECURSE):file(GLOB)用于获取匹配特定模式的文件列表并写入到指定变量,file(GLOB_RECURSE)则是的递归版本(file()函数还有很多用法,放在后面详细介绍)。完整格式为:1
file(GLOB/GLOB_RECURSE <变量> <匹配模式>)
add_executable():用于创建可执行文件的命令,完整格式为:1
add_executable(<目标名> <源文件1> <源文件2> ... <源文件N>)
2. 构建库文件
在工程中也时常会有制作库文件将项目模块化的需求,接下来介绍静态库和动态库的制作和使用方法。在本文的例子中,假设我们需要将dsu这个工具制作为库文件。
首先我们对项目结构稍加修改:
1 | │ CMakeLists.txt |
我们需要将source/dsu目录下的源文件编译到库文件,将source/test目录下的源文件编译为可执行文件。
对于静态库,具体的CMake代码如下:
1 | cmake_minimum_required(VERSION 3.27.6) |
对于动态库,具体的CMake代码如下:
1 | cmake_minimum_required(VERSION 3.27.6) |
由于Windows平台的特性,在本文的例子中,制作动态库时还需要将
dsu的对应接口标记为__declspec(dllexport)/__declspec(dllimport)方能正常链接。
接下来详细解释涉及到的命令:
add_library():用于创建静态库或动态库,完整格式为:1
add_library(<target> [STATIC | SHARED | MODULE | OBJECT] [EXCLUDE_FROM_ALL] <source1> [<source2> ...])
<target>:库的名称,后续可以用target_link_libraries()进行链接。[STATIC | SHARED | MODULE | OBJECT]:STATIC(默认):生成.lib(Windows)文件(静态库)。SHARED:生成.dll(Windows)文件(动态库)。MODULE:生成插件库,类似SHARED,但不会被默认链接。OBJECT:生成对象文件.obj,但不生成最终的.lib或.dll。
[EXCLUDE_FROM_ALL]:- 可选,如果添加该选项,则不会默认构建这个库,除非有其他目标显式依赖它。
<source1> [<source2> ...]:- 需要编译到库中的源文件。
target_link_libraries():用于为一个目标(CMake中的target指的是可执行文件或库文件)指定它依赖的库。这个指令会影响链接阶段,确保目标能够正确地找到并使用指定的库。其完整格式如下:1
target_link_libraries(<target> [<mode>] <library> [<library>...])
<target>:要链接库的目标(可执行文件或库)。<mode>(可选):指定链接的可见性,可选值为PRIVATE、PUBLIC和INTERFACE。<library>:要链接的库,可以是已有的 CMake 目标、外部库文件、系统库等。
其中可见性会影响模块的依赖传递和封装性,具体情况放在后面进行详细讨论。
3.多目标构建
一个大型项目中会有很多库文件和可执行文件,这些目标通常作为独立模块在项目中复用,并且分散在不同的源码目录中。如果继续在同一个CMakeLists.txt中处理所有目标的构建,则无法满足项目的模块化需求,CMake提供了嵌套CMake的方法来方便的处理这种情况。
对项目结构进行修改:
1 | │ CMakeLists.txt |
- 项目需求:利用
dsu模块实现algorithm模块中的两个算法,并完成test模块中的两个testbench。 - 构建需求:将
dsu模块构建为动态库,algorithm模块构建为静态库,test模块每个源文件单独构建为可执行程序。
根目录下的CMakeLists.txt中的代码非常简单:其中,1
2
3
4
5
6
7
8
9
10
11
12
13cmake_minimum_required(VERSION 3.27.6)
project(Utils)
set(CMAKE_CXX_STANDARD 17)
# 注意此处使用的是include_directories & link_directories,因为本文例子中所有目标使用的头文件路径和库链接路径是相同的。如果不同目标使用的路径不同,则需要使用target_include_directories & link_directories
include_directories(${CMAKE_SOURCE_DIR}/include)
link_directories(${CMAKE_SOURCE_DIR}/bin)
# 添加CMake子目录
add_subdirectory(source/dsu)
add_subdirectory(source/algorithm)
add_subdirectory(source/test)add_subdirectory()命令主要用于在 CMake 构建系统中添加子目录,并处理该目录下的CMakeLists.txt文件。是 CMake 工程组织多个模块,实现代码复用的关键命令。
其完整格式为:1
add_subdirectory(<source_dir> [binary_dir] [EXCLUDE_FROM_ALL])
<source_dir>:必填,指定子目录的源代码路径,该目录必须包含CMakeLists.txt。[binary_dir]:可选,指定子目录的构建目录(默认使用CMake指定的build目录)。[EXCLUDE_FROM_ALL]:可选,如果指定,则该目录的构建不会包含在all目标中,只有手动构建时才会编译。
关于子目录的处理方式有一些特性:
- 递归解析 CMakeLists.txt:进入子目录后,会解析该目录的
CMakeLists.txt。 - 继承变量作用域:默认情况下,父
CMakeLists.txt中定义的变量对子目录可见(但子目录修改的变量不会影响父目录,除非使用PARENT_SCOPE)。 - 继承
include_directories()、link_directories()等:父目录设置的include_directories()也适用于子目录。
至于各个子目录中的 CMakeLists.txt 文件,读者可以先尝试自行编写,此处列出一种编写方式:
source/dsu1
2
3
4
5
6
7
8
9
10cmake_minimum_required(VERSION 3.27.6)
project(Dsu)
set(CMAKE_CXX_STANDARD 17)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
add_library(dsu SHARED ${SRC_LIST})
set_target_properties(dsu PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")source/algorithm1
2
3
4
5
6
7
8
9
10
11
12cmake_minimum_required(VERSION 3.27.6)
project(Algorithm)
set(CMAKE_CXX_STANDARD 17)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
add_library(algorithm STATIC ${SRC_LIST})
set_target_properties(algorithm PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")
target_link_libraries(algorithm PUBLIC dsu)source/test1
2
3
4
5
6
7
8
9
10
11
12cmake_minimum_required(VERSION 3.27.6)
project(Test)
set(CMAKE_CXX_STANDARD 17)
add_executable(test1 test1.cpp)
set_target_properties(test1 PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")
target_link_libraries(test1 PUBLIC dsu)
add_executable(test2 test2.cpp)
set_target_properties(test2 PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")
target_link_libraries(test2 PUBLIC algorithm)
上面的实现使用了
set_target_properties()命令,针对单一目标的属性(如库文件或可执行文件的输出路径)进行修改,而不是使用set()命令对这些属性进行全局范围的设置。对于实际的大型项目来说,使用前者更加合理;对于本文例子来说,两者并无差别。
4. 构建流程控制 TODO:
条件判断
CMake 提供了 if() 语句来执行条件分支。
1 | if (condition) |
例如:
1 | set(VALUE 10) |
输出:
1 | VALUE 大于 5 |
| 条件 | 作用 | 示例 |
|---|---|---|
| 数值比较 | 进行数值大小判断 | if (VALUE GREATER 5) |
| 字符串比较 | 判断字符串是否相等 | if ("Hello" STREQUAL "World") |
| 变量是否定义 | 检查变量是否存在 | if (DEFINED MY_VAR) |
| 布尔值判断 | 检查变量的真假 | if (MY_FLAG) |
| 文件/目录存在性 | 判断文件或目录是否存在 | if (EXISTS "path/to/file") |
| 环境变量 | 检查环境变量是否存在 | if (ENV{HOME}) |
| 逻辑运算 | 结合多个条件 | if (A AND B), if (C OR D) |
循环
foreach()
用于遍历列表或范围。
1 | foreach(variable RANGE start stop [step]) |
1 | foreach(variable IN LISTS list_name) |
1 | foreach(variable item1 item2 item3 ...) |
例如:
1 | set(NAMES Alice Bob Charlie) |
输出:
1 | Hello, Alice! |
例如:
1 | foreach(I RANGE 1 5) |
输出:
1 | Value: 1 |
例如:
1 | foreach(I RANGE 1 10 2) |
输出:
1 | Step: 1 |
while()
用于执行条件满足时的循环,适用于动态控制流。1
2
3while (condition)
# 代码块
endwhile()例如:
1
2
3
4
5
6set(COUNT 0)
while (COUNT LESS 5)
message("COUNT = ${COUNT}")
math(EXPR COUNT "${COUNT} + 1")
endwhile()输出:
1
2
3
4
5COUNT = 0
COUNT = 1
COUNT = 2
COUNT = 3
COUNT = 4例如:
1
2
3
4
5
6
7
8set(LIST A B C D E)
set(INDEX 0)
while (INDEX LESS 5)
list(GET LIST ${INDEX} ITEM)
message("Item ${INDEX}: ${ITEM}")
math(EXPR INDEX "${INDEX} + 1")
endwhile()输出:
1
2
3
4
5Item 0: A
Item 1: B
Item 2: C
Item 3: D
Item 4: Ebreak()和continue()
用于控制循环流程。break():立即终止循环。continue():跳过当前迭代,进入下一次循环。
应用
- 跨平台编译
1 | if (CMAKE_SYSTEM_NAME STREQUAL "Windows") |
- 根据用户选项启用功能
1 | option(ENABLE_TESTS "Enable testing" OFF) |
5. 外部模块管理 TODO:
提示
${}的使用 TODO:
| 场景 | 是否需要 ${} |
示例 |
|---|---|---|
| 变量赋值 | 否 | set(VAR "Hello") |
| 变量访问 | 是 | message(${VAR}) |
if() 判断 |
否 | if(VAR) |
| 作为命令参数 | 是 | project(${VAR}) |
| 访问环境变量 | 是 | message($ENV{HOME}) |
foreach() 遍历列表 |
否(遍历变量名) | foreach(ITEM IN LISTS VAR) |
unset() 删除变量 |
否 | unset(VAR) |
常用内置变量和宏 TODO:
1. 项目和构建相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_SOURCE_DIR |
项目源代码的根目录(顶层 CMakeLists.txt 所在目录)。 |
CMAKE_BINARY_DIR |
项目的构建目录(cmake 执行的目录)。 |
CMAKE_CURRENT_SOURCE_DIR |
当前处理的 CMakeLists.txt 文件所在的源目录。 |
CMAKE_CURRENT_BINARY_DIR |
当前处理的 CMakeLists.txt 对应的二进制目录。 |
PROJECT_NAME |
project() 命令定义的项目名称。 |
CMAKE_PROJECT_NAME |
根 CMakeLists.txt 定义的项目名称(最顶层项目)。 |
CMAKE_VERSION |
CMake 的版本号,例如 3.26.0。 |
2. CMake 目录和文件相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_MODULE_PATH |
指定 CMake 额外的模块搜索路径。 |
CMAKE_COMMAND |
CMake 执行命令的完整路径。 |
CMAKE_CURRENT_LIST_FILE |
当前正在处理的 CMake 文件路径。 |
CMAKE_CURRENT_LIST_DIR |
当前 CMake 文件所在的目录。 |
3. 编译器和编译相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_C_COMPILER |
C 语言编译器路径。 |
CMAKE_CXX_COMPILER |
C++ 编译器路径。 |
CMAKE_C_FLAGS |
C 语言编译时的默认标志。 |
CMAKE_CXX_FLAGS |
C++ 编译时的默认标志。 |
CMAKE_BUILD_TYPE |
指定构建类型(Debug、Release、RelWithDebInfo、MinSizeRel)。 |
CMAKE_C_STANDARD |
指定 C 语言标准(如 99、11)。 |
CMAKE_CXX_STANDARD |
指定 C++ 语言标准(如 11、14、17、20)。 |
4. 链接和库相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_LINKER |
链接器的路径。 |
CMAKE_EXE_LINKER_FLAGS |
生成可执行文件时的链接选项。 |
CMAKE_SHARED_LINKER_FLAGS |
生成动态库时的链接选项。 |
CMAKE_STATIC_LINKER_FLAGS |
生成静态库时的链接选项。 |
CMAKE_LIBRARY_OUTPUT_DIRECTORY |
目标库文件的输出目录。 |
CMAKE_ARCHIVE_OUTPUT_DIRECTORY |
静态库的输出目录。 |
5. 目标平台相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_SYSTEM_NAME |
目标操作系统名称(如 Windows、Linux、Darwin)。 |
CMAKE_SYSTEM_VERSION |
目标操作系统的版本。 |
CMAKE_HOST_SYSTEM_NAME |
CMake 运行的主机操作系统名称。 |
CMAKE_HOST_SYSTEM_VERSION |
CMake 运行的主机操作系统版本。 |
CMAKE_SIZEOF_VOID_P |
指针的字节大小(可用于判断 32 位或 64 位)。 |
6. 生成器相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_GENERATOR |
指定的 CMake 生成器(如 Ninja、Unix Makefiles)。 |
CMAKE_MAKE_PROGRAM |
make 命令路径(仅适用于 Makefile 生成器)。 |
7. 安装相关变量
| 变量名 | 作用 |
|---|---|
CMAKE_INSTALL_PREFIX |
install() 指定的默认安装路径。 |
CMAKE_INSTALL_BINDIR |
可执行文件的安装目录(默认 bin/)。 |
CMAKE_INSTALL_LIBDIR |
库文件的安装目录(默认 lib/)。 |
CMAKE_INSTALL_INCLUDEDIR |
头文件的安装目录(默认 include/)。 |
8. 测试和构建系统相关变量
| 变量名 | 作用 |
|---|---|
BUILD_SHARED_LIBS |
是否默认构建动态库(ON/OFF)。 |
CMAKE_VERBOSE_MAKEFILE |
是否打印详细的 Makefile 过程(ON/OFF)。 |
CMAKE_EXPORT_COMPILE_COMMANDS |
是否生成 compile_commands.json(用于 Clangd 等工具)。 |
file() 的常见操作 TODO:
| 操作 | 功能 |
|---|---|
GLOB |
查找符合特定模式的文件 |
GLOB_RECURSE |
递归查找符合模式的文件 |
WRITE |
创建并写入文件 |
APPEND |
追加内容到文件 |
READ |
读取文件内容 |
COPY |
复制文件或目录 |
REMOVE |
删除文件 |
REMOVE_RECURSE |
递归删除文件夹及其内容 |
RENAME |
重命名文件或目录 |
MAKE_DIRECTORY |
创建目录 |
SIZE |
获取文件大小 |
SHA256 |
计算文件 SHA256 哈希值 |
STRINGS |
从文件中提取文本 |
TIMESTAMP |
获取文件时间戳 |
CMake预设
CMakePresets.json文件定义了一系列的CMake预设信息,包括跨平台配置,编译器和生成器配置等。下面是是一个简单的CMakePresets.json样例以及对每个条目的解释:
1 | { |
- windows-base:这是一个基础预设,被标记为隐藏(”hidden”: true),意味着它只可以被其他预设继承。它设置了Ninja生成器,指定了二进制和安装目录,并设置了C和C++编译器为cl.exe。此预设仅在平台为Windows时应用。
- x64-debug 和 x64-release:这两个预设继承自windows-base,分别用于64位的Debug和Release构建。它们设置了体系结构为x64,并分别设置了CMake构建类型为Debug和Release。
- linux-debug:这个预设专门用于Linux平台的Debug构建。它设置了Ninja生成器,指定了二进制和安装目录,并设置了构建类型为Debug。此预设仅在平台为Linux时应用。
- macos-debug:这个预设专门用于macOS平台的Debug构建。配置方法与linux-debug类似。
- Title: CMake入门教程
- Author: Archer阿澈
- Created at : 2023-05-04 17:47:34
- Updated at : 2023-05-04 17:47:34
- Link: https://www.archer-du.top/2023/05/04/Tech/Fundamentals/CMake_Tutorial/
- License: This work is licensed under CC BY-NC-SA 4.0.