Skip to content

Commit 1b7c923

Browse files
authored
Merge pull request #15 from kaysonyu/main
doc(cmake): add intro-cmake documentation
2 parents 1bed23f + dfefdda commit 1b7c923

7 files changed

Lines changed: 268 additions & 0 deletions

File tree

docs/.vitepress/config/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function sidebarGuide(): DefaultTheme.Sidebar {
7878
collapsed: false,
7979
items: [
8080
{ text: "Make 入门", link: "/tools/intro-make" },
81+
{ text: "CMake 入门", link: "/tools/intro-cmake" },
8182
],
8283
},
8384
{

docs/tools/intro-cmake.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# CMake 入门
2+
> [!note]
3+
> 学习本文内容,需要你对以下技能有基本的了解:
4+
> * Make 入门(请参考[这篇文档](./intro-make.md)
5+
>
6+
> 更重要的是,用过 Make 后,你更能体会到 CMake 带来的便利。
7+
8+
随着项目规模扩大,代码文件数量增多,单靠手写 Makefile 变得繁琐且难以维护。而 CMake 是一个跨平台的构建工具,可以生成不同平台、不同工具链所需的 Makefile 或工程文件,是目前 C/C++ 项目的主流构建配置方式。与 Make 类似,利用 CMake 时,我们需要编写的配置文件为 `CMakeLists.txt`
9+
10+
学习 CMake 能帮助你:
11+
- 简化多文件项目的配置
12+
- 跨平台构建(Linux、Windows、Mac)
13+
- 管理大型项目和第三方库
14+
15+
## 安装 CMake
16+
如果你使用 GNU/Linux(包括 WSL),一般可以直接安装 CMake。比如在 Ubuntu 中,如下:
17+
18+
```bash
19+
sudo apt install cmake
20+
```
21+
22+
如果你使用 MacOS,可以使用 brew 安装。
23+
24+
```bash
25+
brew install cmake
26+
```
27+
28+
如果你使用 Windows,可以在[官网](https://cmake.org/download/)下载二进制包或者使用 Chocolatey 等包管理器下载。
29+
30+
下面打开终端,键入:
31+
32+
```bash
33+
$ cmake --version
34+
cmake version 4.0.1
35+
36+
CMake suite maintained and supported by Kitware (kitware.com/cmake).
37+
```
38+
39+
看到上面这样的文字证明你的安装是正确的。
40+
41+
## 一个简单的示例
42+
43+
假设我们有一个 C 语言项目,目录结构如下:
44+
45+
```
46+
project/
47+
├── main.c
48+
├── hello.c
49+
├── hello.h
50+
└── CMakeLists.txt
51+
```
52+
53+
我们希望生成一个可执行文件 main。`CMakeLists.txt` 文件内容如下:
54+
55+
```cmake
56+
cmake_minimum_required(VERSION 3.15)
57+
project(HelloProject LANGUAGES C)
58+
# 编译产物为可执行文件,并指定需要编译的源文件
59+
add_executable(main main.c hello.c)
60+
```
61+
62+
然后在项目根目录运行:
63+
64+
```bash
65+
cmake -B build
66+
cmake --build build
67+
```
68+
69+
执行完成后,你会在 `build/` 目录里看到生成的可执行文件。
70+
71+
在这个示例中,对比直接编写 `Makefile`,使用 `CMakeLists.txt` 更加简洁。
72+
73+
## CMake 的构建流程
74+
75+
CMake 是“配置 + 构建”两步走的:
76+
77+
![](intro-cmake/img-intro-cmake-1.png)
78+
79+
```bash
80+
# 第一步:配置,生成构建系统(例如 Makefile)
81+
cmake -B build
82+
83+
# 第二步:构建
84+
cmake --build build -j4
85+
86+
# (可选)安装
87+
sudo cmake --build build --target install
88+
89+
# 清理
90+
sudo cmake --build build --target clean
91+
```
92+
93+
### 配置阶段的参数
94+
95+
`-B build` 表示把中间文件、构建文件放在 `build/` 目录。
96+
97+
`-G` 指定生成器,`CMake` 可以生成不同类型的构建系统(比如 `Makefile` `MSBuild`,所以可以跨平台)
98+
99+
```bash
100+
cmake -B build -G "Unix Makefiles"
101+
```
102+
103+
![](intro-cmake/img-intro-cmake-2.png)
104+
105+
安装路径、构建类型等选项在配置阶段通过 `-D` 参数指定。
106+
107+
```bash
108+
# 设置安装路径
109+
cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/myapp
110+
# 设置构建模式为发布模式
111+
cmake -B build -DCMAKE_BUILD_TYPE=Release
112+
```
113+
114+
## CMakeLists.txt 基本语法
115+
116+
CMake 通过一个名为 `CMakeLists.txt` 的文件定义构建规则。常见语法如下:
117+
118+
### 定义项目
119+
120+
```cmake
121+
# 规定 CMake 最低版本要求
122+
cmake_minimum_required(VERSION 3.15)
123+
124+
# 定义项目
125+
project(prj LANGUAGES C CXX)
126+
```
127+
128+
### 生成可执行程序
129+
130+
目标产物为可执行文件时,使用 `add_executable`
131+
132+
```cmake
133+
add_executable(main main.cpp hello.cpp)
134+
135+
# 先指定可执行程序,后添加
136+
add_executable(main)
137+
target_sources(main PUBLIC main.cpp hello.cpp)
138+
139+
# 使用 GLOB 根据扩展名批量查找,替换成 GLOB_RECURSE 则会递归所有子文件夹中进行匹配,CONFIGURE_DEPENDS 保证增减文件后自动更新变量
140+
add_executable(main)
141+
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
142+
target_sources(main PUBLIC ${sources})
143+
```
144+
145+
### 生成库
146+
147+
目标产物为库文件时,使用 `add_library`
148+
149+
```cmake
150+
# 静态库
151+
add_library(mylib STATIC mylib.cpp)
152+
153+
# 动态库
154+
add_library(mylib SHARED mylib.cpp)
155+
156+
# OBJ库
157+
add_library(mylib OBJECT mylib.cpp)
158+
```
159+
160+
### 目标属性
161+
162+
CMake 提供了很多方便的命令管理 target 的属性,这里的 target 就是前文提到的目标产物。比如:
163+
164+
```cmake
165+
target_sources(myapp PUBLIC hello.cpp other.cpp) # 添加源文件
166+
target_include_directories(myapp PUBLIC include) # 添加头文件搜索目录
167+
target_link_libraries(myapp PUBLIC hellolib) # 添加链接库
168+
target_compile_definitions(myapp PUBLIC MY_MACRO=1) # 添加宏定义 MY_MACRO=1
169+
target_compile_options(myapp PUBLIC -fopenmp) # 添加编译选项
170+
target_compile_features(mylib PUBLIC cxx_std_17) # 为指定目标启用编译器特性
171+
```
172+
173+
这些命令只对指定的 target 生效,而不会影响全局。
174+
175+
相对地,以下命令则会改变全局配置,在 CMake 中要谨慎使用。
176+
177+
```cmake
178+
# 避免使用
179+
include_directories(include) # 添加头文件搜索目录
180+
link_directories(/opt/cuda) # 添加链接库搜索目录
181+
add_definitions(MY_MACRO=1) # 添加宏定义 MY_MACRO=1
182+
add_compile_options(-fopenmp) # 添加编译选项
183+
```
184+
185+
## 第三方库引入方法
186+
187+
### 作为纯头文件引入 `target_include_directories`
188+
189+
适用于那些只有头文件的库,例如一些轻量级的模板库。这些库不需要编译,因为它们的实现代码都在头文件中,通常是通过模板或者宏等方式实现功能。比如C++的标准模板库就是纯头文件。
190+
191+
`fmt` 库为例,该库介绍说明可以通过纯头文件引入,此时只需要项目中的 `include` 文件夹。
192+
193+
![](intro-cmake/img-intro-cmake-3.png)
194+
195+
`CMakeLists.txt` 中通过 `target_include_directories` 引入第三方库头文件目录。
196+
197+
```CMake
198+
cmake_minimum_required(VERSION 3.15)
199+
project(prj)
200+
201+
add_executable(prj main.cpp)
202+
target_include_directories(prj PUBLIC include)
203+
target_compile_features(prj PUBLIC cxx_std_17)
204+
```
205+
206+
但是直接引入头文件,函数实现在头文件里,没有提前编译,每次需要重复编译同样的内容,编译时间长。
207+
208+
### 作为子模块引入 `add_subdirectory`
209+
210+
这种方式将第三方库的源代码直接包含到项目中,第三方库通常有自己的 `CMakeLists.txt` 文件,通过 `add_subdirectory` 指令,可以将这个库的构建过程集成到主项目的构建过程中。
211+
212+
`fmt` 库为例,这个开源库可以直接将该项目作为用户项目的子项目引入。
213+
214+
![](intro-cmake/img-intro-cmake-4.png)
215+
216+
`CMakeLists.txt` 中通过 `add_subdirectory` 引入第三方库的项目子目录,再通过 `target_link_libraries` 链接第三方项目库。
217+
218+
```CMake
219+
cmake_minimum_required(VERSION 3.15)
220+
project(prj)
221+
222+
add_subdirectory(fmt)
223+
224+
add_executable(prj main.cpp)
225+
target_link_libraries(prj fmt::fmt)
226+
target_compile_features(prj PUBLIC cxx_std_17)
227+
```
228+
229+
### 引用系统中安装的第三方库 `find_package`
230+
231+
在存在菱形依赖的情况下,即项目A依赖于B和C,而B和C又同时依赖于D,使用子模块引用(`add_subdirectory`),可能会导致D被定义两遍,从而引发错误。
232+
233+
而通过 `find_package` 使用系统预安装的库则可以有效避免这个问题。当使用 `find_package` 查找库时,CMake会记录已经找到的库。因此,即使多个模块依赖同一个库, `find_package` 也只会引入一次。
234+
235+
不同操作系统可以通过各自的包管理器来安装所需的库。以Ubuntu为例,可以使用 `apt` 包管理器来安装库。比如安装 `fmt` 库:
236+
237+
```sh
238+
sudo apt install libfmt-dev
239+
```
240+
241+
此时由于头文件等已经在系统查找路径中(比如`/usr/include`),可以直接在文件中导入相关的头文件,此时,项目结构如下:
242+
243+
![](intro-cmake/img-intro-cmake-5.png)
244+
245+
`CMakeLists.txt` 中则需要先 `find_package` 找到 `fmt` 包,再通过 `target_link_libraries` 链接第三方项目库。
246+
247+
```CMake
248+
cmake_minimum_required(VERSION 3.15)
249+
project(prj)
250+
251+
find_package(fmt REQUIRED)
252+
253+
add_executable(prj main.cpp)
254+
target_link_libraries(prj fmt::fmt)
255+
target_compile_features(prj PUBLIC cxx_std_17)
256+
```
257+
258+
在CMake中,一个项目可以包含多个库。CMake允许一个包(package)提供多个库,这些库也被称为组件(components)。因此,在使用 `target_link_libraries` 指令链接库时,应采用 `包名::组件名` 的格式。
259+
260+
例如,在上文中提到的 `fmt::fmt`,其中 `fmt` 是包名,第二个 `fmt` 是该包提供的一个组件名。
261+
262+
## 结语
263+
264+
CMake 作为当今最流行的跨平台构建工具之一,功能强大、生态完善,是绝大多数 C++ 项目的首选。
265+
266+
然而,CMake 的语法和配置方式对于初学者来说略显繁琐。近年来也有一些更现代化、上手更简单的构建工具涌现,比如 [xmake](https://xmake.io/),它用更直观的语法封装了很多常用场景,这里限于篇幅就不再介绍了。
267+
30.5 KB
Loading
236 KB
Loading
157 KB
Loading
170 KB
Loading
117 KB
Loading

0 commit comments

Comments
 (0)