CMake 使用教程

资源

本文档翻译自官方 cmake turorial 。更新日期:2018年9月27日。译者这里以 windows 平台为例,介绍了在 Windows 平台下配合 VS2017 的使用方法。

虽然本文是官方的入门教程,但本教程并不是面向从未使用过 CMake 的用户。本文并不会对使用到的命令进行一一解释,所以在阅读本文之前用户应该对 CMake 有一些了解和接触。

下面是一个一步步的教程包含了使用 CMake 构建系统的常用情况。下面介绍的许多内容都在 Mastering CMake 一书中作为单独的议题介绍过了,但在一个样例项目中演示如何将它们结合在一起同样是是否有用的。本教程可以在 CMake 源文件中的 Tests/Tutorial 目录下找到。每一个步骤都有它们自己的子文件夹,包含了对应步骤完整的教程源码。

通过查看 cmake-buildsystem 和 cmake-language 手册的介绍章节可以对 CMake 的设计理念和源码结构组织有一个整体的了解。

基础的构建步骤(步骤1)

一个最常用的基础项目是从源码中构建一个可执行文件。对于一个简单的项目两行 CMakeLists.txt 文件就能搞定:

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)

上面的例子中使用了小写的命令,事实上,CMakeLists.txt 文件并不区分命令的大小写。tutorial.cxx 源码是用来计算一个数的算数平方根,下面是其一个简单的版本:

// A simple program that computes the square root of a number
#include 
#include 
#include 
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);
  system("pause");
  return 0;
}

译者这里在 16 行附加了一行 system("pause"); ,是为了程序执行完毕后不会立刻关闭窗口。后面的代码的示例中并不会再添加此行,如果用户需要暂停的话,可以在自己的代码中加入该行。

添加版本号和配置头文件

第一个要添加的特性就是给我们的可执行文件和项目提供一个版本号。你可以在你的源码之外做到这一点,在 CMakeLists.txt 文件中做这些会更加灵活。为了添加一个版本号我们修改我们的 CMakeList.txt 文件如下:

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) 中,所以我们必须将该文件夹添加到头文件搜索路径中。接下来我们在源码中创建一个包含以下内容的 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.txt 文件中对应的值替换。接下来我们修改 tutorial.cxx 源码包含配置头文件并使用版本号,修改后的源码如下:

// A simple program that computes the square root of a number
#include 
#include 
#include 
#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;
}

构建项目并执行文件

官方并没有给出如何构建项目,这里以 VS 为例介绍如何构建以上项目并编译执行。在该目录下面建立 build 文件夹,并新建 run.cmd 文件,编写内容如下:

echo off
echo build:
cmake -G "Visual Studio 15 2017 Win64" ..
echo compile:
devenv Tutorial.sln /build "Debug|x64"
echo run:
start ./Debug/Tutorial.exe %1

上面脚本中 echo命令主要是用来输出提示信息,可以忽略。剩下一共有三行代码。

第3行代码为使用 CMake 构建工程文件.-G 参数用来指定编译器,如果不写这里会找到一个默认的编译器。我这里默认的编译器就是 VS2017,但是默认构建的程序为 32 位程序,我这里显示的指定使用 VS2017 构建 64 位程序。

第5行代码是使用命令行的形式编译 VS 的 .sln 文件。关于命令行构建 VS 项目这里不做过多介绍,有兴趣可以参考微软官方给出的 Devenv command line switches。当然我们也可以使用 VS 打开 .sln 文件,然后手动点击 生成

第7行代码为运行程序。

添加一个库文件(步骤2)

现在我们将会给我们的项目添加一个库文件。这个库文件包含了我们自己实现的开方运算。可执行文件使用这个库替代编译器提供的标准开方运算。本教程中我们将其放到 MathFunctions 文件夹下,该文件夹下还有一个包含下面一行代码的 CMakeLists.txt 文件。

add_library(MathFunctions mysqrt.cxx)

mysqrt.cxx 文件只有一个名为 mysqrt 的函数,其提供了和标准库 sqrt 相同的功能。内容如下(官网官方例程中可以找到):

#include "MathFunctions.h"
#include 

// a hack square root calculation using simple operations
double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  double result;
  double delta;
  result = x;

  // do ten iterations
  int i;
  for (i = 0; i < 10; ++i) {
    if (result <= 0) {
      result = 0.1;
    }
    delta = x - (result * result);
    result = result + 0.5 * delta / result;
    fprintf(stdout, "Computing sqrt of %g to be %g\n", x, result);
  }
  return result;
}

对应的头文件为 MathFunction.h,其内容如下:

double mysqrt(double x);

为了构建并使用新的库文件,我们需要在顶层 CMakeList.txt 文件添加 add_subdirectory 语句。我们需要添加额外的头文件包含路径,以便将包含函数原型的 MathFunctions/MathFunctions.h 头文件包含进来。最后我们还需要给可执行文件添加库。最终顶层 CMakeList.txt 文件的最后几行如下所示:

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 文件添加一个选项:

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

这个选项会在 CMake GUI 中显示并会将默认值设置为 ON,用户可以根据需求修改该值。这个设置会本保存下来,所以用户无需在每次运行 CMake 时都去设置。接下来就是将构建和连接 MathFunctions 设置为可选项。修改顶层的 CMakeLists.txt 文件如下所示:

# 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 
#include 
#include 
#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 添加以下代码传递给源码:

#cmakedefine USE_MYMATH

构建、编译和运行使用的代码和上一节相同。

安装和测试(步骤3)

下一步我们将给我们的项目添加安装规则和测试。安装规则简单明了。对于 MathFunctions 库的安装,我们通过在 MathFunction 的 CMakeLists.txt 文件中添加以下两行来设置其库和头文件的安装。

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

对于本文这个应用通过在顶层 CMakeLists.txt 添加以下内容来安装可执行文件和配置的头文件:

# 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 参数可以指定安装文件的根目录(之前还可以加上 -D 参数,具体意义可以参考 what does the parameter "-D" mean)。

添加测试过程同样简单明了。在顶层 CMakeLists.txt 文件的最后我们可以添加一个基础测试数据来验证该应用程序是否正常运行。

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 测试属性(正则表达式)来验证输出中是否包含了特定的字符串。这里验证开方是否正确并且在计算错误时输出输出对应信息。如果你希望添加很多的测试来测试不同的输入值,你可以考虑定义一个像下面这样的宏:

#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 就会根据指定的信息生成一个新的测试。

该步骤对应的 build 文件夹下的构建和运行脚本 run.cmd 内容如下:

echo off
echo build:
cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_INSTALL_PREFIX=D:\project\cpp\cmake\tutorial\step3\install ..
echo compile:
devenv Tutorial.sln /build "Debug|x64"
echo install:
devenv Tutorial.sln /build "Debug|x64" /project INSTALL
echo test:
devenv Tutorial.sln /build "Debug|x64" /project RUN_TESTS

安装位置根据自己的需要进行调整。

添加系统自检(步骤 4)

接下来我们考虑给我们的项目添加一些取决于目标平台是否有一些特性的代码。这里我们将添加一些取决于目标平台是否有 log 和 exp 函数的代码。当然对于大多数平台都会有这些函数,但这里我们认为这并不常见。如果平台有 log 函数我们将在 mysqrt 函数中使用它计算平方根。我们首先在顶层 CMakeLists.txt 文件中使用 CheckFunctionExists 宏测试这些函数是否可用,代码如下:

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

接下来我们修改 TutorialConfig.h.in 文件定义一些宏以表示 CMake 是否在平台上找到这些函数:

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

一定要在使用 configure_file 生成 TutorialConfig.h 之前测试 log 和 exp。因为 configure_file 命令会立刻使用当前 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
  . . .

添加一个生成的文件和生成器(步骤 5)

在这一章节我们将会展示如何在构建一个应用的过程中添加一个生成的源文件。在本例中我们将创建一个预先计算的平方根表作为构建过程的一部分,然后将其编译到我们的应用中。为了做到这一点我们首先需要一个能产生这张表的程序。在 MathFunctions 文件夹下创建一个新的名为 MakeTable.cxx 的文件,内容如下:

// A simple program that builds a sqrt table 
#include 
#include 
#include 
 
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(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 可执行文件,并将它作为构建的一部分运行。只需要一点代码就能实现这个功能:

# 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 库的源文件列表实现。我们同样必须将当前二进制文件路径添加到包含路径中,以保证 Table.h 文件被找到并被 mysqrt.cxx 包含。该项目在构建时会首先构建 MakeTable 可执行文件。接下来会运行该可执行文件并生成 Table.h 文件。最后它将会编译包含 Table.h 的 mysqrt.cxx 文件并生成 MathFunctions 库。此时包含了所有我们添加的特性的顶层 CMakeLists.txt 文件应该像下面这样:

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.txt 文件如下:

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

构造一个安装器(步骤 6)

接下来假设我们想将我们的项目发布给其他人以便供他们使用。我们想提供在不同平台上的二进制文件和源码的发布版本。这一点和我们在之前安装和测试章节(步骤3)略有不同,步骤三安装的二进制文件是我们从源码构建的。这里我们将构建一个支持二进制文件安装的安装包和可以在 cygwin,debian,RPMs 等中被找到的安装管理特性。为了实现这一点我们将使用 CPack 来创建在 Packaging with CPack 章节中介绍过的平台特定安装器(platform specific installers)。我们需要在顶层 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)

首先我们添加了 InstallRequiredSystemLibraries。该模块会包含我们项目在当前平台所需的所有运行时库(runtime libraries)。接下来我们设置了一些 CPack 变量来指定我们项目的许可文件和版本信息。版本信息使用我们在之前设置的内容。最后我们包含 CPack 模块,它会使用这些变量和其它你安装一个应用程序所需的系统属性。

接下来就是正常编译你的项目然后使用 CPack 运行它,为了编译二进制发布版本你需要运行:

cpack --config CPackConfig.cmake

创建一个源文件发布版本你应该使用下面命令:

cpack --config CPackSourceConfig.cmake

Windows 平台下CMake 默认会使用 NSIS 创建安装包,因此我们在执行上面命令前需要安装该软件。当然我们也可以使用 WiX 包安装工具,只需要在 include(CPack) 之前加上 set(CPACK_GENERATOR WIX) 即可。安装包工具的选择和对比可以参考 NISS Vs WiX Vs AnyOther Installation Package 。

添加表盘工具(Dashboard)支持(步骤7)

添加将我们测试结果提交到仪表盘的功能非常简单。在本教程的之前步骤中我们已经给我们的项目定义了一些测试。我们只需要运行这些测试然后提交到仪表盘即可。为了支持仪表盘功能我们需要在顶层 CMakeLists.txt 文件中增加 CTest 模块。

# enable dashboard scripting
include (CTest)

我们同样可以创建一个 CTestConfig.cmake 文件来在表盘工具中指定本项目的名字。

set (CTEST_PROJECT_NAME "Tutorial")

CTest 会在运行时读取该文件。你可以在你的项目上运行 CMake 来创建一个简单的仪表盘,切换目录到二进制文件夹下,然后运行 ctest -DExperimental.你仪表盘的运行结果会上传到 Kitware 的公共仪表盘上 这里。

如果需要上传的话还需要设置 Drop site ,具体细节可以参考官方的 ctest(1) 。

你可能感兴趣的:(CMake 使用教程)