CMake 是一个开源、跨平台的构建系统生成器(Build-system Generator)。
CMake 是构建系统生成器,而不是构建系统,CMake 支持生成不同构建系统所支持的工程文件,如 Visual Studio,XCode,Makefile 等。
本教程作为 CMake 的简明教程,不会事无巨细的讲述 CMake 的每一个语法,而是以实用为目的,介绍 CMake 的基础语法和常用指令。
一、Modern CMake
CMake 距今已有 20 多年的历史,CMake 从 3.0 开始引入 Target 概念,有了 Target 和 Property 的定义,CMake 也就更加地现代化。
我们将引入 Target 概念之前(也就是 3.0 之前)的 CMake 称之为老式 CMake,之后的称之为现代 CMake(Modern CMake)。
现代 CMake 是围绕 Target 和 Property 来定义的,在现代 CMake 中不应该出现诸如下面的指令:
- add_compile_options
- include_directories
- link_directories
- link_libraries
因为这些指令都是目录级别的,在该目录(含子目录)上定义的所有目标都会继承这些属性,这样会导致出现很多隐藏依赖和多余属性的情况。
我们最好直接针对 Target 进行操作,如:
add_executable(hello main.cpp)
# 老式写法
include_directories(./include)
# 现代写法
target_include_directories(hello PRIVATE ./include)
本文讲述的知识点只适用于现代 CMake,让我们脱掉沉重的历史包袱,轻装上阵吧!
二、基础概念
所有的构建系统都需要通过某个入口点来定义项目(如 Visual Studio 的 .sln 文件),CMake 作为构建系统生成器也不例外,CMake 使用的是 CMakeLists.txt
的文件,该文件以 UTF-8 编码(也支持 UTF-8 BOM 文件头),其中存储了符合 CMake 语言规范的脚本代码。
2.1 项目结构
CMake 没有强制规定 CMakeLists.txt 文件的位置以及项目的目录结构,但目前大多数项目都会采用相似的目录结构。
如果项目名称为 my_project,且该项目包含一个名为 lib 的库和一个名为 app 的程序,则目录结构通常如下面所示:
- my_project
- .gitignore
- README.md
- LICENSE.md
- CMakeLists.txt
- cmake
- FindSomeLib.cmake
- something_else.cmake
- include
- my_project
- lib.h
- src
- CMakeLists.txt
- lib.cpp
- apps
- CMakeLists.txt
- app.cpp
- tests
- CMakeLists.txt
- testlib.cpp
- docs
- CMakeLists.txt
- extern
- googletest
- scripts
- helper.py
当然,上面的名称并不是一成不变的,可以根据自己的喜好来定义,例如 my_project 可以是任意的项目名,如果不喜欢复数,可以将 tests 改成test,如果没有 python 代码,也可以移除 python 目录,cmake 目录则用于存放 CMake 辅助脚本。
从上面的目录结构可以看到,CMakeLists.txt 文件分散在各个子目录中,但在 include 目录中没有 CMakeList.txt 文件,这样是为了防止暴露不必要的文件给库的使用者,因为 include 目录中存放的是库的头文件,在安装时通常都会将该目录拷贝到指定位置(如Linux系统的 /usr/include)。
extern 目录用于存放第三方依赖库的源码,这些库可以通过 git submodule
的形式来管理,也可以直接将源码拷贝到此,并提交到项目 git 中。但无论使用哪种方式,依赖库最好能支持 CMake,这样可以方便的使用 add_subdirectory
命令将项目添加到工程中(add_subdirectory 可以添加任何包含 CMakeLists.txt 文件的目录到项目中)。
2.2 一个简单的示例
我们先从一个简单的示例开始,了解 CMake 的基本玩法。
该示例是只包含一个 main.cpp 文件,我们期望编译该文件能生成 hello_cmake 程序。
目录结构如下:
- hello_cmake
- main.cpp
- CMakeLists.txt
main.cpp 文件的内容非常简单:
#include <stdio.h>
int main() {
printf("hello cmake");
return 0;
}
CMakeLists.txt 内容如下:
# 设置 CMake 的最低版本
cmake_minimum_required(VERSION 3.16)
# 设置项目名称
project (hello_cmake)
# 添加一个名为 hello_cmake 的目标
# 目标类型为可执行文件
# 使用 main.cpp 来编译生成 hello_cmake 可执行文件(如hello_cmake.exe)
add_executable(hello_cmake main.cpp)
完成上面步骤,我们就可以使用 CMake GUI 或命令行(当然你需要提前安装 CMake,这不在本文的介绍范围之内)就可以生成相应的工程了。
通过 CMake 命令行生成 Visual Studio 工程的命令如下:
cmake.exe -G "Visual Studio 15 2017" -S .\hello_cmake -B .\hello_cmake\build
2.3 源码外构建
我们通常会将构建目录指定到一个单独的子目录内,这个目录名称的通常是 build
。如果不这样做,CMake 生成的工程文件和临时缓存文件会污染源码目录。这种方式有个学名叫“源码外构建” (out-of-source build)。
使用源码外构建时,我们通常还会将 build 目录添加到 .gitignore 文件中。
2.4 工作流程
编写 CMake 脚本的基本流程如下:
- 在脚本第一行使用 cmake_minimum_required 指定运行当前脚本所需的 CMake 最低版本。
- 使用 project 指定项目名称。
- 使用 add_executable 或 add_library 创建目标。
- 为目标设置包含目录、链接库等属性(可选)。
- 安装(可选)。
编写完 CMake 脚本以后,就可以使用 CMake GUI 或命令行来生成对应的工程文件了。以 Visual Studio 为例,对于有 my_lib 库 和 app 应用程序的项目,CMake 会生成如下图所示的 5 个项目:
下面介绍 CMake 自动生成的一些项目的作用:
- 编译 ALL_BUILD 项目会自动编译除 INSTALL 项目外的所有项目。
- 编译 INSTALL 项目会执行 CMake 脚本中指定的安装操作。
- 编译 ZERO_CHECK 项目会再次执行 CMake 脚本,重新生成项目。因此若 CMake 脚本有更新,既可以使用 CMake 工具来重新生成项目,也可以是重新编译 ZERO_CHECK 项目。
2.5 注释
在 CMake 中使用 #
来声明单行注释,这是我们使用最多的注释方法。虽然也支持使用 #[[ ]]
来声明多行注释(也称块注释),但是使用的比较少,例如:
#[[
这是多行注释也称块注释
你明白了吗?
]]
2.6 CMake最低版本
cmake_minimum_required 是我们接触到第一个 CMake 指令,该指令用于指定编译该脚本所需的最低 CMake 版本。
在每个 CMakeList.txt 文件的第一行都会使用该指令。
cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
如果运行 CMake 的版本低于要求的版本,则将停止处理该脚本并返回错误。
我们始终应该选择一个比编译器晚发布的 CMake 版本,因为只有这样,CMake 才能支持新的编译器选项。但最低版本不应低于 3.0,实际项目中通常最低版本不会低于 3.16(该版本于2020年09月15日发布),本教程也是以此为标准进行讲解的。
2.7 项目名称
使用 project 指定项目名称。
项目名称区别于目标(Target)名称,以 Visual Studio 为例,project 指定的名称对应“解决方案名称”,而 add_executable 或 add_library 等指定的名称才对应具体项目名和生成的“目标文件名”。
设置项目名称后,CMake 会自动定义一些变量(变量的具体用法会在稍后的“3.1 变量”小节进行介绍)。为了方便介绍各个变量的含义,假设我们是通过如下命令来运行 CMake 的:
cmake.exe -G "Visual Studio 15 2017" -S D:\hello_cmake -B D:\hello_cmake\build
下面列举了一些 CMake 自动定义的变量:
- PROJECT_NAME
项目名称,如 hello_cmake - CMAKE_PROJECT_NAME
如果 CMakeLists.txt 位于项目的顶级目录,还会定义 CMAKE_PROJECT_NAME 变量,值与 PROJECT_NAME 一致。 - PROJECT_SOURCE_DIR
项目的根目录(绝对路径),即-S
参数指定的目录,如 D:\hello_cmake - <PROJECT-NAME>_SOURCE_DIR
值与 PROJECT_SOURCE_DIR 相同,只是变量名不同,如 hello_cmake_SOURCE_DIR - PROJECT_BINARY_DIR
项目的构建目录(绝对路径),即-B
参数指定的目录,如 D:\hello_cmake\build - <PROJECT-NAME>_BINARY_DIR
值与 PROJECT_BINARY_DIR 相同,只是变量名不同,如 hello_cmake_BINARY_DIR
主CMakeLists.txt
主 CMakeLists.txt 即项目根目录下的 CMakeLists.txt 文件。可以通过检查 CMAKE_PROJECT_NAME 与 PROJECT_NAME 变量是否相同来判断当前的 CMakeLists.txt 文件是否为主 CMakeLists.txt。
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
endif()
2.8 目标类型
既然现代 CMake 是围绕目标(Target)工作的,Target 如此重要,那我们首先就需要创建一个 Target。
在 C/C++ 开发中,常见的 Target 类型有:可执行文件、静态库、动态库,CMake 还额外提供了一个 MODULE 类型。
下面列举了不同类型的目标的创建方式。
可执行文件
使用 add_executable 指令可以创建可执行文件类型的目标。
add_executable(my_exe main.cpp)
动态库和静态库
通过为 add_library 指令指定不同的参数,可以创建动态库和静态库。
add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)
# 动态库
add_library(my_lib SHARED main.cpp)
# 静态库
add_library(my_lib STATIC main.cpp)
我们也可以在 add_library 中不指定类型参数,改为通过设置 BUILD_SHARED_LIBS 变量来切换静态库和动态库。下面示例在脚本中设置了 BUILD_SHARED_LIBS 变量值为 ON (ON / OFF 对应 CMake 中的开/关):
cmake_minimum_required(VERSION 3.16)
project (hello_cmake)
set(BUILD_SHARED_LIBS ON)
add_library(hello_cmake main.cpp)
也可以通过命令行参数进行指定 BUILD_SHARED_LIBS 变量:
cmake.exe -G "Visual Studio 15 2017" -DBUILD_SHARED_LIBS=ON -S .\hello_cmake -B .\hello_cmake\build
亦可以在 GUI 界面上设置 BUILD_SHARED_LIBS 变量,如: