[译]CMake 教程

这是官网的一篇教程,我在练习过程中保存了代码,可以在这里查看使用,官网也有源码的
地址,不过不是单独一个仓库。因为琐碎明白 Step 7 的作用,没有测试这一步的用法。
原文地址为:https://cmake.org/cmake-tutorial/

下面是一个循序渐进地教程,涵盖了 CMake 是怎么帮助解决常见的构建系统问题的。这里
的许多主题已经在Mastering CMake中作为单独的问题讨论过了,但看看它们怎么在一个
例子工程中协同工作仍会很有帮助。本教程可以在 CMake 源码树的Tests/Tutorial目录中找到。
每一步都有它自己的子目录,里面包含了那一步中用到的完整代码。

一个简单的开始(Step 1)

一个最基本的项目是由若干源文件构建出一个可执行文件。对简单的项目来说,一个两行的
CMakeLists.txt 文件就够了。这将是本教程的起点。CMakeLists.txt 文件的内容为:

1
2
3
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)

注意这个例子在 CMakeLists.txt 文件中使用了小写的命令。无论是大写、小写和大小写混
合 CMake 都支持。tutorial.cxx 程序会计算一个数的平方根,第一版很简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
double outputValue = sqrt(inputValue);
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}

添加版本号并配置头文件

我们要添加的第一个特性是为项目和可执行文件提供一个版本号。虽然你可以专门在源代码
中做这项工作,但在 CMakeLists.txt 文件中提供的话会更灵活。像下面这样修改来添加版
本号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)

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

# add the executable
add_executable(Tutorial tutorial.cxx)

由于配置文件会被写入二进制树,我们必须把该目录添加到搜索被包含文件的路径列表。然
后我们在源码树中创建一个 TutorialConfig.h.in 文件,内容如下:

1
2
3
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

当 CMake 配置这个头文件时,@Tutorial_VERSION_MAJOR@@Tutorial_VERSION_MINOR@
的值会被 CMakeLists.txt 中的值替换。下面我们修改tutorial.cxx 来包含配置头文件,
让它用上版本号。修改后的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"

int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"%s Version %d.%d\n",
argv[0],
Tutorial_VERSION_MAJOR,
Tutorial_VERSION_MINOR);
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
double outputValue = sqrt(inputValue);
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}

主要的改动是包含了 TutorialConfig.h 头文件,并把版本号作为使用方式的一部分打印了
出来。

添加库(Step 2)

现在我们向项目添加一个库。这个库会包含我们自己的计算平方根的实现。可执行文件就可
以使用这个库而不是使用编译器提供的标准平方根函数。在这个教程里我们会把库放在名为
MathFunctions 的子目录里。它将包含一个只有如下一行的 CMakeLists.txt 文件:

1
add_library(MathFunctions mysqrt.cxx)

源文件 mysqrt.cxx 有一个名为 mysqrt 的函数,提供了与编译器的 sqrt 函数相似的功能。
为了使用这个新库,我们在顶层 CMakeLists.txt 文件里添加一个 add_subdirectory
令,这样这个库就会被构建了。我们还增加了另一个 include 目录,这样就能找到头文件
MathFunctions/MathFunctions.h 及函数原型。最后一个改动是把新库添加到可执行里。现
在顶层 CMakeLists.txt 文件最后几行为:

1
2
3
4
5
6
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)

# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial MathFunctions)

现在我们考虑使 MathFunctions 库成为可选的。在这个教程里确实没有理由这样做,但如
果是更大的库或者依赖于第三方代码的库的话,我们可能会想这么做。首先要在顶层
CMakeLists.txt 文件里添加一个选项(option)。

1
2
3
# should we use our own math functions?
option (USE_MYMATH
"Use tutorial provided math implementation" ON)

在图形界面的 CMake 下这会显示为默认值为 ON 而用户可以随意更改。这个设置会被保存
在缓存里,这样用户就不需要每次运行 CMake 就设置这个值了(译注:这里应该是说图形
界面下)。其次要进行的改动是使编译和链接 MathFunctions 库成为可选的。将顶层
CMakeLists.txt 最后修改成下面这样以达到此目的:

1
2
3
4
5
6
7
8
9
10
11
# add the MathFunctions library?
#
if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial ${EXTRA_LIBS})

这里使用 USE_MYMATH 的设置来确定是否编译和使用 MathFunctions。注意使用变量(这里
是 EXTRA_LIBS 收集可选的库在稍后链接进可执行文件的用法。这是保持有许多可选部分
的大项目整洁的常用方法。源代码需要做的相应修改就非常直观了:

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
32
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"%s Version %d.%d\n", argv[0],
Tutorial_VERSION_MAJOR,
Tutorial_VERSION_MINOR);
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}

double inputValue = atof(argv[1]);

#ifdef USE_MYMATH
double outputValue = mysqrt(inputValue);
#else
double outputValue = sqrt(inputValue);
#endif

fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}

在源代码里我们也使用 USE_MYMATH。CMake 通过在 TutorialConfig.h.in 里加入以下内
容来实现:

1
#cmakedefine USE_MYMATH

安装和测试(Step 3)

在这一步中我们将为我们的项目添加安装规则和测试支持。安装规则十分地直接。对于
MathFunctions 要安装的库和头文件来说,需要添加以下两行到 MathFunctions 的
CMakeLists.txt 文件中:

1
2
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

对于可执行文件和配置头文件来说,需要添加以下几行到顶层的 CMakeLists.txt 文件中。

1
2
3
4
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include)

这就可以了。至此你应该可以构建这个项目,可以使用 make install(或者在 IDE 中构建
INSTALL 目标)来安装相应的头文件,库和可执行文件。CMake 变量
CMAKE_INSTALL_PREFIX用来指定文件要安装的根目录。添加测试同样也是一个很直接的过
程。我们可以在顶层 CMakeLists.txt 文件的最后添加一些测试来验证程序是否正确工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
include(CTest)

# does the application run
add_test (TutorialRuns Tutorial 25)

# does it sqrt of 25
add_test (TutorialComp25 Tutorial 25)
set_tests_properties (TutorialComp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")

# does it handle negative numbers
add_test (TutorialNegative Tutorial -25)
set_tests_properties (TutorialNegative PROPERTIES PASS_REGULAR_EXPRESSION "-25 is 0")

# does it handle small numbers
add_test (TutorialSmall Tutorial 0.0001)
set_tests_properties (TutorialSmall PROPERTIES PASS_REGULAR_EXPRESSION "0.0001 is 0.01")

# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")

在构建之后就可以运行命令行工具“ctest”来运行测试了。第一个测试仅仅验证程序能够运
行,不会出现段错误或其他类型的崩溃,且返回值为 0. 这是一个 CTest 测试的基本形式。
下面几个测试都使用了PASS_REGULAR_EXPRESSION测试属性来验证测试的输出包含特定的
字符串。其中验证计算出的平方根是应该做的且使用信息会在给了错误的参数个数时打印。
如果你想添加很多测试来测试不同的输入值,你可以考虑新建一个如下的宏:

1
2
3
4
5
6
7
8
9
10
#define a macro to simplify adding tests, then use it
macro (do_test arg result)
add_test (TutorialComp${arg} Tutorial ${arg})
set_tests_properties (TutorialComp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)

# do a bunch of result based tests
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")

每次调用do_test,一个测试就会加入到项目,新的测试的名字,输入和输入会根据传入
的参数确定。

添加系统依赖(Step 4)

接下来我们考虑为我们的项目添加目标平台可能没有相关特性的代码。在这个例子中我们会
添加一些依赖于目标平台是否有 log 和 exp 函数的代码。当然几乎所有的平台都有这些函
数,但这里我们假设它们没有这么常见。如果平台有 log 函数,我们就在 mysqrt 函数里
用它计算平方根。首先在顶层的 CMakeLists.txt 里我们用 CheckFunctionExists 宏来测
试这些函数是否可用:

1
2
3
4
# does this system provide the log and exp functions?
include (CheckFunctionExists)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

然后我们修改 TutorialConfig.h.in 使得如果 CMake 在目标平台上找到了它们就定义这些
值:

1
2
3
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

使测试 log 和 exp 在配置 TutorialConfig.h 文件的configure_file命令之前进行很重
要。CMake 在遇到configure_file命令时会立刻进行文件配置。最后在我们的 mysqrt 函
数里,我们可以根据系统是否有 log 和 exp 来提供不同的实现:

1
2
3
4
5
// if we have both log and exp then use them
#if defined (HAVE_LOG) && defined (HAVE_EXP)
result = exp(log(x)*0.5);
#else // otherwise use an iterative approach
. . .

添加生成文件和生成器(Step 5)

这一节里我们将展示如何添加一个生成的源文件到程序的构建过程中。在这个例子中我们将
新建一个到构建过程中,里面包含了一些计算过的平方根值,并将这个表也编译进程序。要
做到这些我们就需要一个程序来生成这个表。在 MathFunctions 子目录下添加一个做这个
工作的源文件。

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
32
33
34
35
36
// A simple program that builds a sqrt table
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main (int argc, char *argv[])
{
int i;
double result;

// make sure we have enough arguments
if (argc < 2)
{
return 1;
}

// open the output file
FILE *fout = fopen(argv[1],"w");
if (!fout)
{
return 1;
}

// create a source file with a table of square roots
fprintf(fout,"double sqrtTable[] = {\n");
for (i = 0; i < 10; ++i)
{
result = sqrt(static_cast<double>(i));
fprintf(fout,"%g,\n",result);
}

// close the table with a zero
fprintf(fout,"0};\n");
fclose(fout);
return 0;
}

注意这张表是合法的 C++ 代码,输出文件名是作为参数传入的。下一步要做的就是在
MathFunctions 的 CMakeLists.txt 文件中添加适当的命令来构建 MakeTable 的可执行文
件,并把它作为构建过程的一部分运行。要完成此任务的命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)

# add the command to generate the source code
add_custom_command (
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)

# add the binary tree directory to the search path for
# include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )

# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h )

首先 MakeTable 的可执行文件像其他的可执行文件一样被添加。然后我们添加一个用户命
令指定了怎样运行 MakeTable 生成 Table.h。下一步我们还要让 CMake 知道 mysqrt.cxx
依赖于生成的文件 Table.h。这可以靠把生成的 Table.h 添加到 MathFunctions 库的源文
件列表来达到。我们还需要把当前可执行文件目录(current binary directory)添加到
include 目录表,这样 Table.h 可以被 mysqrt.cxx 找到并包含。当这个项目被构建时,
它会首先建立 MakeTable 可执行文件。然后运行 MakeTable 来产生 Table.h。最后,它会
编译 mysqrt.cxx(包含了 Table.h)来生成 MathFunctions 库。此时包含我们添加的所有特
性的顶层 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
include(CTest)

# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)

check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

# should we use our own math functions
option(USE_MYMATH
"Use tutorial provided math implementation" ON)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)

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

# add the MathFunctions library?
if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial ${EXTRA_LIBS})

# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include)

# does the application run
add_test (TutorialRuns Tutorial 25)

# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
PROPERTIES
PASS_REGULAR_EXPRESSION "Usage:.*number"
)


#define a macro to simplify adding tests
macro (do_test arg result)
add_test (TutorialComp${arg} Tutorial ${arg})
set_tests_properties (TutorialComp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endmacro (do_test)

# do a bunch of result based tests
do_test (4 "4 is 2")
do_test (9 "9 is 3")
do_test (5 "5 is 2.236")
do_test (7 "7 is 2.645")
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
do_test (0.0001 "0.0001 is 0.01")

TutorialConfig.h.in 如下:

1
2
3
4
5
6
7
8
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH

// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

MathFunctions 中的 CMakeLists.txt 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
# add the binary tree directory to the search path
# for include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )

# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)

install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

建立安装器(Step 6)

现在假设我们想要把我们的项目发布给其他人使用。我们想为一众平台提供包括源码和二进
制文件。这和我们前面小节(Step 3)中所做的不太一样,那时我们安装的是从源码构建的二
进制文件。在这个例子中我们会建立一个安装包,它和 cygwin,debian,RPMs 等一样支持
二进制安装和包管理特性。要达成这个目标我们会用到 CPack 来创建平台相关的安装器,
像 Packaging with CPack 一章中描述的一样。我们需要在顶层 CMakeLists.txt 文件中添
加几行:

1
2
3
4
5
6
7
# build a CPack driven installer package
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE
"${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include (CPack)

这样就行了。我们先包含了 InstallRequiredSystemLibraries。这个模块会包含了这个项
目在当前平台上需要的所有运行时库。然后我们设置了一些 CPack 变量,指明了我们在哪
儿保存了这个项目许可证和版本信息。版本信息使用了我们早些在教程中设置的变量。最后
我们包含了 CPack 模块,它会使用这些变量还有其他一些当前系统的属性来设置一个安装
器。

下一步就是用平常的方式构建项目,再在之上运行 CPack。要创建一个二进制发布包你可以
运行如下命令:

1
cpack --config CPackConfig.cmake

要创建一个源码包可以运行:

1
cpack --config CPackSourceConfig.cmake

添加 Dashboard 支持(Step 7)

添加支持提交测试结果到一个 dashboard 很简单。在教程前面的步骤中我们已经定义了一
些测试。我们只需要运行这些测试并提交到 dashboard 就行了。要包含 dashboard 支持我
们需要在我们的顶层 CMakeLists.txt 文件中包含 CTest 模块。

1
2
# enable dashboard scripting
include (CTest)

我们还创建了一个 CTestConfig.cmake 文件,我们可以在其中为 dashboard 指定项目名称。

1
set (CTEST_PROJECT_NAME "Tutorial")

CTest 会在运行时读取这个文件。要建立一个简单的 dashboard 你可以在你的项目上运行
CMake,进入到二进制树所在目录,再运行 ctest -D Experimental。你的 dashboard 的结
果会被上传到 Kitware 的公共 dashboard

0%