本文译自官方简明教程,但有所删改。本文中所涉及到的所有源码,可以从 CMake 源码包的 Tests/Tutorial 目录中取得。
题外话:在本文翻译到差不多一半的时候,我很悲剧地发现,已经有别人很好地做了这项工作了(《CMake 用法导览》),所以我是重复造了轮子。不过既然已经一半了,我还是要把它做完才行。如果来访的朋友觉得我写得不太明白,可以参考一下别人的文章。
第一步 一个简单的开始
对于简单的项目,两行 CMakeLists 文件就可以搞定。如下:
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)
CMake 支持大写、小写或者大小写混合的命令语句,上面采用的是小写。tutorial.cxx 是一个计算数字平方根的小程序。
// 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;
}
添加版本号并配置头文件
现在要给我们的程序添加一个版本号。通过 CMakeLists 文件添加版本号比直接写在程序里面来得更加灵活方便。现在 CMakeLists 变成这样:
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 文件,包含以下内容:
// 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 文件里设定的值所替换。接下来我们修改 tutorial.cxx ,把这个头文件 include 进来,使版本号生效。程序现在变成这样:
// 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;
}
这里对程序的主要修改就是,新增了一个头文件,并且在 Usage 提示里输出了版本号。
第二步 新增一个库
现在我们要为项目新增一个库。这个库包含我们自己的计算平方根的实现,用来取代编译器默认的计算平方根的库。我们新建一个子目录 MathFunctions,把自己的库放在这里面。这个库里也有一个 CMakeLists 文件,内容如下:
add_library(MathFunctions mysqrt.cxx)
mysqrt.cxx 源码有一个 mysqrt 函数,提供了计算平方根的功能。为了使用新的库,我们要在顶层 CMakeLists 文件里添加一个 add_subdirectory 调用,确保新库能被编译。此外还要再加一条 include 路径, 把MathFunctions/mysqrt.h 这个头文件包含进来。最后,还要把新库添加到可执行目标里来。顶层 CMakeLists 的最后几行现在是这个样子的:
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial MathFunctions)
现在,把是否使用 MathFunctions 库配置成一个可选项。本例程里这么做事没有必要的,但是在包含大型库,或者使用依赖于第三方库的代码的时候,这一步就很有必要了。首先,在顶层 CMakeLists 文件里,添加如下选项:
# should we use our own math functions?
option (USE_MYMATH
"Use tutorial provided math implementation" ON)
上面这条会显示在 CMake 的 GUI 里,默认值是 ON,用户可以自行更改。这个设定会存在缓存里,所以用户不必每次 CMake 这个工程的时候都要配置一次。接下来我们对顶层 CMakeLists 文件的末尾做些修改,把是否编译和链接 MathFunctions 库的语句添加进来:
# 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 变量)。这种是一种通用的做法,可以保持包含多个可选项的大型项目的组件的简洁性。相应的,对程序源码做些改变,如下:
// 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 配置文件里面添加了下面这行,所以 CMake 可以在源码中使用这个变量。
#cmakedefine USE_MYMATH
第三步 安装与测试
这一步里我们要为项目添加安装与测试的支持。安装规则是非常好懂的。对于 MathFunctions 库,我们在 MathFunctions 的CMakeLists 文件里,加入如下两句:
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)
在顶层 CMakeLists 文件里加入如下代码来安装可执行文件和头文件
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include)
现在,你就可以编译 tutorial 里的程序,然后输入 make install (或者从 IDE 里编译 INSTALL 目标)。CMake会自动地为你安装合适的头文件、库和可执行文件。CMake变量 CMAKE_INSTALL_PREFIX 用于确定文件将被安装在哪一个目录下。随后,还可以加入一个测试的流程。在顶层 CMakeLists 文件的底部,可以添加一系列基本的测试项来确定应用可以正常工作。
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 测试参数来确定测试的输出是否包含了指定的字符串。这样可以确定计算平方根是否工作正常,当输入非法数值时是否能够给出usage 信息。如果你想要用多个不同的输入来进行多次测试,可以考虑使用下面这样的宏:
#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,都会传入新的名字、输入和期望的输出。
第四步 添加系统检查
接下来我们往项目代码里添加一些目标平台可能没有的特性。本例中,我们要添加的代码,将判断目标平台中是否有同 log 和 exp 函数。当然,几乎所有的系统都有 log 和 exp 函数,但作为教程,这里假定不是所有系统都有。如果平台有 log 那么我们将在 mysqrt 函数里用它来计算平方根。我们在顶层 CMakeLists 文件里使用 CheckFunctionExists.cmake 宏来测试这些函数是否可用:
# does this system provide the log and exp functions?
include (CheckFunctionExists)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)
对 log 和 exp 的测试必须在和 TutorialConfig.h 相关的配置命令之前完成。哪些配置命令会使用 CMake 中现成的设定来配置文件。现在,我们将可以根据系统中是否提供 log 和 exp 函数,来确定 mysqrt 的具体实现方式。
// 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
. . .
第五步 添加生成的文件和生成器
本步骤将展示如何在应用的生成过程中,添加生成的源文件。我们将创建一张预先计算好的平方根值表,将这张表编译进我们的应用中。首先我们需要一个能够生成这张表的程序。为此,在 MathFunctions 子目录下,我们新增一个源文件,叫做 MakeTable.cxx。
// 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 文件里添加合适的命令,来生成 MakeTable 程序,并且在构建的过程中运行这个程序。我们需要引入一些新的命令:
# 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 。通过向 MathFunctions 库相关的代码里添加 Table.h ,可以实现这点。我们还需要把当前的二进制目录添加到 include 路径清单中去,这样 mysqrt.cxx 才能找到并且包含 Table.h 。当整个项目被构建时,会首先编译 MakeTable 可执行文件。执行 MakeTable 来生成 Table.h 。最后编译 include 了 Table.h 的 mysqrt.cxx 来生成 MathFunctions 库。加入了所有上面提到的特性的顶层 CMakeLists 文件变成了这个样子:
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 看起来是这样的:
// 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 文件看起来像这样:
# 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)
第六步 生成一个安装器
假设我们要向用户分发我们的项目,计划在多个平台上同时提供二进制文件和源代码。这和我们在第三步里做的“安装”有些不同。当时我们安装的是我们已经从源代码编译好了的二进制文件。而本例中我们要构件的安装包,支持二进制文件的安装,可以被 cygwin,debian,RPMs 等管理的安装包。我们将使用《Chapter Packaging with CPack》(译注:这时官方完整文档里的)这一章中提到的 CPack 来创建特定平台下的安装包。在顶层的 CMakeLists.txt 文件的末尾,需要添加如下几行:
# 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)
这样就可以了。我们在开头 include 了 installRequiredSystemLibraries。这个模块将会把当前平台下运行本项目所需要的所有运行库都打包起来。然后,我们又设置了存放 license 文件的目录和版本信息。版本信息我们此前已经设置过了,可以直接调用。最后,我们把 CPack 模块 include 进来,它将会调用我们刚刚设置的参数,以及当前平台上的其它信息,来生成安装包。
最后一步,是常规的生成的项目的步骤了。生成二进制包输入如下命令:
cpack --config CPackConfig.cmake
生成源码包输入如下命令:
cpack --config CPackSourceConfig.cmake
第七步 添加对 Dashboard 的支持
向 dashboard 提交我们的测试结果是很简单的,我们在这个教程前面的步骤里已经定义了许多测试数据。我们要做的就是运行这些测试并且向 dashboard 提交结果。为了获得对 dashboards 的支持,我们要在 CMakeLists 里面 include CTest 模块。
# enable dashboard scripting
include (CTest)
我满还可以添加一个 CTestConifg.cmake 文件,来指定 dashboard 里本项目的名字。
set (CTEST_PROJECT_NAME "Tutorial")
CTest 在运行的时候,将会读取这些文件。要创建一个简单的 dashboard ,你可以这样做:切换目录到二进制文件树,运行 ctest -DExperimental。你 dashboard 的结果将会被上传到 Kitware 的公共 dashboard 上。
本文最终修改于 2016/4/11