事先说明
这篇文章是一个CMake工具的简单使用介绍。缘起是因为上个学期上编译原理的课程要求做课程设计,用C++实现。
一直以来出于一种奇妙心态的我非常厌恶使用 Visual Studio 这种笨重的IDE,向来用VScode作为C++主力编辑器,然而很遗憾VSCode自身不怎么支持C++多文档的编译(很麻烦,要不停的修改json配置文件),无奈为了寻求出路就稍微学了一下CMake来组织工程。
本文只对CMake最基本的用法做介绍,不涉及太高深的内容,比较适合像我一样的小白食用。也可以当做在VSCode上开发C++的进阶参考。
至于如何在VSCode上配置最基本的C++程序运行,在很多地方比如知乎或者其他博客里都讲的很详细了。大体无非几步: 下载MinGW、配置.vscode/tasks.json
、配置.vscode/launch.json
、配置.vscode/c_cpp_properties.json
,不做赘述。
CMake是什么
你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。
CMake就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等。
一般使用 CMake 生成 Makefile 并编译的流程如下: 1. 编写 CMake 配置文件 CMakeLists.txt 2. 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile(ccmake 和 cmake 的区别在于前者提供了一个交互式的界面),其中, PATH 是 CMakeLists.txt 所在的目录 3. 使用 make 命令进行编译
最简单的用法
演示
假设现在我们在这么一个目录: Demo1
1 | . |
main.cpp的内容如下: 1
2
3
4
5
6
7
8
int main(int argc, char** argv)
{
int a = -2;
std::cout << a << std::endl;
return 0;
}
为了编译这个文件,我们编辑CMakeLists.txt
文件为:
1 | # CMake 最低版本号要求 |
然后进入Build目录,执行cmake
1 | cd Build/ |
然后我们就会发现在Buile/
目录下多了一大堆东西: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20.
├── Build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.12.3
│ │ │ ├── CMakeCCompiler.cmake
│ │ │ └── <略>
│ │ ├── cmake.check_cache
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── CMakeOutput.log
│ │ ├── CMakeTmp
│ │ ├── Demo1.dir
│ │ │ ├── build.make
│ │ │ └── <略>
│ │ ├── feature_tests.bin
│ │ │ └── <略>
│ ├── cmake_install.cmake
│ └── Makefile
├── CMakeLists.txt
└── main.cpp
注意到Build
目录下现在多了一个Makefile
文件。接着我们只需要在Build
目录下执行命令: 1
make
然后我们就会发现Build
目录下多了一个Demo1.exe
文件,编译完成。
解释
CMakeLists语法
cmake_minimum_required
语法格式为: 1
cmake_minimum_required(VERSION <min>[...<max>] [FATAL_ERROR])
用于设置最低版本号要求, 需要放在文件最开头位置。
project(Main)
Sets project details such as name, version, etc. and enables languages
1 | project(<PROJECT-NAME> [LANGUAGES] [<language-name>...]) |
project命令会创建一些相关变量,例如: - PROJECT_SOURCE_DIR
: 整个项目的根目录,即包含PROJECT()的最近一个CMakeLists.txt文件所在的目录 - PROJECT_BINARY_DIR
: Build路径,在这里就是 **、Demo1/Build
set()
set用于设置变量的值,格式为: 1
set(<variable> <value>... [PARENT_SCOPE])
如果<variable>
不存在,那就回创建一个新的变量, 不过这里的变量是系统预定义的变量。
文件中涉及到的变脸有两个.
一个是CMAKE_CXX_COMPILER
,指的是C++的编译器,这是一个内置的变量, 准确来讲它应该是以CMAKE_<LANG>_COMPILER
为模版的一个实例, <LANG>
指的是语言,官方对它的解释是: > The full path to the compiler for LANG
比如如果我们想要设置C语言的编译器,那就是 CMAKE_C_COMPILER
, 而C++的就是 CMAKE_CXX_COMPILER
。
等等,为什么C++在这里是CXX? 这就涉及到不同平台下C++程序的后缀名问题了,在Windows下我们常用的就是一个.cpp扩展名,但在其他标准中,还有很多种不同扩展名,比如 C cc cxx
等等都是C++文件的扩展名, 详情见C++后缀名的问题
另外一个是CMAKE_CXX_FLAGS
,它指的是编译的可选参数,同样,它也是CMAKE_<LANG>_FLAGS
的一个实例,这里我们设置了 -g
参数,表明保留调试信息。
1 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") |
咦? ${CMAKE_CXX_FLAGS}
是什么东西? 其实很好理解,${<variable>}
指的就是取变量的值,我们可以利用 message()
函数来看看一个变量的值是什么,它可以把参数代表的值打印出来:
1 | # 输出 -g |
add_executable(Demo1 main.cpp)
1 | add_executable(<name> [WIN32] [MACOSX_BUNDLE] |
该命令指定了将源文件输出到可执行文件 <name>
,比如在Windows平台上,就会生成一个 <name>.exe
文件。
默认情况下,可执行文件将会在Build路径下被创建。如果要改变这个位置,有很多种办法,最简单的是修改 EXECUTABLE_OUTPUT_PATH
变量的值,比如:
1 | # 设置exe文件输出的 Bin 目录下 |
额外说明
- 之所以使用一个Build目录就是因为cmake出来的东西太多,最好放到一个专门的目录下
cmake 的格式是
cmake <dir>
, 其中<dir>
中必须有CMakeLists.txt
文件- 由于我现在是在Windows下, 如果直接使用
cmake ..
会默认使用 Visual Studio 的编译器,可以通过-G
指定,这里我们使用的是MinGW make
程序在Windows上没有,可以使用 MinGW 中的bin/mingw32-make.exe
替代,比如我们可以复制一份此程序,然后改名为make.exe
多文件处理
以上我们演示了在只有一个源文件的情况下的使用方法,现在我们看一看多文件的情况,在Demo2文件夹里,目录结构是这样的: 1
2
3
4
5
6
7
8
9
10.
├── a.cxx
├── a.h
├── b.cxx
├── Bin
├── Build
├── build.bat
└── CMakeLists.txt
2 directories, 5 files
build.bat
只是把之前build的命令写到脚本里,不去管它,我们先来看一看a.h
, a.cxx
, b.cxx
的内容:
1 | // a.h |
显然这是一组相互依赖的程序,我们来看一看怎么处理。
方法1
1 | cmake_minimum_required (VERSION 2.8) |
可以看到,和之前的几乎没有区别,只不过 add_executable
的 <source>
参数变成了两个。
方法2
上面的方法存在着一个问题,如果源文件很多的话,那我们将每个CPP文件手动加到 add_executable
中也不合适,而且不利于动态的增加文件,要解决这个问题,我们需要用一些小技巧:
1 | cmake_minimum_required (VERSION 2.8) |
在这里我们使用了 aux_source_directory
这个函数,来看看它的官方解释:
1 | aux_source_directory(<dir> <variable>) |
Collects the names of all the source files in the specified directory and stores the list in the
<variable>
provided.
要注意的是,这里强调了只有源文件才会被加入,比如在这个例子中,只有 a.cxx b.cxx 被加入了,而其余的 a.h CMakeLists.txt 都不会被加进去。
多目录的情况
在实际的使用之中,我们往往都不会把所有代码放在一个目录之下,现在我们来看看当存在多个目录时怎么办。
假设我们现在有三个文件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//./Include/a.h
extern int x;
void f();
//./a.cpp
int x = 0;
void f()
{
std::cout << "This is f()" << std::endl;
}
//./main.cpp
using namespace std;
int main()
{
f();
return 0;
}
Case 1
先看一个最简单的情况:
1 | . |
在这个例子中,我们的源文件还是在工程根目录下,而 a.h
头文件被放在了一个子目录 ./Include
中。
这一次如果我们还是用之前的那个 CMakeLists.txt
的话就会出错(准确来讲是cmake可以过,但make时就会报错): 1
2
3E:\my_programming\C++_Program\UseCMake\Demo3\a.cpp:1:15: fatal error: a.h: No such file or directory
#include "a.h"
^
1 | include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...]) |
使用这个方法就可以把新的目录添加到工程的Include路径当中来,现在我们的 CMakeLists.txt
长这个样子: 1
2
3
4
5
6
7
8
9cmake_minimum_required (VERSION 2.8)
project(Demo3)
set(CMAKE_CXX_COMPILER "g++")
set (CMAKE_CXX_FLAGS "-g -fexec-charset=GBK")
# 添加include路径
include_directories(${PROJECT_SOURCE_DIR}/Include)
aux_source_directory(./ SrcFiles)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/Bin)
add_executable(Demo3 ${SrcFiles})
运行cmake+make,搞定!
顺便说个题外话,一开始在a.h
文件里我写了一个int x = 0;
,结果直接报错了,后来一查才发现问题出在哪里,详见multiple definition of 问题解决方法
Case 2
第二种情况: 1
2
3
4
5
6
7
8
9
10
11.
├── Bin
├── Build
├── CMakeLists.txt
├── Include
│ └── a.h
└── Src
├── a.cpp
└── main.cpp
4 directories, 4 files
头文件和源文件分属不同的目录,编译结果输出到./Bin/
目录下,这也是很常用的一种组织方式。看上去很麻烦,然而实际上很简单,改一下SrcFiles就行了。
1 | cmake_minimum_required (VERSION 2.8) |
和VSCode集成起来
有了上述的基础,我们可以在VSCode中愉快地使用了。以我编译原理的作业为例,我的文件结构是这样的:
1 | . |
为了便于使用我在根目录下编写build.bat
文件: 1
2
3
4@echo off
cd .\Build
cmake -G "MinGW Makefiles" .. > TriffleInfo
make
还记得.vscode下有个task.json文件吗,把它改成这样: 1
2
3
4
5
6
7
8
9
10
11
12
13
14{
"version": "2.0.0",
"tasks": [
{
"label": "compile",
"type": "shell",
"command": ".\\build.bat",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
task.json只执行编译过程,所以launch.json也得改: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/Bin/Compile.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"miDebuggerPath": "D:\\MinGW\\MinGW_Location\\bin\\gdb.exe",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
//"preLaunchTask": "compile"
}
]
}
然后按Ctrl+Shift+B执行编译,按F5运行即可。要是想一步到位,可以把上面那行"preLaunchTask": "compile"
的注释取消了,按F5就可以一次性编译运行了,不过这样不太好,万一编译错了你运行的程序还是之前正确的那个。
结语
CMake的简单用法就到这里了,它当然还有更多更复杂的用法,比如链接第三方库什么的,这些内容由于我没用到所以也懒得去学了,像了解的可以参考官方文档,或者大批CMake教程,这里我只介绍最简单的。