CMake 是一个跨平台的构建、测试、打包工具,被广泛使用于 C++ 开源项目中

本文将介绍 CMake 在 C++ 项目中的通常用法

本文的例子使用 CMake 常用的规范项目结构,因此可以直接当作 C++ 项目的脚手架

环境配置

macOS

首先我们需要 C++ 编译器和调试器,于是我们可以输入以下命令直接安装 Xcode 的命令行工具来使用它自带的 Clang

xcode-select --install

然后使用 brew 安装 CMake 就行了

brew install cmake

Windows

推荐使用 MSYS2 直接安装集成到 mingw-w64 上的 CMake 和 Clang,打开 MSYS2 Shell 输入以下命令,当然你最好要把放着 CMake 和 Clang 的 bin 文件夹扔到 PATH 环境变量里

pacman -S mingw-w64-x86_64-cmake
pacman -S mingw-w64-x86_64-clang

当然也可以直接到 CMake 官网下载安装包,再去装 mingw-w64 的编译器集成,甚至可以装个 Visual Studio 来使用微软提供的 C++ 编译器,但是很麻烦

Helllo World

项目的目录结构

创建一个文件夹命名为 HelloWorld,在其中创建一个 CMakeLists.txt 文件,buildsrc 这两个文件夹。 把 Hello.cppsrc 文件夹里,项目结构大概长这样

- HelloWorld
  - build
  - src
    - Hello.cpp
  - CMakeLists.txt

HelloWorld/CMakeLists.txt

这个是 CMake 项目的配置文件,用来描述项目中包含哪些目标,有什么源文件、头文件等等

它会被 CMake 读取,用于配置 HelloWorld 项目
看下面它的代码,显然就是字面意义上的限定 CMake 最低版本的要求,然后项目名叫做 HelloWorld,添加一个名为 Hello 的可执行目标,它包括了 src 文件夹里的 Hello.cpp 这个源文件
最后编译出来就是一个二进制可执行文件,并以 src/Hello.cpp 里的 main 函数作为入口点

cmake_minimum_required(VERSION 3.0.0)
project(HelloWorld)

add_executable(Hello src/Hello.cpp)

HelloWorld/src/Hello.cpp

这是即将被编译的源文件,包含 main 函数,输出一个字符串

#include <iostream>

int main(int, char**) {
    std::cout << "Hello, world!\n";
    return 0;
}

项目构建

于是我们就可以开始编译了,CMake 不会直接构建项目,它会生成构建所需的与平台有关的中间文件,这是 CMake 跨平台的实现方法,这样对于同一个项目,我们在不同的平台上都能生成对应的中间文件来进行构建
比如在 Windows 上可以生成 Visual Studio 项目文件,然后用 VS 打开它来编译;类 UNIX 上可以生成 Makefile;此外也可以选择生成跨平台的 build.ninja、macOS 上的 Xcode 项目文件等等

但是 CMake 生成的这些中间文件很混乱,因此习惯上我们会新建一个 build 文件夹来存放它们,也就是上面项目目录结构里的那个 build 文件夹
于是在终端中切到 HelloWorld/build 目录,使用生成器生成中间文件,依据操作系统可能需要选择不同的生成器

Linux 和 macOS

类 UNIX 系统上默认会生成 Makefile,于是使用 cmake 命令之后再用 make 命令就能编译出可执行文件,直接运行就可以了

cd build
cmake ..
make
./Hello

Windows

Windows 上不知道默认会生成什么,CMake 文档中没说,Windows 也不会自带 VS 或者 mingw32-make 这样的东西,而且不同 VS 版本的项目还不怎么兼容,因此我们最好使用 -G 参数来手动指定,比如在 PowerShell 中输入下面的命令会在 build 目录下生成 VS 2019 项目

cd build
cmake.exe .. -G "Visual Studio 16 2019"

build 目录下应该会有一个 HelloWorld.sln,接下来就直接用 VS 2019 打开它,正常构建就行了

你也可以安装一个 mingw32-make,然后选择对应的生成器来生成中间文件,这样就能用类似 make 的方式来构建项目

在 MSYS2 Shell 中安装 mingw32-make

pacman -S mingw-w64-x86_64-make

PowerShell 切到 HelloWorld 目录之后

cd build
cmake.exe .. -G "MinGW Makefiles"
mingw32-make.exe
.\Hello.exe

具体能生成什么可以输入下面的命令来查询 CMake 支持的所有生成器

cmake --help

库的配置与链接

刚刚我们构建了一个可执行目标 Hello,但是我们可能需要引入外部的开源库或者项目本身的库,这时我们就需要把 Hello 跟这堆库链接起来,并且把库的头文件暴露给 Hello

假设有一个外部库 HelloExternalLib,一般习惯把外部库单独放在一个文件夹里,于是我们创建一个 external 文件夹把它扔进去
项目结构长这样

- HelloWorld
  - build
  - external
    - HelloExternalLib
      - include
        - HelloExternalLib
          - HelloExternalLib.h
      - src
        - HelloExternalLib.cpp
      - CMakeLists.txt
    - CMakeLists.txt
  - src
    - Hello.cpp
  - CMakeLists.txt

HelloWorld/external/HelloExternalLib/include/HelloExternalLib/HelloExternalLib.h

是这个库的一个头文件,随便声明一个方法好了

#ifndef HELLO_EXTERNAL_LIB_H
#define HELLO_EXTERNAL_LIB_H

void sayHello();

#endif  // HELLO_EXTERNAL_LIB_H

HelloWorld/external/HelloExternalLib/src/HelloExternalLib.cpp

在这个文件中定义那个方法

#include <HelloExternalLib/HelloExternalLib.h>

#include <iostream>

void sayHello() {
    std::cout << "Hello, world!\n";
}

HelloWorld/external/HelloExternalLib/CMakeLists.txt

这个文件用于构建 HelloExternalLib 这个库本身,添加了 HelloExternalLib 的库目标,包括 HelloExternalLib.cpp 这个源文件
其中的 target_include_directories 用于指定库的头文件路径,PUBLIC 代表如果库被链接这些头文件对外部是可见的,改成 PRIVATE 的话就是对外部不可见,只有库本身的源文件能够找到这些头文件,比如 Hello 目标链接了 HelloExternalLibinclude 文件夹中的头文件都是对 Hello 可见的

cmake_minimum_required(VERSION 3.0.0)
project(HelloExternalLib)

add_library(HelloExternalLib src/HelloExternalLib.cpp)

target_include_directories(HelloExternalLib PUBLIC include)

HelloWorld/external/CMakeLists.txt

这个文件仅仅是通过 add_subdirectory 把子文件夹添加到 CMake 项目中,也就是把 HelloExternalLib 这个文件夹加进来,这样 CMake 才会读取子文件夹中的 CMakeLists.txt 文件并进行相应的构建工作
注意子文件夹可以有多级目录嵌套,并且是递归进行的,当然需要在每一级目录下都编写 CMakeList.txt 然后加上 add_subdirectory

add_subdirectory(HelloExternalLib)

HelloWorld/CMakeLists.txt

这个文件就是刚刚用来配置 HelloWorld 项目的

我们加一行 add_subdirectory 来让它添加 external 子目录

并且使用 target_link_librariesHello 这个可执行目标跟 HelloExternalLib 这个库目标链接起来,并使用 PRIVATE 的链接方式
PRIVATE 这种链接方式表示 HelloExternalLib 不会被暴露给链接了 Hello 的其他目标,如果使用 PUBLIC 链接方式,其他的目标链接 Hello 时也会自动链接 HelloExternalLib,尽可能使用 PRIVATE,这样可以避免很多潜在的问题
而对于可执行目标来说,它不会被其它的目标链接,所以可以全部使用 PRIVATE

cmake_minimum_required(VERSION 3.0.0)
project(HelloWorld)

add_subdirectory(external)

add_executable(Hello src/Hello.cpp)

target_link_libraries(Hello PRIVATE HelloExternalLib)

HelloWorld/src/Hello.cpp

这时我们的源文件中就可以包含外部库的头文件并调用里面声明的方法了

#include <HelloExternalLib/HelloExternalLib.h>

int main(int, char**) {
    sayHello();
}

然后生成中间文件
macOS 和 Linux 下可以这样,Windows 下就根据上文添加 -G 参数并选择合适的生成器

cd build
cmake ..

最后使用 make 或者 Windows 下的其他工具构建就行了