> For the complete documentation index, see [llms.txt](https://ricardolu.gitbook.io/trantor/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ricardolu.gitbook.io/trantor/cmake-in-action/cmake-tutorial.md).

# CMake Tutorial

一个简单但全全面的CMake指南。

### Introduction

CMake是一个可扩展的开源系统，它以一种与编译器无关的方式在操作系统中管理构建过程。

和跨平台系统不同，CMake被设计为在本机系统上使用。

CMake支持`in-place`（在`CMAKE_SOURCE_DIR`目录下直接构建）构建和`out-space`（在自定义的目录下构建）编译，也即在一个源码树下进行多个构建工程。

CMake支持构建动态和静态库。

CMake会生成一个缓存文件供图形编辑器（CMake-GUI）使用。

### Popular Open Source Project with CMake

以下是一个当前比较流行的使用CMake进行构建的开源项目列表：

* OpenCV: <https://github.com/opencv/opencv>
* Caffe2: <https://github.com/caffe2/caffe2>
* MySql Server: <https://github.com/mysql/mysql-server>

可以浏览[Wikipedia](https://en.wikipedia.org/wiki/CMake#Applications_that_use_CMake)以获得更多使用CMake进行构建的开源项目。

{% hint style="info" %}
阅读开源项目总是一个好的实践，我在入职伊始阅读了一小部分的Linux Kernel源码，这帮助我养成了良好的编码习惯，以及初步形成了编码规范的意识。在以上的开源项目中，OpenCV作为最流行的开源计算机视觉库，无论是源码还是其完备的CMake构建链，都十分值得深入了解，后续关于`package-management`的章节我将以OpenCV为例进行讲解。
{% endhint %}

### CMake 是一种脚本语言

CMake的目标是成为一个跨平台的构建过程管理器，所以它定义了自己的脚本语言，具有特定的语法和内置特性。同时CMake又是一个软件程序，所以开发者需要调用它的脚本文件来解释和生成真正的构建文件。

`CMakeLists.txt`和`<project_name>.cmake`都是CMake的脚本文件，但是通常使用`CMakeLists.txt`作为主脚本（就我目前短浅的经历而言`<project_name>.name`这种格式更多应用于第三方库管理中）。

* `CMakeLists.txt`通常位于要构建的项目源代码中。
* `CMakeLists.txt`可以位于任何它将起作用的应用程序、库的源码树的根目录下。
* 如果工程由多个模块并且每个模块可以独自构建和编译，`CMakeLists.txt`可以被插入到各个子目录下。
* `.cmake`文件也是CMake脚本文件，它运行cmake命令来准备环境预处理或分离可以写在`CMakeLists.txt`之外的任务。
* `.cmake`也可用于为项目定义模块。这些模块可以是单独构建的库活复杂的多模块项目额外的方法（函数）。

### CMake 命令

CMake命令类似于c++ /Java方法或函数，它们以列表的形式接受参数，并相应地执行某些任务。

CMake命令不区分大小写，具体的命令可以阅读[CMake-Commands文档](https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html)。

一些常用的CMake命令：

* `message`：打印信息
* `cmake_minimum_required`：设置项目使用的CMake的最低版本
* `add_executable`：添加可执行文件target
* `add_library`：添加需要编译的target库
* `add_subdirectory`：添加子目录

CMake编写规范：

* 缩进不是强制的，但为了方便阅读，请尽量合理使用缩进。
* CMake不需要使用‘;’作为语句的结束。
* 所有条件语句需要以对应的结束命令结束。

### CMake 环境变量

CMake的环境变量和普通变量相似，但有一下几点区别：

* 作用范围：CMake环境变量的作用范围为整个CMake进程，并且不会被缓存（也即构建之后无法再修改）。
* 引用形式：CMake环境变量可以通过`$ENV{<variable>}`的形式引用。
* 初始化：CMake环境变量具有初始值，也可使用`set()`和`unset()`修改，但只在CMake进程中生效，这点与系统环境变量不同。

[cmake-env-variables](https://cmake.org/cmake/help/latest/manual/cmake-env-variables.7.html#manual:cmake-env-variables\(7\))列出了所有有特殊含义的CMake环境变量。

### CMake 变量

CMake包含一系列预定义的变量用于定位源代码树和系统组分，与CMake命令不同，CMake变量区分大小写。

常用的CMake变量如下：

`CMAKE_BINARY_DIR`, `PROJECT_BINARY_DIR` ：这两个变量内容一致，如果是内部编译，就指的是工程的顶级目录，如果是外部编译，指的就是工程编译发生的目录。 `CMAKE_SOURCE_DIR`, `PROJECT_SOURCE_DIR`：这两个变量内容一致，都指的是工程的顶级目录。 `CMAKE_CURRENT_BINARY_DIR`：外部编译时，指的是target目录，内部编译时，指的是顶级目录 `CMAKE_CURRENT_SOURCE_DIR`：CMakeList.txt所在的目录 `CMAKE_CURRENT_LIST_DIR`：CMakeList.txt的完整路径 `CMAKE_CURRENT_LIST_LINE`：当前所在的行 `CMAKE_MODULE_PATH`：如果工程复杂，可能需要编写一些cmake模块，这里通过SET指定这个变量 `LIBRARY_OUTPUT_DIR`, `BINARY_OUTPUT_DIR`：库和可执行的最终存放目录 `PROJECT_NAME`, `CMAKE_PROJECT_NAME`：前者是当前CMakeList.txt里设置的project\_name，后者是整个项目配置的project\_name

[cmake-variables](https://cmake.org/cmake/help/v3.0/manual/cmake-variables.7.html#manual:cmake-variables\(7\))给出了所有预定义的CMake变量列表。

开发者也可自定义变量：

```
set_property (assign value to variable)
```

### CMake 列表

CMake中的所有值都存储为字符串，但在特定的上下文中，字符串可以被视为列表。

```
# sets files to "a.txt;b.txt;c.txt"
set(files a.txt b.txt c.txt)
​
foreach(file ${files})
    message("Filename: ${file}")
endforeach()
```

### 使用CMake构建C++代码

前面的段落介绍了CMake脚本的核心原理，接下来将从一个"Hello CMake!"程序出发，由浅到深讲解CMake脚本基本构成。

#### Hello CMake!

假设我们有这样一个`main.cpp`：

```
// main.cpp
#include <iostream>
int main() {
    std::cout<<"Hello CMake!"<<std::endl;
}
```

那么在Linux平台下的编译指令为：

```
g++ main.cpp -o cmake_hello
```

而`CMakeLists.txt`则如下

```
# 设置最低cmake版本要求
cmake_minimum_required(VERSION 3.9.1)
# 定义project的名字
project(CMakeHello)
# 添加目标可执行文件
add_executable(cmake_hello main.cpp)
```

假设是内部编译：

```
cmake .
make
```

外部编译：

```
mkdir build && cd build
cmake ..
make
```

{% hint style="info" %}
通常而言不建议使用内部编译，在OpenCV中就禁止了内部编译。
{% endhint %}

```
# Disable in-source builds to prevent source tree corruption.
if(" ${CMAKE_SOURCE_DIR}" STREQUAL " ${CMAKE_BINARY_DIR}")
  message(FATAL_ERROR "
FATAL: In-source builds are not allowed.
       You should create a separate directory for build files.
")
endif()
```

#### CMAKE\_CXX\_STANDARD

假如你的源代码中使用了更新的c++标准，那么你需要在CMakeLists.txt中指出。

```
set(CMAKE_CXX_STANDARD 14)
```

#### 系统检查

假如你想要为多个平台构建：

* 为不同平台单独生成可执行文件；
* 向指定系统的源代码传递宏定义等。

那么你需要在构建过程中检查当前的构建系统：

```
cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
​
set(CMAKE_CXX_STANDARD 14)
​
# UNIX, WIN32, WINRT, CYGWIN, APPLE are environment variables as flags set by default system
if(UNIX)
    message("This is a ${CMAKE_SYSTEM_NAME} system")
elseif(WIN32)
    message("This is a Windows System")
endif()
​
# or use MATCHES to see if actual system name 
# Darwin is Apple's system name
if(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    message("This is a ${CMAKE_SYSTEM_NAME} system")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    message("This is a Windows System")
endif()
​
add_executable(cmake_hello main.cpp)
```

{% hint style="info" %}
大型代码库的实现通常是与系统无关的，它们使用宏只对正确的系统使用特定的方法。
{% endhint %}

#### 宏

宏帮助工程师有条件地构建代码，根据运行的系统配置放弃或包含某些方法。通过宏定义，开发者可以向源代码传递信息，进而实现不同的功能。

CMake的宏需要在宏的名字使用`-D`标志来表明这是一个宏定义。

```
cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)
​
# Darwin is Apple's system name
# or use MATCHES to see if actual system name 
if(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    # 定义CMAKEMACROSAMPL这个宏
    add_definitions(-DCMAKEMACROSAMPLE="Apple MacOS")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    add_definitions(-DCMAKEMACROSAMPLE="Windows PC")
endif()
​
add_executable(cmake_hello main.cpp)
```

然后在代码中就可以根据定义的宏进行不同的操作，以下代码将打印`CMAKEMACROSAMPL`这个宏的值：

```
#include <iostream>
#ifndef CMAKEMACROSAMPLE
    #define CMAKEMACROSAMPLE "NO SYSTEM NAME"
#endif
auto sum(int a, int b){
        return a + b;
}
int main() {
        std::cout<<"Hello CMake!"<<std::endl;
        std::cout<<CMAKEMACROSAMPLE<<std::endl;
        std::cout<<"Sum of 3 + 4 :"<<sum(3, 4)<<std::endl;
        return 0;
}
```

#### CMake文件组织

CMake允许开发者在根目录下新建自己的构建目录，并在该目录下进行构建，以下为基本的CMake构建文件：

```
mkdir build
cmake ..
ls -all
-rw-r--r--   1 onur  staff  13010 Jan 25 18:40 CMakeCache.txt
drwxr-xr-x  15 onur  staff    480 Jan 25 18:40 CMakeFiles
-rw-r--r--   1 onur  staff   4964 Jan 25 18:40 Makefile
-rw-r--r--   1 onur  staff   1256 Jan 25 18:40 cmake_install.cmake
make all
```

除了手动创建`build`目录在进行构建，CMake还可以通过使用构建参数来完成同样的工作：

```
cmake -H. -Bbuild
# H indicates source directory
# B indicates build directory
```

#### 指定目标输出位置

```
cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
​
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
​
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
​
add_executable(cmake_hello main.cpp)
```

* 通过设置`CMAKE_RUNTIME_OUTPUT_DIRECTORY`或`EXECUTABLE_OUTPUT_PATH`变量的值，可以指定binary生成的位置。
* 通过指定`CMAKE_LIBRARY_OUTPUT_DIRECTORY`或`LIBRARY_OUTPUT_PATH`可以指定动态库生成的位置。
* 通过指定`CMAKE_ARCHIVE_OUTPUT_DIRECTORY`或`ARCHIVE_OUTPUT_PATH`可以指定静态库生成的位置。

通常来说指定单一路径即可，但对于大型工程项目而言，需要编写更复杂的CMake脚本来进行生成目标位置管理。

#### Debug和Release配置

CMake建议根据需要实现具有多种配置选项的`CMakeLists.txt`，例如Debug和Release选项允许用户构建两周性能和使用场景不同的项目。CMake通过定义`CMAKE_BUILD_TYPE`变量来完成构建版本控制：

```
cmake -DCMAKE_BUILD_TYPE=Debug -H.  -Bbuild/Debug
cmake -DCMAKE_BUILD_TYPE=Release -H. -Bbuild/Release
```

需要为不同的版本创建不同的目录，这时`CMakeLists.txt`就可以使用`CMAKE_BUILD_TYPE`这个变量完成构建类型的检查：

```
if(${CMAKE_BUILD_TYPE} MATCHES Debug)
    message("Debug Build")
elseif(${CMAKE_BUILD_TYPE} MATCHES Release)
    message("Release Build")
endif()
```

\[1]  CMake不是一个跨平台软件，需要为不同的平台单独开发，这里与交叉编译无关。
