CMake是一个跨平台、开源的构建工具,在各种大型C/C++项目中被广泛使用。
CMake根据CMakeLists.txt中的变量,来确定当前的构建环境和构建规则。构建规则包括:要需要包含哪些头文件、链接哪些库、语言的版本、编译器参数等等。参考
CMake作为一项硬技能,要好好学一下。本页为我在学习CMake过程中的一些笔记。分为三个层级:
- 第一层:如何使用
- 第二层:每一步背后发生了什么
- 第三层:一些更细致全面的学习
第一层:如何使用
这一节首先用一个简单的样例,看看如何操作,来借助CMake完成编译。
假如我们现在有一个源文件demo.cpp
1
2
3
4
5
6
7
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl;
return 0;
}
编写CMakeLists.txt
1
2
3
4
5
6
7
8
# cmake最低版本需求
cmake_minimum_required(VERSION 3.13)
# 指定project名称
project(DemoProject)
# 编译源码生成目标
add_executable(demo demo.cpp)
文件目录如下:
1
2
|- demo.cpp
|- CMakeList.txt
依次执行
1
2
3
mkdir build
cd build
cmake ..
这一步直接使用
cmake .
当然也是可以的。但是生成的中间文件会和源文件混杂在一起,发生凌乱。因此一般都是新建一个build
文件夹,用来保存中间文件(即通常说的"out-of-source build"
)
再执行
1
make
即可完成编译,在build
目录下生成可执行文件demo
。
执行
1
2
3
4
# build目录下
./demo
# 输出: Hello, World!
第二层:每一步发生了什么
这一节看看上面的各个步骤分别发生了什么。
上面各步是一个典型的
"out-of-source build"
过程。具体而言,build
目录是我们执行cmake并保存生成文件的地方;..
是CMakeLists.txt所在的目录。
-
mkdir build; cd build
- 创建并进入
build
文件夹,编译过程生成的中间文件将保存在此,与源文件隔离开来。
- 创建并进入
-
cmake ..
这一句的原型为:
cmake <path-to-source>
。即到当前(/build
)上一层目录中寻找CMakeLists.txt
-
这一步会生成以下文件(最关键的是
Makefile
),并保存在当前目录(/build
)下|- CMakeFiles |- ... |- cmake_install.cmake |- CMakeCache.txt |- Makefile
- 这些生成的文件分别代表什么?参考
- 我理解,除了
Makefile
,其他都是类似于”中间文件“的存在吧。有了它们之后,再次执行cmake
会跳过很多操作。
- 我理解,除了
- 这些生成的文件分别代表什么?参考
-
make
- 根据生成的
Makefile
执行make
编译。
- 根据生成的
第三层:一些更全面的学习
这一节记录一些CMake的基本语法与使用策略。
基本语法
一些常用的指令
CMakeLists.txt文件中指令不区分大小写,但一般来说同一个文件中指令应保持大小写风格一致。
-
cmake_minimum_required
- 一般这条指令出现在CMakeLists.txt的最开始,指明了对cmake的最低版本的要求。
- 参考
-
project
- 指定工程名称(和版本、描述等)。
- 工程名称给定后,一些内置变量,如
PROJECT_NAME
、CMAKE_PROJECT_NAME
会被自动赋为该值。 - 参考
-
include_directories
- 添加头文件搜索目录。
- 目录可以使用绝对路径格式,也可使用(相对于当前CMakeLists.txt的)相对路径格式。
- 相当于g++中
-I
的作用。 - 参考,参考
- The difference between
include_directories
andtarget_include_directories
?
-
link_directories
- 添加需要链接的库文件目录。
- 相当于g++中
-L
的作用。 - 何时使用:如果要链接的库,没有放在
/lib
和/usr/lib
和/usr/local/lib
这三个目录里,需要使用该指令指定库文件所在的目录。
-
target_link_libraries
- 添加需要链接的库名称(或路径等,参见官方文档)。
- 相当于g++中
-l
的作用。
-
add_library
-
把指定的源文件来打包成动态库或静态库。
问:源文件写
.cpp
还是.h
?似乎是要写
cpp
。写.h
的话,会在cmake阶段报错CMake Error: Cannot determine link language for target
-
-
add_executable
- 使用指定的源文件来生成目标可执行文件。
- 参考
-
add_definitions
- 向C/C++编译器添加
-DXXX
定义。如果代码中定义了#ifdef XXX #endif
,这个代码块就会生效。 - 参考
- 向C/C++编译器添加
-
aux_source_directory
- 使用方法为:
aux_source_directory(<dir> <variable>)
,即根据dir
内的所有源代码文件名生成列表,保存在变量variable
中。 - 参考
- 使用方法为:
-
add_subdirectory
- 添加一个子目录并构建该子目录。
- 子目录下应该包含
CMakeLists.txt
文件和代码文件。 - 后文讲组织关系时会重点介绍该指令。
- 参考
-
file
- 专注于文件系统中的文件和路径的读、写、查等。
- 参考(cmake文档) 参考
-
message
- cmake中打印信息的语句,类比Python中的
print()
- 参考(cmake文档)
- cmake中打印信息的语句,类比Python中的
-
option
- 定义一个bool开关,可在终端赋值
- 语法为:
option(<variable> "<help_text>" [value])
- 参考
变量
访问变量
使用${}
进行变量的访问,如${VAR}
。
定义变量
显式定义
使用set
显式定义变量,基本的用法为
1
set(<variable> <value>)
示例
1
2
set(VAR "I am a variable")
message("${VAR}") # I am a variable
注:
如果
set
语句的value不加双引号,则定义的变量则被解释成列表,如
1 2 set(VAR I am a variable) message("${VAR}") # I;am;a;variable如果
message
语句中对变量的引用不加双引号,则输出中各元素中无空格,如
1 2 set(VAR "I am a variable") message(${VAR}) # Iamavariable
隐式定义
举例
使用project(PROJ_NAME)
语句指定工程名称后,则<PROJ_NAME>_BINARY_DIR
和<PROJ_NAME>_SOURCE_DIR
则会被隐式地定义:
1
2
3
4
5
project(ToyDemo)
message("<PROJ_NAME>_BINARY_DIR: ${ToyDemo_BINARY_DIR}")
# output: path/to/current_bin_dir
message("<PROJ_NAME>_SOURCE_DIR: ${ToyDemo_SOURCE_DIR}")
# output: path/to/current_source_dir
内置变量
以下记录下常见的内置变量
变量名 | 含义 |
---|---|
PROJECT_NAME | 通过project()指令定义的项目名称 |
PROJECT_SOURCE_DIR | 源CMakeLists.txt所在的目录 |
PROJECT_BINARY_DIR | 执行cmake命令所在的目录 |
控制指令写法
分支控制
1
2
3
4
5
6
7
if(<condition>)
<commands>
elseif(<condition>)
<commands>
else() # ()不能省
<commands>
endif() # ()不能省
注:
需要注意的是,
<condition>
中如果访问变量,不用(不应)加${}
**示例**
循环控制
foreach
1
2
3
4
5
6
7
foreach(<loop_var> <items>)
<commands>
endforeach()
foreach(<loop_var> RANGE <stop>) # 注意右侧闭区间,会输出[0, stop]共stop + 1个数!
foreach(<loop_var> RANGE <start> <stop> [<step>])
其中items
代指要遍历的列表,loop_var
代指正在被迭代到的项。
示例
1
2
3
4
5
6
7
8
9
10
11
12
set(mylist "a" "b" "c" "d")
foreach(var ${mylist})
message("current value: ${var}")
endforeach()
foreach(var RANGE 3)
message("current value: ${var}") # 输出 0 1 2 3 共【4】个数!
endforeach()
foreach(var RANGE 0 6 2)
message("current value: ${var}") # 输出 0 2 4 6
endforeach()
while
1
2
3
while(<condition>)
<commands>
endwhile()
如果需要用到数学计算,参照这里
示例
1
2
3
4
5
set(var 0)
while(var LESS 3)
message("cur var ${var}")
math(EXPR var "${var} + 1")
endwhile()
break和continue
把break()
或continue()
插入到循环体中终止循环。
常见的文件组织方式及应对策略
含有多个子目录
示例
文件目录如下:
1
2
3
4
5
6
7
8
9
10
11
.
├── CMakeLists.txt
├── main.cpp
├── sub1
│ ├── CMakeLists.txt
│ ├── test1.cpp
│ └── test1.h
└── sub2
├── CMakeLists.txt
├── test2.cpp
└── test2.h
主目录下的CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project(test)
include_directories(sub1)
include_directories(sub2)
add_subdirectory(sub1)
add_subdirectory(sub2)
add_executable(test main.cpp)
target_link_libraries(test sub1_lib sub2_lib)
主目录下的main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
// main.cpp
#include "test1.h"
#include "test2.h"
#include <iostream>
int main(int argc, char** argv)
{
test1_func();
test2_func();
return 0;
}
sub1目录下的CMakeLists.txt
1
2
3
4
# sub1/CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project(sub1)
add_library(sub1_lib test1.cpp)
sub1目录下的test1.h
和test1.cpp
1
2
3
4
5
6
// sub1/test1.h
void test1_func();
// sub1/test1.cpp
#include "test1.h"
void test1_func(){}
sub2目录下的CMakeLists.txt
1
2
3
4
# sub2/CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project(sub2)
add_library(sub2_lib test2.cpp)
sub2目录下的test2.h
和test2.cpp
1
2
3
4
5
6
// sub2/test2.h
void test2_func();
// sub2/test2.cpp
#include "test2.h"
void test2_func(){}
之后在主目录下正常执行cmake即可。
需要注意的问题
-
CMakeLists.txt
文件名大小敏感,否则会报错"CMake Error: does not appear to contain CMakeLists.txt"