CMake Tutorial

翻译自cmake-tutorial. 注意:翻译不一定准确,内容仅供参考!

A Basic Starting Point (Step1)

一个最基本的项目是由一些源代码文件构建得到的可执行文件,对于一个简单的项目,只需要一个三行的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
// 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;
}

Adding a Version Number and Configured Header File

这里要添加的第一个特性是为可执行文件和项目提供一个版本号,虽然这可以在源代码中完成,但在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 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)

由于配置文件将被写入二进制树(binary tree),所以必须将该文件路径添加到搜索路径中以包含这些文件。然后在源代码树(source tree)中创建一个包含以下内容的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的包含,并将版本号作为使用信息的一部分输出到屏幕。

Adding a Library (Step 2)

现在需要添加一个库到项目中,这个库包含了平方根计算的实现,可执行文件便可以使用这个库,而不去使用编译器提供的平方根计算函数。本教程将把这个库放在一个叫MathFunctions的子文件夹中,CMakeLists.txt文件需要有下面这样一行:

1
add_library(MathFunctions mysqrt.cxx)

源文件mysqrt.cxx有一个叫mysqrt的函数,拥有跟编译器自带的sqrt函数同样的功能,为了使用这个新库,需要在CMakeLists.txt文件的上一层添加add_subdirectory的调用,以让该库得到构建。此外还添加一个包含路径来让该函数能够找到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文件的前面添加一个选项。

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

这个选项将在CMake GUI中出现,且默认值为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

Installing and Testing (Step 3)

接下来添加安装规则(Install Rules)和测试(Testing)支持到项目中,安装规则非常直接明了,对于MathFunctions库,可以通过在MathFunctionsCMakeLists.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
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便可以运行所有测试,第一个测试验证程序是否能够正常运行,不会出现段错误(segfault)或者崩溃(crash),能够返回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的每一次调用,一个新的测试便被添加到项目中,名字、输入、结果便是传入的相关参数。

Adding System Introspection (Step 4)

下面考虑向项目中添加一些代码,而这些代码取决于目标平台可能没有的特性。对于这个例子,这里将根据目标平台是否有logexp函数来添加一些代码,当然,几乎所有的平台都包含这两个函数,但这个教程假设目标平台不是常见的那些。如果平台有log函数,则直接在mysqrt函数中使用这个函数计算平方根。这里首先在CMakeLists.txt文件中利用CheckFunctionExists.cmake宏来测试这些函数的可用性:

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

很重要的一点,函数logexp的可用性测试需要在TutorialConfig.h文件的configure_file命令调用前完成,configure_file命令立即在CMake中用当前的设置配置文件。最后在mysqrt函数中添加一个基于logexp函数的替代实现,如果系统可以获取这两个函数,具体代码如下:

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
. . .

Adding a Generated File and Generator (Step 5)

这一小节介绍怎样在一个应用程序的构建过程中添加生成的源代码,这个例子将创建一个表,表中存储一些预先计算好的平方根作为构建过程的一部分,然后将这个表编译进应用程序。首先需要一个程序来生成这个表,在MathFunctions子文件夹,一个叫MakeTable.cxx的新的源文件会完成这个工作:

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++代码生成的,输出文件的文件名由传入的参数确定。接下来要添加合适的命令到MathFunctionsCMakeLists.txt文件中来构建MakeTable可执行文件,然后将其作为构建过程的一部分。下面是完成这些所需要的一些命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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的可执行文件跟其他可执行文件的添加方式一样,然后添加一个自定义命令(custom command)来具体说明怎样通过运行MakeTable来生成Table.h,接下来必须让CMake知道mysqrt.cxx依赖于生成的Table.h文件,具体通过将生成的Table.h文件添加到MathFunctions库的源文件代码列表中完成。此外还必须将当前的二进制路径添加到包含文件的列表中,以让Table.h文件能够被mysqrt.cxx找到并包含。当这个项目被构建时,首先会构建MakeTable可执行文件,然后运行MakeTable生成Table.h,最后再编译包含有Table.hmysqrt.cxx来生成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

MathFunctionsCMakeLists.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)

Building an Installer (Step 6)

接下来假设需要将项目发布给其他人使用,需要在众多平台提供二进制和源代码发布包,这和在前面第三节讲的安装从源代码构建得到的二进制文件还有一点区别。这个例子将讲解如何构建安装包,并支持二进制安装和包管理这些在cygwindebianRPMs等平台都拥有的特性。为了达到这个目的,这里使用CPack来生成各个平台的安装程序,在Chapter 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,这个模块将包含项目在当前平台所需要的所有运行库(runtime libraries),接下来设置一些CPack变量,包括许可协议和项目的版本信息,版本信息利用教程前面设置的变量。最后再包含CPack模块,CPack将使用这些变量和当前系统的一些特性来设置安装程序。

下一步便使用通常的方法构建项目,然后运行CPack。要构建一个二进制发布包(binary distribution)使用如下命令:

1
cpack --config CPackConfig.cmake

要生成一个源代码发布包(source distribution)需要输入如下命令:

1
cpack --config CPackSourceConfig.cmake

Adding Support for a Dashboard (Step 7)

增加提交测试结果到仪表板(dashboard)的支持非常简单,前面的教程已经在项目中定义了许多测试,现在只需运行这些测试然后提交到仪表板。为了添加仪表板的支持,首先需要在CMakeLists.txt文件的前面包含CTest模块:

1
2
# enable dashboard scripting
include (CTest)

然后创建一个CTestConfig.cmake文件,并在里面设置这个项目在仪表板中的名字:

1
set (CTEST_PROJECT_NAME "Tutorial")

CTest在运行时会读入这个文件。如果想创建一个简单的仪表板,可以在项目中运行CMake,然后将路径切换到二进制树目录,运行ctest –D Experimental命令,仪表板的结果会被上传到Kitware公共仪表板

参考