CMake配置记录

CMake是一个代码项目管理工具。最开始是为了将项目VTK兼容于VS的环境下,生成VS的工程文件而开发的工具,后来随着应用愈广,演化为独立的代码项目管理工具。

相较于automake的工程配置,cmake的输出显得更加规范格式化。编译过程中的进度百分比的显示,对于长时间的编译过程,对编译进度具备心理预期,更好的规划时间安排。

生成Makefile

CMake的配置文件是CMakeList.txt,可以通过cmake命令指定src目录,然后在当前目录下生成Makefile文件。

1
2
3
4
mkdir build/
cd build/
cmake ../src
make

构建可执行程序

最简单的demo,指定cmake版本、工程名字、生成程序。

CMakeList.txt:

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

C源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
# CMakeLists.txt中配置宏
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
 
# 配置头文件,传递CMakeLists.txt中的配置宏
configure_file (
  "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
  "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  )
 
# 指定TutorialConfig.h头文件目录
include_directories("${PROJECT_BINARY_DIR}")
 
# add the executable
add_executable(Tutorial tutorial.cxx)

然后在PROJECT_SOURCE_DIR目录下创建一个TutorialConfig.h.in文件,内容如下:

1
2
#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
#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;
}

调试

设置调试的命令

1
2
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb ")
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

cmake命令

1
cmake -DCMAKE_BUILD_TYPE=Debug/Release ./src 

添加链接库文件和链接路径

1
2
3
4
5
6
7
8
9
10
11
12
#添加库搜索路径,影响后面定义的目标
link_directories(${PROJECT_SOURCE_DIR}/libs)

#目录级链接库,影响后面定义的目标
link_libraries(mylib pthread)

#目标级链接库,推荐
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib pthread dl m)

#直接链接指定路径的库文件
target_link_libraries(app PRIVATE ${PROJECT_SOURCE_DIR}/libs/libfoo.a)

添加源码搜索路径

1
2
3
4
5
6
7
8
9
10
aux_source_directory(src SRC_LIST)
aux_source_directory(src/core CORE_SRC)

add_executable(app ${SRC_LIST} ${CORE_SRC})

# 添加头文件
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/third_party/include)
target_include_directories(app PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_include_directories(mylib PUBLIC ${PROJECT_SOURCE_DIR}/include)

增加编译选项

add_compile_options():目录级,影响当前目录及子目录的目标。

target_compile_options():目标级

target_compile_definitions():专门加 -D 宏定义。

target_include_directories():专门加头文件路径。

1
2
3
4
5
6
7
8
9
10
add_compile_options(-Wall -Wextra -O2)

add_compile_options(
	$<$<CONFIG:Debug>:-g>
	$<$<CONFIG:Release>:-O3>
)

add_compile_options(
	$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
) # 构建链接库

add_library(lib_name STATIC/SHARED src) 函数作用:构建链接库 参数lib_name:是要生成的库名称, 参数STATIC/SHARED/OBJECT:指定生成静态库或动态库, 参数src:指明库的生成所需要的源文件

1
2
3
add_library(mylib STATIC src/a.cpp src/b.cpp)
add_library(myshared SHARED src/a.cpp)
add_library(myobj OBJECT src/a.cpp)

项目依赖

构造一个子项目为链接库,主项目依赖这个子项目。子项目功能是计算平方根,用以代替编译器自己提供的标准平方根计算方法。子项目名称为MathFunctions。项目目录如下:

1
2
3
4
5
6
7
项目根目录/
├── CMakeLists.txt
├── tutorial.cxx
└── MathFunctions/
	├── CMakeLists.txt
	├── mysqrt.cxx
	└── MathFunctions.h

为了编译这个库的代码,需要在顶层根目录的 CMakeLists.txt 中调用 add_subdirectory(MathFunctions)。这样 CMake 会处理子目录 MathFunctions 中的 CMakeLists.txt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 是否使用我们自己实现的数学函数?
option(USE_MYMATH
	"Use tutorial provided math implementation" ON)

# 是否添加 MathFunctions 库?
if(USE_MYMATH)
# 添加一个头文件路径,确保MathFunctions/MathFunctions.h可以被函数原型找到。
include_directories("${PROJECT_SOURCE_DIR}/MathFunctions")
# 把该子项目加入构建,并构建其中的 MathFunctions 库
add_subdirectory(MathFunctions)
set(EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif(USE_MYMATH)

# 添加可执行文件
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial ${EXTRA_LIBS})

# 创建一个名为 MathFunctions 的库目标,并把源文件 mysqrt.cxx 编译进这个库中
add_library(MathFunctions mysqrt.cxx)

这里使用 USE_MYMATH 的设置来决定是否编译并使用 MathFunctions。在CMake-GUI中,USE_MYMATH 将以默认的ON值显示,用户可以随意更改。该值将存储在缓存文件中,用户不需要每次运行cmake指令时都对其进行一次设定。 注意这里使用了一个变量 EXTRA_LIBS 来收集可选库,之后再将这些库链接到可执行文件中。这是大型项目中包含许多可选组件时常用的一种保持项目整洁的方法。

源码中也需要使用 USE_MYMATH。这个宏由 CMake 通过配置文件 TutorialConfig.h.in 提供给源代码。对应的源码修改比较直接,如下所示:

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

安装和测试

对于 MathFunctions,可以在 MathFunctions/CMakeLists.txt 中添加两行代码,用来安装库目标和头文件:

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

对于主程序,还需要在顶层根目录的 CMakeLists.txt 中加入几行代码,用来安装可执行文件和由 CMake 配置生成的头文件:

1
2
3
4
# 添加安装目标
install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
        DESTINATION include)

完成这些设置后,就可以正常构建工程。执行 make install,或者在 IDE 中构建 INSTALL 目标,工程会安装合适的头文件、库文件和可执行文件。CMake 变量 CMAKE_INSTALL_PREFIX 用来定义安装文件的根目录。

在顶层根目录的 CMakeLists.txt 中,可以添加若干基础测试来验证程序是否按预期工作。首先引入 CTest:

1
include(CTest)

然后添加测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 应用程序是否能运行
add_test(TutorialRuns Tutorial 25)

# 是否计算了 25 的平方根
add_test(TutorialComp25 Tutorial 25)
set_tests_properties(TutorialComp25
  PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")

# 是否能处理负数
add_test(TutorialNegative Tutorial -25)
set_tests_properties(TutorialNegative
  PROPERTIES PASS_REGULAR_EXPRESSION "-25 is 0")

# 是否能处理小数
add_test(TutorialSmall Tutorial 0.0001)
set_tests_properties(TutorialSmall
  PROPERTIES PASS_REGULAR_EXPRESSION "0.0001 is 0.01")

# 用法提示是否正常
add_test(TutorialUsage Tutorial)
set_tests_properties(TutorialUsage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")

构建完成后,可以运行 ctest 命令行工具来执行这些测试。

第一个测试 TutorialRuns 只是验证应用程序能够运行、不会发生段错误或其他崩溃,并且返回值为 0。这是 CTest 测试的基本形式。后面的几个测试都使用 PASS_REGULAR_EXPRESSION 测试属性,用来验证测试输出中是否包含指定字符串。这里分别验证计算出的平方根是否符合预期,以及当参数数量不正确时是否打印用法提示。

如果希望添加大量针对不同输入值的测试,可以考虑定义一个宏来简化测试添加过程:

1
2
3
4
5
6
7
8
9
10
# 定义一个宏来简化测试添加,然后使用它
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_test(25 "25 is 5")
do_test(-25 "-25 is 0")

每次调用 do_test,都会根据传入的参数向工程中添加一个测试,测试名称、输入值和期望结果都来自这些参数。


添加系统依赖检测

接下来考虑向工程中添加一些依赖于目标平台特性的代码。这个例子中,添加的代码依赖目标平台是否提供 log 和 exp 函数。当然,几乎所有平台都有这些函数,但本教程假设它们并不常见。如果平台提供 log,就在 mysqrt 函数中使用它来计算平方根。

首先在顶层 CMakeLists.txt 中使用 CheckFunctionExists.cmake 宏检测这些函数是否可用:

1
2
3
4
# 当前系统是否提供 log 和 exp 函数?
include(CheckFunctionExists)
check_function_exists(log HAVE_LOG)
check_function_exists(exp HAVE_EXP)

然后修改 TutorialConfig.h.in,如果 CMake 在平台上找到了这些函数,就定义相应的宏:

1
2
3
// 平台是否提供 exp 和 log 函数?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

需要注意,对 log 和 exp 的检测必须在针对 TutorialConfig.h 的 configure_file 命令之前完成。因为 configure_file 命令会立即使用 CMake 当前的设置来配置文件。如果在检测之前配置,HAVE_LOG 和 HAVE_EXP 还没有确定,生成的头文件就不会包含正确结果。

最后,在 mysqrt 函数中,如果系统同时提供 log 和 exp,就可以使用它们提供另一种实现:

1
2
3
4
5
6
7
// 如果同时有 log 和 exp,就使用它们
#if defined(HAVE_LOG) && defined(HAVE_EXP)
  result = exp(log(x) * 0.5);
#else
  // 否则使用迭代方法
  // ...
#endif

这样,同一份源码就可以根据目标平台能力选择不同的平方根实现。

构建生成器并生成文件

这一部分展示如何把生成的文件加入应用程序的构建过程。这个例子中,在构建过程中创建一个预计算平方根表,然后把这个表编译进应用程序。

为了实现这一点,首先需要一个生成表的程序。在 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
// 一个用于构建平方根表的简单程序
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

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

  // 确保参数足够
  if (argc < 2)
  {
    return 1;
  }

  // 打开输出文件
  FILE *fout = fopen(argv[1], "w");
  if (!fout)
  {
    return 1;
  }

  // 创建一个包含平方根表的源文件
  fprintf(fout, "double sqrtTable[] = {\n");
  for (i = 0; i < 10; ++i)
  {
    result = sqrt(static_cast<double>(i));
    fprintf(fout, "%g,\n", result);
  }

  // 用 0 结束表
  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
17
18
# 首先添加生成表的可执行文件
add_executable(MakeTable MakeTable.cxx)

# 添加生成源代码的命令
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
)

# 把二进制目录加入头文件搜索路径
include_directories(${CMAKE_CURRENT_BINARY_DIR})

# 添加主库
add_library(MathFunctions
  mysqrt.cxx
  ${CMAKE_CURRENT_BINARY_DIR}/Table.h
)

首先像添加普通可执行文件一样添加 MakeTable。然后添加一个自定义命令,指定如何通过运行 MakeTable 生成 Table.h。接着必须让 CMake 知道 mysqrt.cxx 依赖生成文件 Table.h。做法是把生成的 Table.h 加入 MathFunctions 库的源文件列表中。同时还需要把当前二进制目录加入头文件搜索路径,这样 mysqrt.cxx 才能找到并包含 Table.h。

当构建这个项目时,构建顺序是:

  1. 首先构建 MakeTable 可执行文件;
  2. 然后运行 MakeTable 生成 Table.h;
  3. 最后编译包含 Table.h 的 mysqrt.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
cmake_minimum_required(VERSION 2.6)
project(Tutorial)
include(CTest)

# 版本号
set(Tutorial_VERSION_MAJOR 1)
set(Tutorial_VERSION_MINOR 0)

# 当前系统是否提供 log 和 exp 函数?
include(${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)

check_function_exists(log HAVE_LOG)
check_function_exists(exp HAVE_EXP)

# 是否使用我们自己实现的数学函数
option(USE_MYMATH
  "Use tutorial provided math implementation" ON)

# 配置头文件,把一些 CMake 设置传递给源代码
configure_file(
  "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
  "${PROJECT_BINARY_DIR}/TutorialConfig.h"
)

# 把二进制目录加入头文件搜索路径
# 以便找到 TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

# 是否添加 MathFunctions 库?
if(USE_MYMATH)
  include_directories("${PROJECT_SOURCE_DIR}/MathFunctions")
  add_subdirectory(MathFunctions)
  set(EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif(USE_MYMATH)

# 添加可执行文件
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial ${EXTRA_LIBS})

# 添加安装目标
install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
        DESTINATION include)

# 应用程序是否能运行
add_test(TutorialRuns Tutorial 25)

# 用法提示是否正常
add_test(TutorialUsage Tutorial)
set_tests_properties(TutorialUsage
  PROPERTIES
  PASS_REGULAR_EXPRESSION "Usage:.*number"
)

# 定义一个宏来简化测试添加
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_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
// Tutorial 的配置选项和设置
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH

// 平台是否提供 exp 和 log 函数?
#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
18
19
20
21
# 首先添加生成表的可执行文件
add_executable(MakeTable MakeTable.cxx)

# 添加生成源代码的命令
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
)

# 把二进制目录加入头文件搜索路径
include_directories(${CMAKE_CURRENT_BINARY_DIR})

# 添加主库
add_library(MathFunctions
  mysqrt.cxx
  ${CMAKE_CURRENT_BINARY_DIR}/Table.h
)

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

构建和安装包

假设希望把项目分发给其他人使用,并希望在多种平台上同时提供二进制分发包和源码分发包。这与前面“安装和测试”中的安装略有不同:前面安装的是从源码构建出来的二进制文件,而这里要构建支持二进制安装和包管理功能的安装包,例如 cygwin、debian、RPM 等平台上的包。

为此可以使用 CPack 创建平台相关的安装包。具体来说,需要在顶层 CMakeLists.txt 文件末尾添加几行代码:

1
2
3
4
5
6
7
# 构建由 CPack 驱动的安装包
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

添加仪表盘支持

前面的步骤中已经为项目定义了一些测试,现在运行这些测试并把结果提交到仪表盘。

为了包含仪表盘支持,在顶层 CMakeLists.txt 中引入 CTest 模块:

1
2
# 启用仪表盘脚本
include(CTest)

还需要创建一个 CTestConfig.cmake 文件,在其中指定仪表盘使用的项目名称和上传路径,支持配置私有仪表盘。

1
2
set(CTEST_PROJECT_NAME "Tutorial")
set(CTEST_SUBMIT_URL "https://my.cdash.org/submit.php?project=Tutorial")

CTest 运行时将读取这个文件。要创建一个简单的仪表盘,可以先对项目运行 CMake,然后切换到构建目录,再运行:

1
ctest -D Experimental

仪表盘结果会上传到 Kitware 的公共仪表盘。