0%

CMake 简便手册

简介

这个主要参考CMake官网的手册整理而成。 一步一步阐述了如何使用CMake构建一个工程。详细参考Mastering CMake

所有的源码在下载的CMake软件包里面的Tests/Tutorial文件夹。

第一步 开始吧

大部分的项目都是从源码进行编译生成可执行程序的。
对于最简单的工程,包含两行代码的CMakeLists.txt文件就可以搞定。
下面就开始我们的第一个小目标,内容如下所示:

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

从上面的例子看到,cmake的关键词都是小写的,其实大写、小写或者大小写混合,CMake都是支持的。

下面的代码是一个比较简单的计算平方根的小程序。

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)

从上面的内容可以看到我们需要的头文件在binary目录,所以我们需要把该目录包含进来。

接下来我们创建一个文件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文件中的值。

接下来,我们修改源码来使用这个版本号。源码如下所示。

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;
}

第二步 添加一个库

接下里我们给项目添加一个库。这个库包含我们自己实现的一个求平方根的算法。随后我们就使用这个库而不用系统自带的库。在这个示例中,我们把这个库放在子文件夹MathFunctions中,并在CMakeLists.txt文件中将添加下面一行

1
add_library(MathFunctions mysqrt.cxx)

源码文件mysqrt.cxx中包含了一个类似求平方根的功能。
为了使用这个新的库,我们在顶级目录的CMakeLists.txt文件中添加add_subdirectory调用,这样就可以在编译的时候使用该库。
同时我们也添加了一个头文件包含目录,用来使用头文件中的原型定义。
最后新添加的几行内容如下所示:

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)

接下来我们把这个库设置成可选的。虽然在这个示例中没有必要,不过对于其他依赖于第三方库的软件,这个就很有必要的。

第一步就是在顶级目录的 CMakeLists.txt 文件中添加option参数

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

上面的option选项ON就使能了参数USE_MYMATH。

为了在编译连接时使用到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。我们可以通过在文件TutorialConfig.h.in中添加下面一行来搞定。

1
#cmakedefine USE_MYMATH

第三步 安装和测试

下一步我们将增加项目的安装和测试。

只要增加下面两行命令就可以将库文件和头文件安装到指定位置。

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

然后在顶级目录加上下面两句话就可以安装可执行文件和配置头文件了。

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

这些就是我们需要做的全部工作了,此时我们就可以编译这个项目了,然后输入make install就可以安装相应的头文件、库和可执行文件了。

CMAKE中的变量CMAKE_INSTALL_PREFIX用于指定文件安装的根路径。

增加测试也是很多程序都要做的,下面的几步用于测试程序能够正常运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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命令来运行这些测试了。

第一个简单的例子主要用于检测程序是否出现段错误或者其他bug,有一个0值返回。这是CTest测试的基本框架。
接下来的几个测试使用了参数PASS_REGULAR_EXPRESSION用来检测输出是否包含特定字符串。在这个示例中,如果测试的结果没有包含相应的字符,将会输出错误信息。

如果希望做很多不同输入的测试,可以考虑定义一个宏来完成。

1
2
3
4
5
6
7
8
9
#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")

第四部 增加系统Introspection

接下来我们增加一些代码用来解决依赖于特定系统的特性。在这个示例中我们测试系统是否包含log和exp函数。当然大部分操作系统都具有。如果操作平台有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

这里需要特别注意的是这些测试一定要在configure_file命令之前进行,因为configure_file命令会立即使用当前的一些设置。最后我们得到了一个没有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
. . .

第五步 增加一个Generated File and Generator

这个步骤里面我们将展示如何在编译应用程序的过程中添加一个源文件。在这个例子里面我们将在编译过程中创建一个表,然后把该表编译进我们的程序。
首先我们需要生成该表的程序,在MathFunctions子目录我们创建一个新的文件MakeTable.cpp

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;
}

添加下面的几行命令来完成

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

# 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 )

根目录的文件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
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)

第六步 构建一个安装包

这一步假定我们需要把我们的项目打包给其他人来使用。我们希望能够提供多个系统版本的二进制和源码包。
与直接编译源码不同,这里我们使用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即可。

如果希望构建二进制版本,执行命令:

1
cpack --config CPackConfig.cmake

如果希望创建源码版本,执行命令:

1
cpack --config CPackSourceConfig.cmake
处无为之事,行不言之教;作而弗始,生而弗有,为而弗恃,功成不居!

欢迎关注我的其它发布渠道