万字总结编译利器CMake,从入门到项目实战演练!

一、什么是 CMake

你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。

CMake就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等 [1]。

CMake是我非常喜欢且一直使用的工具。它不但能帮助我跨平台、跨编译器,而且最酷的是,它帮我节约了太多的存储空间。特别是与水银结合起来使用,其友好的体验,足以给我们这些苦逼码农一丝慰藉。

以下内容翻译自官网教程:CMake

1.1CMake Tutorial

A Basic Starting Point (Step 1)

最基本的就是将一个源代码文件编译成一个exe可执行程序。对于一个简单的工程来说,两行的CMakeLists.txt文件就足够了。这将是我们教程的开始。CMakeLists.txt文件看起来会像这样:

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

注意,在这个例子中,CMakeLists.txt都是使用的小写字母。事实上,CMake命令是大小写不敏感的,你可以用大写,也可以用小写,也可以混写。tutorial.cxx源码会计算出一个数的平方根。它的第一个版本看起来非常简单,如下:

// 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文件来做的话会提供更多的灵活性。为了增加版本号,我们可以如此更改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) 

由于配置文件必须写到binary tree中,因此我们必须将这个目录添加到头文件搜索目录中。我们接下来在源码目录中创建了http://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@ 的值将会被改变。接下来,我们修改了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;
}

最主要的变更是包含了TutorialConfig.h头文件,并输出了版本号。

Adding a Library (Step 2)

现在我们给工程添加一个库。这个库会包含我们自己的平方根实现。如此,应用程序就可以使用这个库而非编译器提供的库了。在这个教程中,我们将库放入一个叫MathFunctions的子文件夹中。

它会使用如下的一行CMakeLists文件:

add_library(MathFunctions mysqrt.cxx)

原文件mysqrt.cxx有一个叫做mysqrt的函数可以提供与编译器的sqrt相似的功能。为了使用新的库,我们需要在顶层的CMakeLists 文件中添加add_subdirectory的调用。我们也要添加一个另外的头文件搜索目录,使得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。当用户选择了之后,这个值会被保存在CACHE中,这样就不需要每次CMAKE都进行更改了。下面一步条件构建和链接MathFunctions库。为了达到这个目的,我们可以改变顶层的CMakeLists文件,使得其看起来像这样:

# 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这个宏。它由CMAKE通过配置文件http://TutorialConfig.h.in来提供给源代码。

#cmakedefine USE_MYMATH

Installing and Testing (Step 3)

接下来我们会为我们的工程增加安装规则和测试支持。安装规则是非常非常简单的。对于MathFunctions库我们安装库和头文件只需要添加如下的语句:

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)

这就是所有需要做的。现在你就可以编译这个教程了,然后输入make install(或者编译IDE中的INSTALL目标),则头文件、库和可执行程序等就会被正确地安装。CMake变量CMAKE_INSTALL_PREFIX被用来决定那些文件会被安装在哪个根目录下。添加测试也是一个相当简单的过程。在最顶层的CMakeLists文件的最后我们可以添加一系列的基础测试来确认这个程序是否在正确工作。

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

第一个测试简单地确认应用是否运行,没有段错误或者其它的崩溃问题,并且返回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的任意一次调用,就有另一个测试被添加到工程中。

Adding System Introspection (Step 4)

接下来,我们来考虑添加一些有些目标平台可能不支持的代码。在这个样例中,我们将根据目标平台是否有log和exp函数来添加我们的代码。当然大多数平台都是有这些函数的,只是本教程假设这两个函数没有被那么普遍地支持。如果平台有log,那么在mysqrt中,就用它来计算平方根。我们首先使用CheckFunctionExists.cmake来测试这些函数的是否存在,在顶层的CMakeLists文件中:

# does this system provide the log and exp functions?
include (CheckFunctionExists.cmake)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

接下来我们修改http://TutorialConfig.h.in来定义CMake是否找到这些函数的宏

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

重要的一点是,对tests和log的测试必须要在配置文件命令前完成。配置文件命令会使用CMake中的配置立马配置文件。最后在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
  . . .

Adding a Generated File and Generator (Step 5)

在这一节当中,我们会告诉你如何将一个生成的源文件加入到应用程序的构建过程中。在此例中,我们会创建一个预先计算好的平方根的表,并将这个表编译到应用程序中去。为了达到这个目的,我们首先需要一个程序来生成这样的表。在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被添加为可执行程序。然后我们添加了一个自定义命令来详细描述如何通过运行MakeTable来产生Table.h。接下来,我们需要让CMake知道mysqrt.cxx依赖于生成的文件Table.h。这是通过往MathFunctions这个库里面添加生成的Table.h来实现的。我们也需要添加当前的生成目录到搜索路径中,从而Table.h可以被mysqrt.cxx找到。

当这个工程被构建时,它首先会构建MakeTable这个可执行程序。然后运行MakeTable从而生成Table.h。最后,它会编译mysqrt.cxx来生成MathFunctions library。

在这一刻,我们添加了所有的特征到最顶层的CMakeLists文件,它现在看起来是这样的:

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
 
# 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)

Building an Installer (Step 6)

最后假设我们想要把我们的工程发布给别人从而让他们去使用。我们想要同时给他们不同平台的二进制文件和源代码。这与步骤3中的install略有不同,install是安装我们从源代码中构建的二进制文件。而在此例中,我们将要构建安装包来支持二进制安装以及cygwin,debian,RPMs等的包管理特性。为了达到这个目的,我们会使用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}")
set (CPACK_PACKAGE_VERSION_PATCH "${Tutorial_VERSION_PATCH}")
include (CPack)

这就是所有要做的。我们首先包含了InstallRequiredSystemLibraries。这个模块将会包含当前平台所需要的所有运行时库。接下来,我们设置了一些CPack的变量来保存license以及工程的版本信息。版本信息利用了我们在早前的教程中使用到的变量。最后我们包含了CPack这个模块来使用这些变量和你所使用的系统的其它特性来设置安装包。

接下来一步是用通常的方式构建工程,然后在CPack上运行它。如果要构建一个二进制包你需要运行:

cpack --config CPackConfig.cmake

如果要创建一个关代码包你需要输入

cpack --config CPackSourceConfig.cmake

Adding Support for a Dashboard (Step 7)

将测试结果上传到dashboard上是非常简单的。我们在早前的步骤中已经定义了一些测试。我们仅需要运行这些例程然后提交到dashboard上。为了包含对dashboards的支持,我们需要在顶层的CMakeLists文件中包含CTest模块。

# enable dashboard scripting
include (CTest)

我们也创建了一个CTestConfig.cmake文件来指定这个工程在dashobard上的的名字。

set (CTEST_PROJECT_NAME "Tutorial")

CTest会读这个文件并且运行它。如果要创建一个简单的dashboard,你可以在你的工程上运行CMake,改变生成路径的目录,然后运行ctest -D Experimental。你的dashboard的结果会被上传到Kitware的公共dashboard中。

在Linux平台下使用 CMake生成Makefile 并编译的流程如下:

  1. 编写 CMake 配置文件 CMakeLists.txt 。
  2. 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile 1 1ccmakecmake 的区别在于前者提供了一个交互式的界面。。其中, PATH 是 CMakeLists.txt 所在的目录。
  3. 使用 make 命令进行编译。

本文将从实例入手,一步步讲解 CMake 的常见用法,文中所有的实例代码可以在这里找到。如果你读完仍觉得意犹未尽,可以继续学习我在文章末尾提供的其他资源。

1.2CMake指南教程(官方地址

CMake教程提供了逐步指南,涵盖了CMake可以帮助解决的常见构建系统问题。 了解示例项目中各个主题如何协同工作将非常有帮助。示例的教程文档和源代码可在CMake源代码树的Help/guide/tutorial目录中找到。 每个步骤都有其自己的子目录,其中包含可以用作起点的代码。 教程示例是渐进式的,因此每个步骤都为上一步提供了完整的解决方案。

(第1步)基本起点

最基本的项目是从源代码文件构建一个可执行文件。 对于简单的项目,只需三行CMakeLists.txt文件。 这是本教程的起点。 在Step1目录中创建一个CMakeLists.txt文件,如下所示:

cmake_minimum_required(VERSION 3.10)  
# set the project name project(Tutorial)  
# add the executable add_executable(Tutorial tutorial.cxx)

请注意,此示例在CMakeLists.txt文件中使用小写的命令。 CMake支持大写,小写和大小写混合的命令。 Step1目录中提供了tutorial.cxx的源代码,可用于计算数字的平方根。

添加版本号和配置头文件

我们将添加的第一个功能是为我们的可执行文件和项目提供版本号。 虽然我们可以仅在源代码中执行此操作,但是使用CMakeLists.txt可以提供更大的灵活性。

首先,修改CMakeLists.txt文件来设置版本号。

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

###早期版本的写法
###		project(Tutorial)
###		set (Tutorial_VERSION_MAJOR 1)
###		set (Tutorial_VERSION_MINOR 0)

然后,配置一个头文件,将版本号传递给源代码:

configure_file(TutorialConfig.h.in TutorialConfig.h)

###早期版本的写法
###configure_file ("${PROJECT_SOURCE_DIR}/TutorialConfig.h.in" "${PROJECT_BINARY_DIR}/TutorialConfig.h")

由于配置的文件将被写入二进制树中,所以我们必须将该目录添加到搜索include文件的路径列表中。在CMakeLists.txt文件的末尾添加以下行:

#必须在add_excutable之后
target_include_directories(Tutorial PUBLIC  "${PROJECT_BINARY_DIR}")
###早期版本的写法:
###可以位于任意位置,一般放在add_excutable之前
###include_directories("${PROJECT_BINARY_DIR}")

使用您喜欢的编辑器在源码目录中创建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@的值将被替换。接下来,修改tutorial.cxx以包括配置的头文件TutorialConfig.h。最后,通过更新tutorial.cxx来打印出版本号,如下所示:

 if (argc < 2) {
    // report version
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
              << Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

完整的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.10)

#set project name and version
project(Tutorial VERSION 1.0)

configure_file(TutorialConfig.h.in TutorialConfig.h)

#add the executable
add_executable(Tutorial tutorial.cxx)

target_include_directories(Tutorial PUBLIC  "${PROJECT_BINARY_DIR}"  )

指定c++标准

接下来,通过在tutorial.cxx中用std::stod替换atof,将一些C ++ 11功能添加到我们的项目中。 同时,删除#include <cstdlib>

const double inputValue = std::stod(argv[1]);

我们需要在CMake代码中明确声明应使用正确的标志。 在CMake中启用对特定C ++标准的支持的最简单方法是使用CMAKE_CXX_STANDARD变量。 对于本教程,请将CMakeLists.txt文件中的CMAKE_CXX_STANDARD变量设置为11,并将CMAKE_CXX_STANDARD_REQUIRED设置为True:

cmake_minimum_required(VERSION 3.10)
# set the project name and version
project(Tutorial VERSION 1.0)
# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

构建和测试

运行cmake或cmake-gui以配置项目,然后使用所选的构建工具进行构建。例如,从命令行我们可以导航到CMake源代码树的Help /guide/tutorial目录并运行以下命令:

mkdir Step1_build
cd Step1_build
cmake ../Step1
cmake --build .

导航到构建教程的目录(可能是make目录或Debug或Release构建配置子目录),然后运行以下命令:

Tutorial 4294967296
Tutorial 10
Tutorial

(第2步)添加库

现在,我们将添加一个库到我们的项目中。 该库是我们自己的实现的用于计算数字的平方根的库。 可执行文件可以使用此库,而不是使用编译器提供的标准平方根函数。

在本教程中,我们将库放入名为MathFunctions的子目录中。 该目录已包含头文件MathFunctions.h和源文件mysqrt.cxx。 源文件具有一个称为mysqrt的函数,该函数提供与编译器的sqrt函数类似的功能。

将以下一行CMakeLists.txt文件添加到MathFunctions目录中:

add_library(MathFunctions mysqrt.cxx)

为了使用新的库,我们将在顶层CMakeLists.txt文件中添加add_subdirectory调用,以便构建该库。 我们将新的库添加到可执行文件,并将MathFunctions添加为include目录,以便可以找到mqsqrt.h头文件。 顶级CMakeLists.txt文件的最后几行现在应如下所示:

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

#必须位于add_excutable之后
target_link_libraries(Tutorial PUBLIC MathFunctions)

###早期版本的写法
###target_link_libraries(Tutorial MathFunctions)

#add the binary tree to the search path for include files so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")

现在让我们将MathFunctions库设为可选。 虽然对于本教程而言确实不需要这样做,但是对于大型项目来说,这是很常见的。 第一步是向顶层CMakeLists.txt文件添加一个选项。

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(TutorialConfig.h.in TutorialConfig.h)

此选项将显示在CMake GUI和ccmake中,默认值ON,可由用户更改。 此设置将存储在缓存中,因此用户不必每次在构建目录上运行CMake时设置该值。

下一个更改是使构建和链接MathFunctions库成为布尔选项。 为此,我们将顶层CMakeLists.txt文件的结尾更改为如下所示:

if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
  list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()

# add the executable
add_executable(Tutorial tutorial.cxx)

#必须位于add_executable之后
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# add the binary tree to the search path for include files so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}" ${EXTRA_INCLUDES})

###早期版本的写法
##if(USE_MYMATH)
###include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
###add_subdirectory (MathFunctions)
###set(EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
###endif(USE_MYMATH)
###include_directories("${PROJECT_BINARY_DIR}")
###add_executable(Tutorial tutorial.cxx)
###target_link_libraries(Tutorial ${EXTRA_LIBS})

请注意,使用变量EXTRA_LIBS来收集任意可选库,以供以后链接到可执行文件中。 变量EXTRA_INCLUDES类似地用于可选的头文件。 当处理许多可选组件时,这是一种经典方法,我们将在下一步中介绍现代方法。

对源代码的相应更改非常简单。 首先,如果需要,在tutorial.cxx中包含MathFunctions.h头文件:

#ifdef USE_MYMATH
#  include "MathFunctions.h"
#endif

然后,在同一文件中,使USE_MYMATH控制使用哪个平方根函数:

#ifdef USE_MYMATH
  const double outputValue = mysqrt(inputValue);
#else
  const double outputValue = sqrt(inputValue);
#endif

由于源代码现在需要USE_MYMATH,因此可以使用以下行将其添加到TutorialConfig.h.in中:

#cmakedefine USE_MYMATH

练习:为什么在USE_MYMATH选项之后配置TutorialConfig.h.in如此重要? 如果我们将两者倒置会怎样?运行cmake或cmake-gui以配置项目,然后使用所选的构建工具进行构建。 然后运行构建的Tutorial可执行文件。使用ccmake或CMake GUI更新USE_MYMATH的值。 重新生成并再次运行本教程。 sqrt或mysqrt哪个函数可提供更好的结果?

完整的CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 3.5)                                                                                  
# set the project name and version
project(Tutorial VERSION 1.0)
 
# specify the C++ standard
set(CMAKE_CXX_STANDARD 11) 
set(CMAKE_CXX_STANDARD_REQUIRED True)
 
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(TutorialConfig.h.in TutorialConfig.h)
 
if(USE_MYMATH)
    add_subdirectory(MathFunctions)
    list(APPEND EXTRA_LIBS MathFunctions)
    list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()
 
# add the executable
add_executable(Tutorial tutorial.cxx)

#必须位于add_executable之后
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})  

# add the binary tree to the search path for include files so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC  "${PROJECT_BINARY_DIR}" ${EXTRA_INCLUDES}) 

(第3步)添加库的使用要求

使用要求可以更好地控制库或可执行文件的链接和include行,同时还可以更好地控制CMake内部目标的传递属性。 利用使用要求的主要命令是:

  • 目标编译定义
  • 目标编译选项
  • target_include_directories
  • 目标链接库

让我们从第2步中重构代码,以利用现代的CMake方法编写使用要求。 我们首先声明,链接到MathFunctions的任何东西都需要包括当前源码目录,而MathFunctions本身不需要。 因此,这可以成为INTERFACE使用要求。

请记住,INTERFACE是指消费者需要的,而生产者不需要东西。 将以下行添加到MathFunctions/CMakeLists.txt的末尾:

target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR)

现在,我们已经指定了MathFunction的使用要求,我们可以安全地从顶级CMakeLists.txt中删除对EXTRA_INCLUDES变量的使用:

if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
endif()
... ...
... ...

target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")

(第4步)安装与测试

现在,我们可以开始向项目添加安装规则和测试支持。

安装规则非常简单:对于MathFunctions,我们要安装库和头文件,对于应用程序,我们要安装可执行文件和配置的头文件。

因此,在MathFunctions/CMakeLists.txt的末尾添加:

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

然后在顶级cmakelt .txt的末尾添加

install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h" DESTINATION include)

这就是创建本教程的基本本地安装所需的全部工作。运行cmake或cmake-gui以配置项目,然后使用所选的构建工具进行构建。 从命令行键入cmake --install进行安装(自3.15中引入,较早版本的CMake必须使用make install),或从IDE构建INSTALL目标。 这将安装适当的头文件,库和可执行文件。

CMake变量CMAKE_INSTALL_PREFIX用于确定文件的安装根目录。 如果使用cmake --install,则可以通过--prefix参数指定自定义安装目录。 对于多配置工具,请使用--config参数指定配置。

验证已安装的Tutorial可以运行。

测试支持

接下来,测试我们的应用程序。 在顶级CMakeLists.txt文件的末尾,我们可以启用测试,然后添加一些基本测试以验证应用程序是否正常运行。

enable_testing()

# does the application run
add_test(NAME Runs COMMAND Tutorial 25)

# does it sqrt of 25
add_test (NAME Comp25 COMMAND Tutorial 25)
set_tests_properties (Comp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")

# does the usage message work?
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage  PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")

# define a function to simplify adding tests
function(do_test target arg result)
  add_test(NAME Comp${arg} COMMAND ${target} ${arg})
  set_tests_properties(Comp${arg}  PROPERTIES PASS_REGULAR_EXPRESSION ${result} )
endfunction(do_test)

# do a bunch of result based tests
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is [-nan|nan|0]")
do_test(Tutorial 0.0001 "0.0001 is 0.01")

###早期版本的写法
###include(CTest)
###add_test (TutorialRuns Tutorial 25)
###
###add_test (TutorialComp25 Tutorial 25)
###set_tests_properties (TutorialComp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")
###
##add_test (TutorialUsage Tutorial)
###set_tests_properties (TutorialUsage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")
###
####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_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 [-nan|nan|0]")
###do_test(0.0001 "0.0001 is 0.01")

第一个测试只是验证应用程序你能否运行,没有段错误或其他崩溃,并且返回值为零。 这是CTest测试的基本形式。

下一个测试使用PASS_REGULAR_EXPRESSION测试属性来验证测试的输出是否包含某些字符串。 在这种情况下,验证在提供了错误数量的参数时是否打印了用法消息。

最后,我们有一个名为do_test的函数,该函数运行应用程序并验证所计算的平方根对于给定输入是否正确。 对于do_test的每次调用,都会基于传递的参数将另一个测试添加到项目中,该测试具有名称,输入和预期结果。

重新构建应用程序,然后cd到二进制目录并运行ctest -Nctest -VV。 对于多配置生成器(例如Visual Studio),必须指定配置类型。 例如,要在“调试”模式下运行测试,请从构建目录(而不是“调试”子目录!)中使用ctest -C Debug -VV。 或者,从IDE构建RUN_TESTS目标。

(第5步)添加系统自检

让我们考虑向我们的项目中添加一些代码,这些代码取决于目标平台可能不具备的功能。 对于此示例,我们将添加一些代码,具体取决于目标平台是否具有logexp函数。 当然,几乎每个平台都具有这些函数,但对于本教程而言,假设它们并不常见。

如果平台具有logexp,那么我们将使用它们来计算mysqrt函数中的平方根。 我们首先使用顶级CMakeLists.txt中的CheckSymbolExists模块测试这些函数的可用性。 我们将在TutorialConfig.h.in中使用新定义,因此请确保在配置该文件之前进行设置。

include(CheckSymbolExists)
set(CMAKE_REQUIRED_LIBRARIES "m")
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)

###早期版本的写法
###include (CheckFunctionExists)
###check_function_exists (log HAVE_LOG)
###check_function_exists (exp HAVE_EXP)

现在,将这些定义添加到TutorialConfig.h.in中,以便我们可以从mysqrt.cxx中使用它们:

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

修改mysqrt.cxx以包括cmath。 接下来,在mysqrt函数的同一文件中,我们可以使用以下代码提供基于logexp(如果在系统上可用)的替代实现(在return result;前不要忘记#endif!):

#if defined(HAVE_LOG) && defined(HAVE_EXP)
  double result = exp(log(x) * 0.5);
  std::cout << "Computing sqrt of " << x << " to be " << result
            << " using log and exp" << std::endl;
#else
  double result = x;

运行cmake或cmake-gui来配置项目,然后使用所选的构建工具进行构建并运行Tutorial可执行文件。

您会注意到,我们也没有使用logexp,即使我们认为它们应该是可用。 我们应该很快意识到,我们忘记在mysqrt.cxx中包含TutorialConfig.h

我们还需要更新MathFunctions/CMakeLists.txt,以便mysqrt.cxx知道此文件的位置:

target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_BINARY_DIR})

完成此更新后,继续并再次构建项目,然后运行构建的Tutorial可执行文件。 如果仍未使用logexp,请从构建目录中打开生成的TutorialConfig.h文件。 也许它们在当前系统上不可用?

哪个函数现在可以提供更好的结果,sqrtmysqrt

指定编译定义

除了在TutorialConfig.h中保存HAVE_LOGHAVE_EXP值,对我们来说还有更好的地方吗? 让我们尝试使用target_compile_definitions

首先,从TutorialConfig.h.in中删除定义。 我们不再需要包含mysqrt.cxx中的TutorialConfig.hMathFunctions/CMakeLists.txt中的其他包含内容。

接下来,我们可以将HAVE_LOGHAVE_EXP的检查移至MathFunctions/CMakeLists.txt,然后将这些值指定为PRIVATE编译定义。

include(CheckSymbolExists)
set(CMAKE_REQUIRED_LIBRARIES "m")
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)

if(HAVE_LOG AND HAVE_EXP)
  target_compile_definitions(MathFunctions
                             PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()

完成这些更新后,继续并重新构建项目。 运行内置的Tutorial可执行文件,并验证结果与本步骤前面的内容相同。

(第6步)添加自定义命令和生成的文件

出于本教程的目的,假设我们决定不再使用平台logexp函数,而是希望生成一个可在mysqrt函数中使用的预计算值表。 在本节中,我们将在构建过程中创建表,然后将该表编译到我们的应用程序中。

首先,让我们删除MathFunctions/CMakeLists.txt中对logexp函数的检查。 然后从mysqrt.cxx中删除对HAVE_LOGHAVE_EXP的检查。 同时,我们可以删除#include <cmath>

MathFunctions子目录中,提供了一个名为MakeTable.cxx的新的源文件以生成表。

查看完文件后,我们可以看到该表是作为有效的C++代码生成的,并且输出文件名作为参数传入。

下一步是将适当的命令添加到MathFunctions/CMakeLists.txt文件中,以构建MakeTable可执行文件,然后在构建过程中运行它。 需要一些命令来完成此操作。

首先,在MathFunctions/CMakeLists.txt的顶部,添加MakeTable的可执行文件,就像添加任何其他可执行文件一样。

add_executable(MakeTable MakeTable.cxx)

然后,我们添加一个自定义命令,该命令指定如何通过运行MakeTable生成Table.h

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  )

接下来,我们必须让CMake知道mysqrt.cxx依赖于生成的文件Table.h。 这是通过将生成的Table.h添加到库MathFunctions的源列表中来完成的。

add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h )

我们还必须将当前的二进制目录添加到include目录列表中,以便mysqrt.cxx可以找到并包含Table.h

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
          )

现在,我们来使用生成的表。 首先,修改mysqrt.cxx以包含Table.h。 接下来,我们可以重写mysqrt函数以使用该表:

double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  // use the table to help find an initial value
  double result = x;
  if (x >= 1 && x < 10) {
    std::cout << "Use the table to help find an initial value " << std::endl;
    result = sqrtTable[static_cast<int>(x)];
  }

  // do ten iterations
  for (int i = 0; i < 10; ++i) {
    if (result <= 0) {
      result = 0.1;
    }
    double delta = x - (result * result);
    result = result + 0.5 * delta / result;
    std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  }

  return result;
}

运行cmake或cmake-gui以配置项目,然后使用所选的构建工具进行构建。构建此项目时,它将首先构建MakeTable可执行文件。 然后它将运行MakeTable来生成Table.h。 最后,它将编译包括了Table.hmysqrt.cxx,以生成MathFunctions库。运行Tutorial可执行文件,并验证它是否正在使用该表。

(第7步)构建一个安装程序

接下来,假设我们想将项目分发给其他人,以便他们可以使用它。 我们希望在各种平台上提供二进制和源代码。 这与我们之前在“安装和测试”(第4步)中进行的安装有些不同,在“安装和测试”中,我们是安装根据源代码构建的二进制文件。 在此示例中,我们将构建支持二进制安装和包管理功能的安装程序包。 为此,我们将使用CPack创建平台特定的安装程序。 具体来说,我们需要在顶级CMakeLists.txt文件的底部添加几行。

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变量设置为存储该项目的许可证和版本信息的位置。 版本信息是在本教程的前面设置的,并且license.txt已包含在此步骤的顶级源目录中。

最后,我们包含CPack模块,该模块将使用这些变量和当前系统的其他一些属性来设置安装程序。

下一步是以常规方式构建项目,然后在其上运行CPack。 要构建二进制发行版,请从二进制目录运行:

cpack

要指定生成器,请使用-G选项。 对于多配置构建,请使用-C指定配置。 例如:

cpack -G ZIP -C Debug

要创建源码分发,您可以输入:

cpack --config CPackSourceConfig.cmake

或者,运行make package或在IDE中右键单击Package目标和Build Project

运行在二进制目录中找到的安装程序。 然后运行已安装的可执行文件,并验证其是否有效。

(第8步)添加Dashboard支持

添加支持以将测试结果提交到Dashboard非常容易。 我们已经在“测试支持”中为我们的项目定义了许多测试。 现在,我们只需要运行这些测试并将其提交到Dashboard即可。 为了包含对Dashboard的支持,我们在顶层CMakeLists.txt中包含了CTest模块。

# enable dashboard scripting
include(CTest)

替换

# enable testing 
enable_testing()

CTest模块将自动调用enable_testing(),因此我们可以将其从CMake文件中删除。

我们还需要在顶级目录中创建一个CTestConfig.cmake文件,在该目录中我们可以指定项目的名称以及提交Dashboard的位置。

set(CTEST_PROJECT_NAME "CMakeTutorial")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")

set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")
set(CTEST_DROP_SITE_CDASH TRUE)

CTest将在运行时读入该文件。 要创建一个简单的Dashboard,您可以运行cmake或cmake-gui来配置项目,但不构建它。 而是,将目录更改为二进制树,然后运行:

ctest [-VV] -D Experimental

请记住,对于多配置生成器(例如Visual Studio),必须指定配置类型:

ctest [-VV] -C Debug -D Experimental

或者,从IDE中构建Experimental目标。

ctest将构建和测试项目,并将结果提交给Kitware公共仪表板Dashboard。 Dashboard的结果将被上传到Kitware的公共Dashboard。

(第9步)混合静态和动态库

在本节中,我们将展示如何使用BUILD_SHARED_LIBS变量来控制add_library的默认行为,并允许控制如何构建没有显式类型(STATIC,SHARED,MODULE或OBJECT)的库。

为此,我们需要将BUILD_SHARED_LIBS添加到顶级CMakeLists.txt。 我们使用option命令,因为它允许用户可以选择该值是On还是Off。

接下来,我们将重构MathFunctions使其成为使用mysqrt或sqrt封装的真实库,而不是要求调用代码执行此逻辑。 这也意味着USE_MYMATH将不会控制构建MathFuctions,而是将控制此库的行为。

第一步是将顶级CMakeLists.txt的开始部分更新为:

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# control where the static and shared libraries are built so that on windows
# we don't need to tinker with the path to run the executable
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

# configure a header file to pass the version number only
configure_file(TutorialConfig.h.in TutorialConfig.h)

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)

既然我们已经使MathFunctions始终被使用,我们将需要更新该库的逻辑。 因此,在MathFunctions/CMakeLists.txt中,我们需要创建一个SqrtLibrary,当启用USE_MYMATH时有条件地对其进行构建。 现在,由于这是一个教程,我们将明确要求SqrtLibrary是静态构建的。

最终结果是MathFunctions/CMakeLists.txt应该如下所示:

# add the library that runs
add_library(MathFunctions MathFunctions.cxx)

# state that anybody linking to us needs to include the current source dir
# to find MathFunctions.h, while we don't.
target_include_directories(MathFunctions
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )

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

  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

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

  # library that just does sqrt
  add_library(SqrtLibrary STATIC
              mysqrt.cxx
              ${CMAKE_CURRENT_BINARY_DIR}/Table.h
              )

  # state that we depend on our binary dir to find Table.h
  target_include_directories(SqrtLibrary PRIVATE
                             ${CMAKE_CURRENT_BINARY_DIR}
                             )

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()

# define the symbol stating we are using the declspec(dllexport) when
# building on windows
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")

# install rules
install(TARGETS MathFunctions DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

接下来,更新MathFunctions/mysqrt.cxx以使用mathfunctionsdetail命名空间:

#include <iostream>

#include "MathFunctions.h"

// include the generated table
#include "Table.h"

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

  // use the table to help find an initial value
  double result = x;
  if (x >= 1 && x < 10) 
  {
    std::cout << "Use the table to help find an initial value " << std::endl;
    result = sqrtTable[static_cast<int>(x)];
  }

  // do ten iterations
  for (int i = 0; i < 10; ++i)
  {
    if (result <= 0) 
      result = 0.1;
    double delta = x - (result * result);
    result = result + 0.5 * delta / result;
    std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  }

  return result;
}
}
}

我们还需要在tutorial.cxx中进行一些更改,以使其不再使用USE_MYMATH

  1. 始终包含MathFunctions.h
  2. 始终使用mathfunctions::sqrt
  3. 不要包含cmath

最后,更新 MathFunctions/MathFunctions.h以使用dll导出定义:

#if defined(_WIN32)
#  if defined(EXPORTING_MYMATH)
#    define DECLSPEC __declspec(dllexport)
#  else
#    define DECLSPEC __declspec(dllimport)
#  endif
#else // non windows
#  define DECLSPEC
#endif

namespace mathfunctions {
double DECLSPEC sqrt(double x);
}

此时,如果您构建了所有内容,则会注意到链接失败,因为我们将没有位置独立代码的静态库与具有位置独立代码的库组合在一起。 解决方案是无论构建类型如何,都将SqrtLibrary的POSITION_INDEPENDENT_CODE目标属性显式设置为True。

# state that SqrtLibrary need PIC when the default is shared libraries
  set_target_properties(SqrtLibrary PROPERTIES
                        POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
                        )

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary)

练习:我们修改了MathFunctions.h以使用dll导出定义。 使用CMake文档,您可以找到一个帮助器模块来简化此过程吗?

(第10步)添加生成器表达式

在构建系统生成期间会评估生成器表达式,以生成特定于每个构建配置的信息。

在许多目标属性(例如LINK_LIBRARIES,INCLUDE_DIRECTORIES,COMPLIE_DEFINITIONS等)的上下文中允许生成器表达式。 在使用命令填充这些属性(例如target_link_libraries(),target_include_directories() ,target_compile_definitions()等)时,也可以使用它们。

生成器表达式可用于启用条件链接,编译时使用的条件定义,条件包含目录等。 条件可以基于构建配置,目标属性,平台信息或任何其他可查询信息。

生成器表达式有不同类型,包括逻辑,信息和输出表达式。

逻辑表达式用于创建条件输出。 基本表达式是0和1表达式。$<0:...>导致空字符串,而<1:...>导致内容“…”。 它们也可以嵌套。

生成器表达式的常见用法是有条件地添加编译器标志,例如用于语言级别或警告的标志。 一个不错的模式是将该信息与一个INTERFACE目标相关联,以允许该信息传播。 让我们从构造一个INTERFACE目标并指定所需的C++标准级别11开始,而不是使用CMAKE_CXX_STANDARD

所以,下面的代码:

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

将被替换为:

add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

接下来,我们为项目添加所需的编译器警告标志。 由于警告标志根据编译器的不同而不同,因此我们使用COMPILE_LANG_AND_ID生成器表达式来控制在给定一种语言和一组编译器ID的情况下应应用的标志,如下所示:

set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(tutorial_compiler_flags INTERFACE
 "$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
 "$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)

查看此内容,我们看到警告标志封装在BUILD_INTERFACE条件内。 这样做是为了使我们已安装项目的使用者不会继承我们的警告标志。

练习:修改MathFunctions/CMakeLists.txt,以便所有目标都具有对tutorial_compiler_flags target_link_libraries()调用。

(第11步)增加输出配置

在本教程的“(第4步)安装和测试”中,我们添加了CMake的功能,以安装项目的库和头文件。 在"(第7步)构建安装程序"期间,我们添加了打包此资料的功能,以便可以将其分发给其他人。

下一步是添加必要的信息,以便其他CMake项目可以使用我们的项目,无论是从构建目录,本地安装还是打包的文件。

第一步是更新我们的install(TARGETS)命令,不仅要指定DESTINATION,还要指定EXPORTEXPORT关键字生成并安装一个CMake文件,该文件包含用于从安装树中导入install命令中列出的所有目标的代码。 因此,让我们继续,通过更新MathFunctions/CMakeLists.txt中的install命令显式EXPORTMathFunctions库,如下所示:

install(TARGETS MathFunctions tutorial_compiler_flags
        DESTINATION lib
        EXPORT MathFunctionsTargets)
install(FILES MathFunctions.h DESTINATION include)

现在我们已经导出了MathFunctions,我们还需要显式安装生成的MathFunctionsTargets.cmake文件。 这是通过将以下内容添加到顶级CMakeLists.txt的底部来完成的:

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)

此时,您应该尝试运行CMake。 如果一切设置正确,您将看到CMake将生成如下错误:

Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
path:

  "/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions"

which is prefixed in the source directory.

CMake试图说的是,在生成导出信息的过程中,它将导出与当前机器固有联系的路径,并且在其他机器上无效。 解决方案是更新MathFunctionstarget_include_directories,以了解从构建目录和install/包中使用它时需要不同的INTERFACE位置。 这意味着将MathFunctions的target_include_directories调用转换为:

target_include_directories(MathFunctions
                           INTERFACE
                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                            $<INSTALL_INTERFACE:include>
                           )

更新后,我们可以重新运行CMake并确认它不再发出警告。

至此,我们已经正确地打包了CMake所需的目标信息,但仍然需要生成MathFunctionsConfig.cmake,以便CMake find_package命令可以找到我们的项目。 因此,我们继续将名为Config.cmake.in新文件添加到项目顶层项目的顶层目录,其内容如下:

@PACKAGE_INIT@
include ( "${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake" )

然后,要正确配置和安装该文件,请将以下内容添加到顶级CMakeLists.txt的底部:

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)

include(CMakePackageConfigHelpers)
# generate the config file that is includes the exports
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
  INSTALL_DESTINATION "lib/cmake/example"
  NO_SET_AND_CHECK_MACRO
  NO_CHECK_REQUIRED_COMPONENTS_MACRO
  )
# generate the version file for the config file
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  COMPATIBILITY AnyNewerVersion
)

# install the configuration file
install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
  DESTINATION lib/cmake/MathFunctions
  )

至此,我们为项目生成了可重定位的CMake配置,可以在安装或打包项目后使用它。 如果我们也希望从构建目录中使用我们的项目,则只需将以下内容添加到顶级CMakeLists.txt的底部:

export(EXPORT MathFunctionsTargets
  FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

通过此导出调用,我们现在生成一个Targets.cmake,允许在构建目录中配置的MathFunctionsConfig.cmake由其他项目使用,而无需安装它。

导入一个CMake项目(消费者)

本示例说明项目如何查找生成Config.cmake文件的其他CMake软件包。

它还显示了在生成Config.cmake时如何声明项目的外部依赖关系。

打包调试和发布(多个包)

默认情况下,CMake的模型是一个构建目录仅包含一个配置,可以是Debug,Release,MinSizeRel或RelWithDebInfo。

但是可以将CPack设置为同时捆绑多个构建目录,以构建一个包含同一项目的多个配置的软件包。

首先,我们需要构建一个名为multi_config的目录,该目录将包含我们要打包在一起的所有构建。

其次,在multi_config下创建一个debugrelease目录。 最后,您应该具有如下布局:

─ multi_config
    ├── debug
    └── release

现在,我们需要设置调试和发布版本,这大致需要以下内容:

cmake -DCMAKE_BUILD_TYPE=Debug ../../MultiPackage/
cmake --build .
cd ../release
cmake -DCMAKE_BUILD_TYPE=Release ../../MultiPackage/
cmake --build .
cd ..

既然调试和发行版本均已完成,我们就可以使用自定义的MultiCPackConfig.cmake文件将两个版本打包到一个发行版中。

cpack --config ../../MultiPackage/MultiCPackConfig.cmake

二、Cmake入门案例

2.1单个源文件

本节对应的源代码所在目录:Demo1。对于简单的项目,只需要写几行代码就可以了。例如,假设现在我们的项目中只有一个源文件 http://main.cc ,该程序的用途是计算一个数的指数幂。

#include <stdio.h>
#include <stdlib.h>

/**
 * power - Calculate the power of number.
 * @param base: Base value.
 * @param exponent: Exponent value.
 *
 * @return base raised to the power exponent.
 */
double power(double base, int exponent)
{
    int result = base;
    int i;
    
    for(i = 1; i < exponent; ++i){
        result = result * base;
    }

    return result;
}

int main(int argc, char *argv[])
{
    if (argc < 3){
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    double result = power(base, exponent);
    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}

编写 CMakeLists.txt

首先编写 CMakeLists.txt 文件,并保存在与 http://main.cc 源文件同个目录下:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo1)

# 指定生成目标
add_executable(Demo main.cc)

CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的。符号 # 后面的内容被认为是注释。命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔。

对于上面的 CMakeLists.txt 文件,依次出现了几个命令:

  1. cmake_minimum_required:指定运行此配置文件所需的 CMake 的最低版本;
  2. project:参数是 main,该命令表示项目的名称是 main 。
  3. add_executable: 将名为 main.cc 的源文件编译成一个名称为 Demo 的可执行文件。

编译项目

之后,在当前目录执行 cmake . ,得到 Makefile 后再使用 make 命令编译得到 Demo1 可执行文件。

[ehome@xman Demo1]$ cmake .
-- The C compiler identification is GNU 4.8.2
-- The CXX compiler identification is GNU 4.8.2
-- Check for working C compiler: /usr/sbin/cc
-- Check for working C compiler: /usr/sbin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/sbin/c++
-- Check for working CXX compiler: /usr/sbin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ehome/Documents/programming/C/power/Demo1
[ehome@xman Demo1]$ make
Scanning dependencies of target Demo
[100%] Building C object CMakeFiles/Demo.dir/main.cc.o
Linking C executable Demo
[100%] Built target Demo
[ehome@xman Demo1]$ ./Demo 5 4
5 ^ 4 is 625
[ehome@xman Demo1]$ ./Demo 7 3
7 ^ 3 is 343
[ehome@xman Demo1]$ ./Demo 2 10
2 ^ 10 is 1024

2.2多个源文件

同一目录,多个源文件:本小节对应的源代码所在目录:Demo2。

上面的例子只有单个源文件。现在假如把 power 函数单独写进一个名为 MathFunctions.c 的源文件里,使得这个工程变成如下的形式:

/Demo2
    |
    +--- main.cc
    |
    +--- MathFunctions.cc
    |
    +--- MathFunctions.h

这个时候,CMakeLists.txt 可以改成如下的形式:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo2)

# 指定生成目标
add_executable(Demo main.cc MathFunctions.cc)

唯一的改动只是在 add_executable 命令中增加了一个 MathFunctions.cc 源文件。这样写当然没什么问题,但是如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作。更省事的方法是使用 aux_source_directory 命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。其语法如下:

aux_source_directory(<dir> <variable>)

因此,可以修改 CMakeLists.txt 如下:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo2)

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 指定生成目标
add_executable(Demo ${DIR_SRCS})

这样,CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRCS ,再指示变量 DIR_SRCS 中的源文件需要编译成一个名称为 Demo 的可执行文件。

2.3多个目录,多个源文件

本小节对应的源代码所在目录:Demo3。

现在进一步将 MathFunctions.h 和 http://MathFunctions.cc 文件移动到 math 目录下。

./Demo3
    |
    +--- main.cc
    |
    +--- math/
          |
          +--- MathFunctions.cc
          |
          +--- MathFunctions.h

对于这种情况,需要分别在项目根目录 Demo3 和 math 目录里各编写一个 CMakeLists.txt 文件。为了方便,我们可以先将 math 目录里的文件编译成静态库再由 main 函数调用。

根目录中的 CMakeLists.txt :

# 项目信息
project (Demo3)

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 添加 math 子目录
add_subdirectory(math)

# 指定生成目标 
add_executable(Demo main.cc)

# 添加链接库
target_link_libraries(Demo MathFunctions)

该文件添加了下面的内容: 第3行,使用命令 add_subdirectory 指明本项目包含一个子目录 math,这样 math 目录下的 CMakeLists.txt 文件和源代码也会被处理 。第6行,使用命令 target_link_libraries 指明可执行文件 main 需要连接一个名为 MathFunctions 的链接库 。

子目录中的 CMakeLists.txt:

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)

# 生成链接库
add_library (MathFunctions ${DIR_LIB_SRCS})

在该文件中使用命令 add_library 将 src 目录中的源文件编译为静态链接库。

2.4自定义编译选项

本节对应的源代码所在目录:Demo4。

CMake 允许为项目增加编译选项,从而可以根据用户的环境和需求选择最合适的编译方案。

例如,可以将 MathFunctions 库设为一个可选的库,如果该选项为 ON ,就使用该库定义的数学函数来进行运算。否则就调用标准库中的数学函数库。

修改 CMakeLists 文件

我们要做的第一步是在顶层的 CMakeLists.txt 文件中添加该选项:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo4)

# 加入一个配置头文件,用于处理 CMake 对源码的设置
configure_file (
  "${PROJECT_SOURCE_DIR}/config.h.in"
  "${PROJECT_BINARY_DIR}/config.h"
  )

# 是否使用自己的 MathFunctions 库
option (USE_MYMATH
       "Use provided math implementation" ON)

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

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 指定生成目标
add_executable(Demo ${DIR_SRCS})
target_link_libraries (Demo  ${EXTRA_LIBS})

其中:

  1. 第7行的 configure_file 命令用于加入一个配置头文件 config.h ,这个文件由 CMake 从 config.h.in 生成,通过这样的机制,将可以通过预定义一些参数和变量来控制代码的生成。
  2. 第13行的 option 命令添加了一个 USE_MYMATH 选项,并且默认值为 ON
  3. 第17行根据 USE_MYMATH 变量的值来决定是否使用我们自己编写的 MathFunctions 库。

修改 http://main.cc 文件

之后修改 http://main.cc 文件,让其根据 USE_MYMATH 的预定义值来决定是否调用标准库还是 MathFunctions 库:

#include <stdio.h>
#include <stdlib.h>
#include "config.h"

#ifdef USE_MYMATH
  #include "math/MathFunctions.h"
#else
  #include <math.h>
#endif


int main(int argc, char *argv[])
{
    if (argc < 3){
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    
#ifdef USE_MYMATH
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#else
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#endif
    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}

编写 http://config.h.in 文件

上面的程序值得注意的是第2行,这里引用了一个 config.h 文件,这个文件预定义了 USE_MYMATH 的值。但我们并不直接编写这个文件,为了方便从 CMakeLists.txt 中导入配置,我们编写一个 config.h.in 文件,内容如下:

#cmakedefine USE_MYMATH

这样 CMake 会自动根据 CMakeLists 配置文件中的设置自动生成 config.h 文件。

编译项目

现在编译一下这个项目,为了便于交互式的选择该变量的值,可以使用 ccmake 命令 2 2 也可以使用 cmake -i 命令,该命令会提供一个会话式的交互式配置界面。

img

从中可以找到刚刚定义的 USE_MYMATH 选项,按键盘的方向键可以在不同的选项窗口间跳转,按下 enter 键可以修改该选项。修改完成后可以按下 c 选项完成配置,之后再按 g 键确认生成 Makefile 。ccmake 的其他操作可以参考窗口下方给出的指令提示。

我们可以试试分别将 USE_MYMATH 设为 ONOFF 得到的结果:

USE_MYMATH 为 ON

运行结果:

[ehome@xman Demo4]$ ./Demo
Now we use our own MathFunctions library. 
 7 ^ 3 = 343.000000
 10 ^ 5 = 100000.000000
 2 ^ 10 = 1024.000000

此时 config.h 的内容为:

#define USE_MYMATH

USE_MYMATH 为 OFF

运行结果:

[ehome@xman Demo4]$ ./Demo
Now we use the standard library. 
 7 ^ 3 = 343.000000
 10 ^ 5 = 100000.000000
 2 ^ 10 = 1024.000000

此时 config.h 的内容为:

/* #undef USE_MYMATH */

2.5安装和测试

本节对应的源代码所在目录:Demo5。

CMake 也可以指定安装规则,以及添加测试。这两个功能分别可以通过在产生 Makefile 后使用 make installmake test 来执行。在以前的 GNU Makefile 里,你可能需要为此编写 installtest 两个伪目标和相应的规则,但在 CMake 里,这样的工作同样只需要简单的调用几条命令。

定制安装规则

首先先在 math/CMakeLists.txt 文件里添加下面两行:

# 指定 MathFunctions 库的安装路径
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

指明 MathFunctions 库的安装路径。之后同样修改根目录的 CMakeLists 文件,在末尾添加下面几行:

# 指定安装路径
install (TARGETS Demo DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/config.h"
         DESTINATION include)

通过上面的定制,生成的 Demo 文件和 MathFunctions 函数库 libMathFunctions.o 文件将会被复制到 /usr/local/bin 中,而 MathFunctions.h 和生成的 config.h 文件则会被复制到 /usr/local/include 中。我们可以验证一下33顺带一提的是,这里的 /usr/local/ 是默认安装到的根目录,可以通过修改 CMAKE_INSTALL_PREFIX 变量的值来指定这些文件应该拷贝到哪个根目录。

[ehome@xman Demo5]$ sudo make install
[ 50%] Built target MathFunctions
[100%] Built target Demo
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/bin/Demo
-- Installing: /usr/local/include/config.h
-- Installing: /usr/local/bin/libMathFunctions.a
-- Up-to-date: /usr/local/include/MathFunctions.h
[ehome@xman Demo5]$ ls /usr/local/bin
Demo  libMathFunctions.a
[ehome@xman Demo5]$ ls /usr/local/include
config.h  MathFunctions.h

为工程添加测试

添加测试同样很简单。CMake 提供了一个称为 CTest 的测试工具。我们要做的只是在项目根目录的 CMakeLists 文件中调用一系列的 add_test 命令。

# 启用测试
enable_testing()

# 测试程序是否成功运行
add_test (test_run Demo 5 2)

# 测试帮助信息是否可以正常提示
add_test (test_usage Demo)
set_tests_properties (test_usage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent")

# 测试 5 的平方
add_test (test_5_2 Demo 5 2)

set_tests_properties (test_5_2
 PROPERTIES PASS_REGULAR_EXPRESSION "is 25")

# 测试 10 的 5 次方
add_test (test_10_5 Demo 10 5)

set_tests_properties (test_10_5
 PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")

# 测试 2 的 10 次方
add_test (test_2_10 Demo 2 10)

set_tests_properties (test_2_10
 PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")

上面的代码包含了四个测试。第一个测试 test_run 用来测试程序是否成功运行并返回 0 值。剩下的三个测试分别用来测试 5 的 平方、10 的 5 次方、2 的 10 次方是否都能得到正确的结果。其中 PASS_REGULAR_EXPRESSION 用来测试输出是否包含后面跟着的字符串。

让我们看看测试的结果:

[ehome@xman Demo5]$ make test
Running tests...
Test project /home/ehome/Documents/programming/C/power/Demo5
    Start 1: test_run
1/4 Test #1: test_run .........................   Passed    0.00 sec
    Start 2: test_5_2
2/4 Test #2: test_5_2 .........................   Passed    0.00 sec
    Start 3: test_10_5
3/4 Test #3: test_10_5 ........................   Passed    0.00 sec
    Start 4: test_2_10
4/4 Test #4: test_2_10 ........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) =   0.01 sec

如果要测试更多的输入数据,像上面那样一个个写测试用例未免太繁琐。这时可以通过编写宏来实现:

# 定义一个宏,用来简化测试工作
macro (do_test arg1 arg2 result)
  add_test (test_${arg1}_${arg2} Demo ${arg1} ${arg2})
  set_tests_properties (test_${arg1}_${arg2}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)
 
# 使用该宏进行一系列的数据测试
do_test (5 2 "is 25")
do_test (10 5 "is 100000")
do_test (2 10 "is 1024")

关于 CTest 的更详细的用法可以通过 man 1 ctest 参考 CTest 的文档。

支持 gdb

让 CMake 支持 gdb 的设置也很容易,只需要指定 Debug 模式下开启 -g 选项:

set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

之后可以直接对生成的程序使用 gdb 来调试。

2.6添加环境检查

本节对应的源代码所在目录:Demo6。

有时候可能要对系统环境做点检查,例如要使用一个平台相关的特性的时候。在这个例子中,我们检查系统是否自带 pow 函数。如果带有 pow 函数,就使用它;否则使用我们定义的 power 函数。

添加 CheckFunctionExists 宏

首先在顶层 CMakeLists 文件中添加 CheckFunctionExists.cmake 宏,并调用 check_function_exists 命令测试链接器是否能够在链接阶段找到 pow 函数。

# 检查系统是否支持 pow 函数
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (pow HAVE_POW)

将上面这段代码放在 configure_file 命令前。

预定义相关宏变量

接下来修改 http://config.h.in 文件,预定义相关的宏变量。

// does the platform provide pow function?
#cmakedefine HAVE_POW

在代码中使用宏和函数

最后一步是修改 http://main.cc ,在代码中使用宏和函数:

#ifdef HAVE_POW
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#else
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#endif

2.8添加版本号

本节对应的源代码所在目录:Demo7。

给项目添加和维护版本号是一个好习惯,这样有利于用户了解每个版本的维护情况,并及时了解当前所用的版本是否过时,或是否可能出现不兼容的情况。

首先修改顶层 CMakeLists 文件,在 project 命令之后加入如下两行:

set (Demo_VERSION_MAJOR 1)
set (Demo_VERSION_MINOR 0)

分别指定当前的项目的主版本号和副版本号。

之后,为了在代码中获取版本信息,我们可以修改 http://config.h.in 文件,添加两个预定义变量:

// the configured options and settings for Tutorial
#define Demo_VERSION_MAJOR @Demo_VERSION_MAJOR@
#define Demo_VERSION_MINOR @Demo_VERSION_MINOR@

这样就可以直接在代码中打印版本信息了:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#include "math/MathFunctions.h"

int main(int argc, char *argv[])
{
    if (argc < 3){
        // print version info
        printf("%s Version %d.%d\n",
            argv[0],
            Demo_VERSION_MAJOR,
            Demo_VERSION_MINOR);
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    
#if defined (HAVE_POW)
    printf("Now we use the standard library. \n");
    double result = pow(base, exponent);
#else
    printf("Now we use our own Math library. \n");
    double result = power(base, exponent);
#endif
    
    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}

2.9生成安装包

本节对应的源代码所在目录:Demo8。

本节将学习如何配置生成各种平台上的安装包,包括二进制安装包和源码安装包。为了完成这个任务,我们需要用到 CPack ,它同样也是由 CMake 提供的一个工具,专门用于打包。

首先在顶层的 CMakeLists.txt 文件尾部添加下面几行:

# 构建一个 CPack 安装包
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE
  "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Demo_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Demo_VERSION_MINOR}")
include (CPack)

上面的代码做了以下几个工作:

  1. 导入 InstallRequiredSystemLibraries 模块,以便之后导入 CPack 模块;
  2. 设置一些 CPack 相关变量,包括版权信息和版本信息,其中版本信息用了上一节定义的版本号;
  3. 导入 CPack 模块。

接下来的工作是像往常一样构建工程,并执行 cpack 命令。

生成二进制安装包:

cpack -C CPackConfig.cmake

生成源码安装包

cpack -C CPackSourceConfig.cmake

我们可以试一下。在生成项目后,执行 cpack -C CPackConfig.cmake 命令:

[ehome@xman Demo8]$ cpack -C CPackSourceConfig.cmake
CPack: Create package using STGZ
CPack: Install projects
CPack: - Run preinstall target for: Demo8
CPack: - Install project: Demo8
CPack: Create package
CPack: - package: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.sh generated.
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: Demo8
CPack: - Install project: Demo8
CPack: Create package
CPack: - package: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.tar.gz generated.
CPack: Create package using TZ
CPack: Install projects
CPack: - Run preinstall target for: Demo8
CPack: - Install project: Demo8
CPack: Create package
CPack: - package: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.tar.Z generated.

此时会在该目录下创建 3 个不同格式的二进制包文件:

[ehome@xman Demo8]$ ls Demo8-*
Demo8-1.0.1-Linux.sh  Demo8-1.0.1-Linux.tar.gz  Demo8-1.0.1-Linux.tar.Z

这 3 个二进制包文件所包含的内容是完全相同的。我们可以执行其中一个。此时会出现一个由 CPack 自动生成的交互式安装界面:

[ehome@xman Demo8]$ sh Demo8-1.0.1-Linux.sh 
Demo8 Installer Version: 1.0.1, Copyright (c) Humanity
This is a self-extracting archive.
The archive will be extracted to: /home/ehome/Documents/programming/C/power/Demo8

If you want to stop extracting, please press <ctrl-C>.
The MIT License (MIT)

Copyright (c) 2013 Joseph Pan(http://hahack.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Do you accept the license? [yN]: 
y
By default the Demo8 will be installed in:
  "/home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux"
Do you want to include the subdirectory Demo8-1.0.1-Linux?
Saying no will install in: "/home/ehome/Documents/programming/C/power/Demo8" [Yn]: 
y

Using target directory: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux
Extracting, please wait...

Unpacking finished successfully

完成后提示安装到了 Demo8-1.0.1-Linux 子目录中,我们可以进去执行该程序:

[ehome@xman Demo8]$ ./Demo8-1.0.1-Linux/bin/Demo 5 2
Now we use our own Math library. 
5 ^ 2 is 25

关于 CPack 的更详细的用法可以通过 man 1 cpack 参考 CPack 的文档。

将其他平台的项目迁移到 CMake:

CMake 可以很轻松地构建出在适合各个平台执行的工程环境。而如果当前的工程环境不是 CMake ,而是基于某个特定的平台,是否可以迁移到 CMake 呢?答案是可能的。下面针对几个常用的平台,列出了它们对应的迁移方案。

autotools

  • am2cmake 可以将 autotools 系的项目转换到 CMake,这个工具的一个成功案例是 KDE 。
  • Alternative Automake2CMake 可以转换使用 automake 的 KDevelop 工程项目。
  • Converting autoconf tests

qmake

  • qmake converter 可以转换使用 QT 的 qmake 的工程。

Visual Studio

  • vcproj2cmake.rb 可以根据 Visual Studio 的工程文件(后缀名是 .vcproj.vcxproj)生成 CMakeLists.txt 文件。
  • vcproj2cmake.ps1 vcproj2cmake 的 PowerShell 版本。
  • folders4cmake 根据 Visual Studio 项目文件生成相应的 “source_group” 信息,这些信息可以很方便的在 CMake 脚本中使用。支持 Visual Studio 9/10 工程文件。

CMakeLists.txt 自动推导

  • gencmake 根据现有文件推导 CMakeLists.txt 文件。
  • CMakeListGenerator 应用一套文件和目录分析创建出完整的 CMakeLists.txt 文件。仅支持 Win32 平台。

版权声明:本文为知乎博主「玩转Linux内核」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
o whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Do you accept the license? [yN]:
y
By default the Demo8 will be installed in:
“/home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux”
Do you want to include the subdirectory Demo8-1.0.1-Linux?
Saying no will install in: “/home/ehome/Documents/programming/C/power/Demo8” [Yn]:
y

Using target directory: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux
Extracting, please wait…

Unpacking finished successfully


完成后提示安装到了 Demo8-1.0.1-Linux 子目录中,我们可以进去执行该程序:

```text
[ehome@xman Demo8]$ ./Demo8-1.0.1-Linux/bin/Demo 5 2
Now we use our own Math library. 
5 ^ 2 is 25

关于 CPack 的更详细的用法可以通过 man 1 cpack 参考 CPack 的文档。

将其他平台的项目迁移到 CMake:

CMake 可以很轻松地构建出在适合各个平台执行的工程环境。而如果当前的工程环境不是 CMake ,而是基于某个特定的平台,是否可以迁移到 CMake 呢?答案是可能的。下面针对几个常用的平台,列出了它们对应的迁移方案。

autotools

  • am2cmake 可以将 autotools 系的项目转换到 CMake,这个工具的一个成功案例是 KDE 。
  • Alternative Automake2CMake 可以转换使用 automake 的 KDevelop 工程项目。
  • Converting autoconf tests

qmake

  • qmake converter 可以转换使用 QT 的 qmake 的工程。

Visual Studio

  • vcproj2cmake.rb 可以根据 Visual Studio 的工程文件(后缀名是 .vcproj.vcxproj)生成 CMakeLists.txt 文件。
  • vcproj2cmake.ps1 vcproj2cmake 的 PowerShell 版本。
  • folders4cmake 根据 Visual Studio 项目文件生成相应的 “source_group” 信息,这些信息可以很方便的在 CMake 脚本中使用。支持 Visual Studio 9/10 工程文件。

CMakeLists.txt 自动推导

  • gencmake 根据现有文件推导 CMakeLists.txt 文件。
  • CMakeListGenerator 应用一套文件和目录分析创建出完整的 CMakeLists.txt 文件。仅支持 Win32 平台。

版权声明:本文为知乎博主「玩转Linux内核」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接: https://zhuanlan.zhihu.com/p/638429917

猜你喜欢

转载自blog.csdn.net/m0_50662680/article/details/131484268