CMake

1. 构建最小项目

1
2
3
4
5
6
7
8
# 使用 CMake 的最小版本号
cmake_minimum_required(VERSION 3.15)

# set the project name
project(Tutorial)

# add the executable
add_executable(Tutorial tutorial.cpp)

注意,此示例在 CMakeLists.txt 中使用小写命令。CMake 支持大写/小写/混合大小写命令。

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
// tutorial.cpp
// 用于计算数字的平方根

#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>

int main(int argc, char* argv[])
{
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

// convert input to double
const double inputValue = atof(argv[1]);

// calculate square root
const double outputValue = sqrt(inputValue);
std::cout << "The square root of " << inputValue
<< " is " << outputValue
<< std::endl;
return 0;
}

1.1 构建、编译、运行

进入到 step1 目录,并创建一个构建目录 build,接下来,进入 build 目录并运行 CMake 来配置项目,并生成构建系统:

1
2
3
mkdir build
cd build
cmake -G"MinGW Makefiles" ..

构建系统是需要指定 CMakeLists.txt 所在路径,此时在 build 目录下,所以用 .. 表示 CMakeLists.txt 在上一级目录。

Windows 下,CMake 默认使用微软的 MSVC 作为编译器,我想使用 MinGW 编译器,可以通过 -G 参数来进行指定,只有第一次构建项目时需要指定。

此时在 build 目录下会生成 Makefile 文件,然后调用编译器来实际编译和链接项目:

1
cmake --build .

--build 指定编译生成的文件存放目录,其中就包括可执行文件,. 表示存放到当前目录,

在 build 目录下生成了一个 Tutorial.exe 可执行文件,试着执行它:

1
2
> Tutorial.exe 5
The square root of 5 is 2.23607

此时的目录结构为:

1
2
3
4
step1/
build/
CMakeLists.txt
tutorial.cpp

1.2 外部构建与内部构建

这里创建了一个 build 目录存放编译产物,可以避免编译产物与代码文件混在一起,这种叫做外部构建。

还有一种内部构建,即直接在项目根目录下进行构建系统与编译,这时构建和编译命令就更改为:

1
2
cmake -G"MinGW Makefiles" .
cmake --build .

内部构建会使得项目文件很混乱,一般直接用外部构建即可。

2. 优化 CMakeList.txt 文件

2.1 set 与 PROJECT_NAME

这是之前见过的 CMakeLists.txt 文件:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.15)

# set the project name
project(Tutorial)

# add the executable
add_executable(Tutorial tutorial.cpp)

指定了项目名后,后面可能会有多个地方用到这个项目名,如果更改了这个名字,就要改多个地方,比较麻烦,那么可以使用 PROJECT_NAME 来表示项目名。

1
add_executable(${PROJECT_NAME} tutorial.cpp)

生成可执行文件需要指定相关的源文件,如果有多个,那么就用空格隔开,比如:

1
add_executable(${PROJECT_NAME} a.cpp b.cpp c.cpp)

我们也可以用一个变量来表示这多个源文件:

1
2
set(SRC_LIST a.cpp b.cpp c.cpp)
add_executable(${PROJECT_NAME} ${SRC_LIST})

set 命令指定 SRC_LIST 变量来表示多个源文件,用 ${var_name} 获取变量的值。

于是原来的 CMakeLists.txt 文件就可以变成如下所示:

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.15)

# set the project name
project(Tutorial)

SET(SRC_LIST tutorial.cpp)

# add the executable
add_executable(${PROJECT_NAME} ${SRC_LIST})

2.3 添加版本号和配置头文件

我们可以在 CMakeLists.txt 为可执行文件和项目提供一个版本号。首先,修改 CMakeLists.txt 文件,使用 project 命令设置项目名称和版本号。

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.15)

# set the project name and version
project(Tutorial VERSION 1.0.2)

configure_file(TutorialConfig.h.in TutorialConfig.h)

然后,配置头文件将版本号传递给源代码:

1
configure_file(TutorialConfig.h.in TutorialConfig.h)

由于 TutorialConfig.h 文件这里被设置为自动写入 build 目录,因此需要将该目录添加到搜索头文件的路径列表中,也可以修改为写到其它目录。

将以下行添加到 CMakeLists.txt 文件的末尾:

1
2
3
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
)

PROJECT_BINARY_DIR 表示当前工程的二进制路径,即编译产物会存放到该路径,此时PROJECT_BINARY_DIR 就是 build 所在路径。

然后手动创建 http://TutorialConfig.h.in 文件,包含以下内容:

1
2
3
4
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define Tutorial_VERSION_PATCH @PROJECT_VERSION_PATCH@

当使用 CMake 构建项目后,会在 build 中生成一个 TutorialConfig.h 文件,内容如下:

1
2
3
4
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR 1
#define Tutorial_VERSION_MINOR 0
#define Tutorial_VERSION_PATCH 2

下一步在 tutorial.cpp 包含头文件 TutorialConfig.h,最后通过以下代码打印出可执行文件的名称和版本号。

1
2
3
4
5
6
7
if (argc < 2) {
// report version
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

2.3 添加时间戳

有时候我们需要知道编译时的时间戳,并在程序运行时打印出来。

那就需要在 CMakeLists.txt 中添加如下这句:

1
string(TIMESTAMP COMPILE_TIME %Y%m%d-%H%M%S)

这表示将时间戳已指定格式保存到 COMPILE_TIME 变量中。

然后修改上面的 http://TutorialConfig.h.in 文件:

1
2
3
4
5
6
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define Tutorial_VERSION_PATCH @PROJECT_VERSION_PATCH@

#define TIMESTAMP @COMPILE_TIME@

在构建项目后,TutorialConfig.h 文件就会自动增加一句:

1
#define TIMESTAMP 20230220-203532

这样就可以在源码中打印出 TIMESTAMP 的值了。

2.4 指定C++标准

接下来将 step1/tutorial.cpp 源码中的 atof 替换为 std::stod,这是 C++11 的特性,并删除 #include<cstdlib>

1
const double inputValue = std::stod(argv[1]);

在 CMake 中支持特定 C++标准的最简单方法是使用 CMAKE_CXX_STANDARD 标准变量。在 CMakeLists.txt 中设置 CMAKE_CXX_STANDARD 为11,CMAKE_CXX_STANDARD_REQUIRED 设置为True。确保在 add_executable 命令之前添加 CMAKE_CXX_STANDARD_REQUIRED 命令。

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.15)

# set the project name and version
project(${PROJECT_NAME} VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

需要注意的是,如果你的gcc编译器版本够高,也可以不用指定 C++ 版本为 11。从 GCC 6.1 开始,当不指定任何版本 C++ 标准时,默认版本是 C++ 14,如果你想用 C++17 的语言,还是需要指定的。

修改完成后,需要对代码进行重新编译 cmake --build .,此时可以不用进行项目构建。

此时目录结构为:

1
2
3
4
5
step2/
build/
CMakeLists.txt
tutorial.cpp
TutorialConfig.h.in

3. 添加库

现在我们将向项目中添加一个库,这个库包含计算数字平方根的实现,可执行文件使用这个库,而不是编译器提供的标准平方根函数。

我们把库放在名为 MathFunctions 的子目录中。此目录包含头文件 MathFunctions.h 和源文件 mysqrt.cpp。源文件有一个名为 mysqrt 的函数,它提供了与编译器的 sqrt 函数类似的功能,MathFunctions.h 则是该函数的声明。

在 MathFunctions 目录下创建一个 CMakeLists.txt 文件,并添加以下一行:

1
2
# MathFunctions/CMakeLists.txt
add_library(MathFunctions mysqrt.cpp)

表示添加一个叫 MathFunctions 的库文件。

CMake 中的 target 有可执行文件和库文件,分别使用 add_executableadd_library 命令生成,除了指定生成的可执行文件名/库文件名,还需要指定相关的源文件。

此时文件结构为:

1
2
3
4
5
6
7
8
9
step3/
build/
MathFunctions/
CMakeLists.txt
MathFunctions.h
mysqrt.cpp
CMakeLists.txt
tutorial.cpp
TutorialConfig.h.in

为了使用 MathFunctions 这个库,我们将在顶级 CMakeLists.txt 文件中添加一个 add_subdirectory(MathFunctions) 命令指定库所在子目录,该子目录下应包含 CMakeLists.txt 文件和代码文件。

可执行文件要使用库文件,需要能够找到库文件和对应的头文件,可以分别通过 target_link_librariestarget_include_directories 来指定。

使用 target_link_libraries 将新的库文件添加到可执行文件中,使用 target_include_directories 将 MathFunctions 添加为头文件目录,添加到 Tutorial 目标上,以便 mysqrt.h 可以被找到。

顶级 CMakeLists.txt 的最后几行如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(${PROJECT_NAME} tutorial.cpp)

target_link_libraries(${PROJECT_NAME} PUBLIC MathFunctions)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${PROJECT_SOURCE_DIR}/MathFunctions
)

MathFunctions 库就算添加完成了,接下来就是在主函数使用该库中的函数,先在 tutorial.cpp 文件中添加头文件:

1
#include "MathFunctions.h"

然后使用 mysqrt 函数即可:

1
const double outputValue = mysqrt(inputValue);

4. 库设置为可选项

现在将 MathFunctions 库设为可选的,虽然对于本教程来说,没有必要这样做,但对于较大的项目来说,这种情况很常见。

第一步是向顶级 CMakeLists.txt 文件添加一个选项。

1
option(USE_MYMATH "Use tutorial provided math implementation" ON)

option 表示提供用户可以选择的选项。命令格式为:option(<variable> "description [initial value])

USE_MYMATH 这个选项缺省值为 ON,用户可以更改这个值。此设置将存储在缓存中,以便用户不需要在每次构建项目时设置该值。

下一个更改是使 MathFunctions 库的构建和链接成为条件。于是创建一个 if 语句,该语句检查选项 USE_MYMATH 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES ${PROJECT_SOURCE_DIR}/MathFunctions)
endif()

# add the executable
add_executable(${PROJECT_NAME} tutorial.cpp)

target_link_libraries(${PROJECT_NAME} PUBLIC ${EXTRA_LIBS})

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${EXTRA_INCLUDES}
)

在 if 块中,有 add_subdirectory 命令和 list 命令,APPEND表示将元素MathFunctions追加到列表EXTRA_LIBS中,将元素 ${PROJECT_SOURCE_DIR}/MathFunctions 追加到列表EXTRA_INCLUDES中。EXTRA_LIBS 存储 MathFunctions 库,EXTRA_INCLUDES 存储 MathFunctions 头文件。

变量EXTRA_LIBS用来保存需要链接到可执行程序的可选库,变量EXTRA_INCLUDES用来保存可选的头文件搜索路径。这是处理可选组件的经典方法,我将在下一步介绍现代方法。

接下来对源代码的进行修改。首先,在 tutorial.cpp 中包含 MathFunctions.h 头文件:

1
2
3
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

然后,还在 tutorial.cpp 中,使用 USE_MYMATH 选择使用哪个平方根函数:

1
2
3
4
5
#ifdef USE_MYMATH
const double outputValue = mysqrt(inputValue);
#else
const double outputValue = sqrt(inputValue);
#endif

因为源代码使用了 USE_MYMATH 宏,可以用下面的行添加到 tutorialconfig.h.in 文档中:

1
2
// TutorialConfig.h.in
#cmakedefine USE_MYMATH

现在使用 cmake 命令构建项目,并运行生成的 Tutorial 可执行文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
build> cmake -G"MinGW Makefiles" ..
build> cmake --build .
build> Tutorial.exe 8
Computing sqrt of 8 to be 4.5
Computing sqrt of 8 to be 3.13889
Computing sqrt of 8 to be 2.84378
Computing sqrt of 8 to be 2.82847
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
The square root of 8 is 2.82843

默认调用 mysqrt 函数,也可以在构建项目时指定 USE_MYMATH 的值为 OFF:

1
2
> cmake -DUSE_MYMATH=OFF ..
> cmake --build .

此时会调用自带的 sqrt 函数。

5. 添加库的使用要求