一文搞懂 CMake

快速入门 camke, 掌握 cmake 在 c/c++ 项目构建上魅力

认识 CMake

编译 c/c++ 项目的工具迭代

通过上图我们可以大致了解到: cmake 是一个跨平台项目构建工具, 通过 cmake 命令, 可以将讲简单的工程配置描述文件 CMakeFiles.txt (即指令合集) 构建成 makefile 文件,帮我们省去编写繁杂的 makefile, 然后在通过 make 命令将源代码编译成目标文件

使用 cmake 工具, 首先需要安装 cmake 到本地环境

1
brew install cmake

安装好后, 接下来我们结合工程示例 https://github.com/oksep/cmake-begin ,逐一讲解 cmake 相关概念

简单工程

源码参考: v1_basic, 项目有如下文件结构:

1
2
3
4
5
6
7
8
9
10
.
├── CMakeLists.txt
├── include
│   └── head.h
└── src
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

我们主要讲解 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
# 配置 cmake 最低版本
cmake_minimum_required(VERSION 3.15)

# 指定 C++ 编译标准, set 可以配置预定义宏, 也可以新增变量并赋值
set(CMAKE_CXX_STANDARD 17)

# 配置项目名称
project(CALC)

# 配置可执行文件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

# 搜索指定目录的文件, 将得到的文件列表赋值给 SRC_LIST
# 搜索文件有两种方式: aux_source_directory() 和 file()
# 其中 CMAKE_CURRENT_SOURCE_DIR 为预定义宏, 表示当前文件路径
# aux_source_directory(./src SRC_LIST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB HEAD_LIST ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)

# 指定头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 将源文件 SRC_LIST 编译为可执行文件 app
add_executable(app ${SRC_LIST})

执行 cmake . -B ./build 命令
cmake 会读取 CMakeLists.txt 的相关指令, 在 build 文件夹下生成以下构建文件:

1
2
3
4
5
build
├── CMakeCache.txt
├── CMakeFiles
├── Makefile
└── cmake_install.cmake

执行构建命令 cmake --build build, 生成目标文件
这里实际上是执行了 make 命令的相关操作, 等价于 cd build && make:

1
2
3
4
5
6
7
[ 16%] Building CXX object CMakeFiles/app.dir/src/add.cpp.o
[ 33%] Building CXX object CMakeFiles/app.dir/src/div.cpp.o
[ 50%] Building CXX object CMakeFiles/app.dir/src/main.cpp.o
[ 66%] Building CXX object CMakeFiles/app.dir/src/mult.cpp.o
[ 83%] Building CXX object CMakeFiles/app.dir/src/sub.cpp.o
[100%] Linking CXX executable /Users/sep/Documents/cmake-tutorial/v1_basic/bin/app
[100%] Built target app

执行 ./bin/app 一个简单的计算器程序就可以运行起来了:

1
2
3
4
5
a = 12, b = 4
a + b = 16
a - b = 8
a * b = 48
a / b = 3.000000

生成静态库/动态库

源码参考: v2_gen_lib, 主要讲解 CMakeLists.txt 文件的配置(重点关注注释):

1
2
3
4
5
6
7
8
9
10
11
...
# 配置库文件所输出的位置, LIBRARY_OUTPUT_PATH 为 cmake 预定义宏
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

# 生成静态库, calc_static 为库名称, STATIC 标识为静态库, SRC_LIST 为要编译的所有源文件
# output -> libcalc.a
add_library(calc_static STATIC ${SRC_LIST})

# 生成动态库, calc_shared 为库名称, SHARED 标识为动态库, SRC_LIST 为要编译的所有源文件
# output -> libcalc.dylib
add_library(calc_shared SHARED ${SRC_LIST})

这里可以对比 v1_basic 工程中的 CMakeLists.txt 文件, 配置生成了不同的目标文件:

# command windows format linux format macOS format
可执行文件 add_executable .exe / /
静态库文件 add_library .lib .a .a
动态库文件 add_library .dll .so .dylib

同样执行 cmake 相关命令 cmake . -B build && cmake --build build, 可以看到在 lib/ 文件夹下, 输出了对应的库文件

1
2
3
lib
├── libcalc_static.a # 静态库
└── libcalc_shared.dylib # 动态库

链接静态库

源码参考: v3_link_static_lib, 项目有如下文件结构:

1
2
3
4
5
6
7
8
./v3_link_static_lib
├── CMakeLists.txt
├── include
│   └── head.h # 将 v2_gen_lib 中的 head.h 头文件拷贝到这里
├── lib
│   └── libcalc.a # 将 v2_gen_lib 生成的静态库拷贝到这里
└── src
└── main.cpp

使工程链接静态库, 在 CMakeLists.txt 的主要配置(重点关注注释):

1
2
3
4
5
6
7
8
9
10
11
12
...
# 静态库的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 要链接静态库的位置
link_directories(${PROJECT_SOURCE_DIR}/lib)

# 链接静态库 libcalc.a, 一般在指定链接库的时候, 会掐头去尾(lib___.a), 保留中间部分(calc)
link_libraries(calc)

# 生成可执行文件
add_executable(app ${SRC_LIST})

执行 cmake . -B build && cmake --build build 后, libcalc.a 被打包到了可执行文件中, 运行 bin/app 静态库 libcalc.a 立即被加载到内存中

链接动态库

源码参考: v4_link_shared_lib

文件目录结构同 v3_link_static_lib, 我们将 v2_gen_lib 生成的动态库libcalc.dylib 放在 lib/ 路径下, CMakeLists.txt 主要差异在(重点关注注释):

1
2
3
4
5
6
7
8
9
10
# 静态库的头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 要链接动态态库的位置
link_directories(${PROJECT_SOURCE_DIR}/lib)

add_executable(app ${SRC_LIST})

# 先生成可执行程序 app, 再对动态库 calc 进行链接
target_link_libraries(app calc)

这里需要注意: 链接动态库, 是在 add_executable 生成 app 后再去配置的

接下来执行 cmake . -B build && cmake --build build, 运行 bin/app, 只有当真正调用到 calc 相关函数的时候, libcalc.dylib 才会被加载到内存中

子模块

源码参考: v5_nested_project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── CMakeLists.txt
├── calc
│   ├── CMakeLists.txt
│   └── src
├── include
│   ├── calc.h
│   └── sort.h
├── sort
│   ├── CMakeLists.txt
│   └── src
├── test1
│   ├── CMakeLists.txt
│   └── calc.cpp
└── test2
├── CMakeLists.txt
└── sort.cpp

这里我们定义了四个子模块和一个根模块

  • 计算器模块: calc/CMakeLists.txt 配置生成静态库
  • 排序模块: sort/CMakeLists.txt 配置生成动态库
  • 测试计算器模块: test1/CMakeLists.txt 配置测试链接静态库
  • 测试排序模块: test2/CMakeLists.txt 配置测试链接动态库
  • 根模块

在根模块 CMakeLists.txt 中有如下定义(重点关注注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.15)
project(test)
set(CMAKE_CXX_STANDARD 17)

# 这里用 `set()` 指令定义了多个字段,这些字段在添加子模块时, 会被传递给各个子模块共享使用
set(LIB_PATH ${CMAKE_SOURCE_DIR}/lib)
set(EXEC_PATH ${CMAKE_SOURCE_DIR}/bin)
set(HEAD_PATH ${CMAKE_SOURCE_DIR}/include)
set(LIB_CALC calc)
set(LIB_SORT sort)
set(APPNAME1 test1)
set(APPNAME2 test2)

# 告诉 cmake 该项目包含以下四个子模块
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)

子模块 calc 的 CMakeLists.txt 配置(重点关注注释):

1
2
3
4
5
6
7
8
9
10
11
...
file(GLOB SOURCES "src/*.cpp")

# 使用根模块声明的 HEAD_PATH 字段, 指定头文件路径
include_directories(${HEAD_PATH})

# 使用根模块声明的 LIB_PATH 字段, 指定静态库输出路径
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

# 使用根模块声明的 LIB_CALC 字段, 指定静态库输出名称
add_library(${LIB_CALC} STATIC ${SOURCES})

其它子模块: sort/test1/test2 与 calc 模块类似, 都是使用了根模块声明的字段来配置各个子模块

接下来执行 cmake . -B build && cmake --build build 有以下日志:

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
27
28
29
30
31
-- The C compiler identification is AppleClang 15.0.0.15000100
-- The CXX compiler identification is AppleClang 15.0.0.15000100
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.9s)
-- Generating done (0.0s)
-- Build files have been written to: /Users/sep/Documents/cmake-tutorial/v5_nested_project/build
[ 8%] Building CXX object calc/CMakeFiles/calc.dir/src/add.cpp.o
[ 16%] Building CXX object calc/CMakeFiles/calc.dir/src/div.cpp.o
[ 25%] Building CXX object calc/CMakeFiles/calc.dir/src/mult.cpp.o
[ 33%] Building CXX object calc/CMakeFiles/calc.dir/src/sub.cpp.o
[ 41%] Linking CXX static library /Users/sep/Documents/cmake-tutorial/v5_nested_project/lib/libcalc.a
[ 41%] Built target calc
[ 50%] Building CXX object sort/CMakeFiles/sort.dir/src/insert.cpp.o
[ 58%] Building CXX object sort/CMakeFiles/sort.dir/src/select.cpp.o
[ 66%] Linking CXX shared library /Users/sep/Documents/cmake-tutorial/v5_nested_project/lib/libsort.dylib
[ 66%] Built target sort
[ 75%] Building CXX object test1/CMakeFiles/test1.dir/calc.cpp.o
[ 83%] Linking CXX executable /Users/sep/Documents/cmake-tutorial/v5_nested_project/bin/test1
[ 83%] Built target test1
[ 91%] Building CXX object test2/CMakeFiles/test2.dir/sort.cpp.o
[100%] Linking CXX executable /Users/sep/Documents/cmake-tutorial/v5_nested_project/bin/test2
[100%] Built target test2

此时项目里新增了各个子模块构建出来的产物:

1
2
3
4
5
6
7
.
├── bin
│   ├── test1
│   └── test2
├── lib
│   ├── libcalc.a
│   └── libsort.dylib

可以分别执行 bin/test1 和 bin/test2 查看效果

静态库链接静态库

源码参考: v6_static_link_static_lib

1
2
3
4
5
6
7
8
9
10
11
12
13
├── CMakeLists.txt
├── calc
│   ├── CMakeLists.txt
│   └── src
├── include
│   ├── calc.h
│   └── sort.h
├── sort
│   ├── CMakeLists.txt
│   └── src
└── test
├── CMakeLists.txt
└── main.cpp

这里是对 v5_nested_project 做的改造, 各个子模块间的依赖关系有:

test -> sort.a(静态库) -> calc.a(静态库)

各模块配置r(重点关注注释):

  • calc/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ...
    # 头文件路径
    include_directories(${HEAD_PATH})

    # 指定静态库输出位置
    set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

    # 指定生成静态库 calc
    add_library(${LIB_CALC} STATIC ${SOURCES})
  • sort/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ...
    # 头文件路径
    include_directories(${HEAD_PATH})

    # 链接静态库 calc
    link_libraries(${LIB_CALC})

    # 指定静态库输出位置
    set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

    # 指定生成静态库 sort
    add_library(${LIB_SORT} STATIC ${SOURCES})
  • sort/src/insert.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>
    #include "calc.h"
    #include "sort.h"

    // 这里包含静态库 calc 所声明的头文件, 正常调用函数就可以
    void sort_insert(int *arr, int len)
    {
    int s = add(1, 2);
    printf("sort_insert call add: s = %d\n", s);
    ...
    }
  • test/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ...
    # 头文件路径
    include_directories(${HEAD_PATH})

    # 链接库路径
    link_directories(${LIB_PATH})

    # 链接静态库 sort, 注意这里只需要链接 sort 就可以, sort 在构建时自动链接 calc
    link_libraries(${LIB_SORT})

    # 指定生成可执行文件 test
    add_executable(test ${SOURCES})

接下来执行 cmake . -B build && cmake --build build 便可以生成: 链接了静态库 sort (sort 链接了静态库 calc) 的可执行文件 bin/test

静态库链接动态库

源码参考: v7_static_link_shared_lib

这里是对 v6_static_link_static_lib 做的改造, 各个子模块间的依赖关系有:

test -> sort.a(静态库) -> calc.dylib(动态库)

配置静态库 sort 链接动态库 calc 主要有以下修改:

  • calc/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    # 指定头文件
    include_directories(${HEAD_PATH})

    # 指定动态库输出路径
    set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

    # 指定输出动态库 calc
    add_library(${LIB_CALC} SHARED ${SOURCES})
  • calc/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 指定头文件
    include_directories(${HEAD_PATH})

    # 指定静态库输出路径
    set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

    # 链接动态库文件路径
    link_directories(${LIB_PATH})

    # 指定输出静态库 sort
    add_library(${LIB_SORT} STATIC ${SOURCES})

    # 链接动态库
    target_link_libraries(${LIB_SORT} ${LIB_CALC})

接下来执行 cmake . -B build && cmake --build build 便可以生成: 链接了静态库 sort (sort 链接了动态库 calc) 的可执行文件 bin/test

CMake 常用指令

除了上面几个案例介绍的指令外, 可以参考 这里官网 对 cmake 指令有更全面的了解

CMake 与 Ninja

Ninja 也是一个构建系统, 使用 ninja 构建项目相比 makefile 可以更快, 同时也支持跨平台, Ninja 与 CMake 的关系可以参考下图:

Ninja

我们可以通过 cmake 生成 .ninja 文件, 然后再用 ninja 命令执行构建生成目标文件

使用 ninja, 首先需要在本地安装:

1
brew install ninja

上面的章节中所用到的命令 cmake . -B build 都是生成的 makefile 构建文件, 这其实是 cmake 工具的生成器 generators 概念

执行 cmake --help 可以查看到 cmake 的 generator 在本机系统支持生成哪些类型的构建文件

1
2
3
4
5
6
7
8
9
Generators

The following generators are available on this platform (* marks default):
* Unix Makefiles = Generates standard UNIX makefiles.
Ninja = Generates build.ninja files.
Ninja Multi-Config = Generates build-<Config>.ninja files.
Watcom WMake = Generates Watcom WMake makefiles.
Xcode = Generate Xcode project files.
...

接下来再以 v1_basic 为案例执行命令 cmake . -B build-ninja -GNinja, 可以看到在 build-ninja 目录下有:

1
2
3
4
├── CMakeCache.txt
├── CMakeFiles
├── build.ninja
└── cmake_install.cmake

以上是 cmake 生成的 ninja 相关构建文件, 我们可以 cd build-ninja && ninja 执行构建编译, 也可以执行 cmake --build build-ninja 命令来生成可执行文件 bin/app

这里再附贴一下 makefile 与 ninja 在编译速度上更直观的对比演示:

CMake 与 vcpkg

vcpkg 是 c/c++ 项目的依赖库管理工具, 可以帮助我们非常方便的管理三方库

下载 vcpkg:

1
git clone https://github.com/microsoft/vcpkg

配置环境变量:

1
2
export VCPKG_ROOT=[path-to-vcpkg]
export PATH="$VCPKG_ROOT:$PATH"

vcpkg-smaple

smaple 工程 为例, 创建 vcpkg 相关配置:

1
vcpkg new --application

vcpkg 会自动生成以下两个文件:

  • vcpkg-configuration.json 仓库配置
  • vcpkg.json 三方库依赖配置

配置 vcpkg.json 管理三方库:

1
2
3
4
5
6
7
{
"dependencies": [
"cxxopts",
"fmt",
"range-v3"
]
}

配置 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
cmake_minimum_required(VERSION 3.15)
set(CMAKE_CXX_STANDARD 17)

# 配置 cmake tookchain
set(
CMAKE_TOOLCHAIN_FILE $ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake
CACHE STRING "Vcpkg toolchain file"
)

project(fibonacci CXX)

# 查找三方库
find_package(fmt REQUIRED)
find_package(range-v3 REQUIRED)
find_package(cxxopts REQUIRED)

add_executable(fibo main.cxx)

# 链接三方库
target_link_libraries(fibo
PRIVATE
fmt::fmt
range-v3::range-v3
cxxopts::cxxopts)

执行 vcpkg install 安装依赖库, 此时工程有以下文件:

1
2
3
4
5
6
├── CMakeLists.txt
├── build
├── main.cxx
├── vcpkg-configuration.json
├── vcpkg.json
└── vcpkg_installed

至此, 可以对 main.cxx 正常引用三方库进行代码编写了, 执行下 cmake --build build 试试效果 😊😊

python bindings

接下我们使用 cmake + vcpkg + pybinding11 来实现 python native bindings 的实战案例, 源码参考: python-binding

同样配置 vcpkg.json 的依赖项:

1
2
3
4
5
{
"dependencies": [
"pybind11"
]
}

然后使用 vcpkg install 安装依赖库

在 CMakeFileLists.txt 中配置(重点参考注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 配置 cmake tookchain
set(
CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
CACHE STRING "Vcpkg toolchain file"
)

project(example LANGUAGES CXX)

set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

# 查找 pybind11 库
find_package(pybind11 REQUIRED)

# 配置头文件
include_directories($pybind11_INCLUDE_DIRS)

# pybind11_add_module 是找到 pybind11 后 pybind11Config.cmake 所定义的函数, 在这里配置源文件和输出模块
pybind11_add_module(example example.cpp)

example.cpp 中定义向 python 提供的 add() 方法, 声明模块宏 PYBIND11_MODULE(example, m):

1
2
3
4
5
6
7
8
9
10
11
#include <pybind11/pybind11.h>

namespace py = pybind11;

int add(int i, int j) {
return i + j;
}

PYBIND11_MODULE(example, m) {
m.def("add", &add, "A function which adds two numbers");
}

执行构建 cmake . -B build && cmake --build build, 然后进入到 lib 目录, 可以看到生成了对应的 native 库: example.cpython-311-darwin.so

执行 python 解释器, 可以看到成功调用到了 navive 库 add() 方法并返回计算结果:

1
2
3
4
5
6
7
8
/lib > python                                                                                        py base 21:58:59
Python 3.11.5 (main, Sep 11 2023, 08:31:25) [Clang 14.0.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>>
>>> example.add(1, 2)
3
>>>