前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万字总结编译利器CMake,从入门到项目实战演练

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

原创
作者头像
嵌入式Linux内核
发布2023-06-28 21:58:28
1.1K0
发布2023-06-28 21:58:28
举报
文章被收录于专栏:用户10025783的专栏

一、什么是 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(https://cmake.org/)

1.1CMake教程

基本起点(步骤 1)

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

代码语言:javascript
复制
cmake_minimum_required(版本2.6)
项目(教程)
add_executable(教程tutorial.cxx)

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

代码语言:javascript
复制
// 一个计算数字平方根的简单程序
#include <stdio.h> 
#include <stdlib.h> 
#include <math.h> 
int main (int argc, char *argv[]) 
{ 
  if (argc < 2) 
    { 
    fprintf(stdout,"用法: %s 数字\n",argv[0]);
    返回1;
    双
  输入值 = atof(argv[1]);
  双输出值 = sqrt(输入值); 
  fprintf(stdout,"%g 的平方根是 %g\n", 
          inputValue, outputValue);
  返回0;
}

添加版本号和配置的头文件

我们第一个要加入的特性是,在工程和可执行程序上加一个版本号。虽然你可以直接在源代码里面这么做,然而如果用CMakeLists文件来做的话会提供更多的灵活性。为了增加版本号,我们可以如此更改CMakeLists文件:

代码语言:javascript
复制
cmake_minimum_required (VERSION 2.6)
项目 (教程) 
# 版本号。
set (Tutorial_VERSION_MAJOR 1) 
set (Tutorial_VERSION_MINOR 0) 
 
# 配置头文件以将一些 CMake 设置传递
给源代码 # 
configure_file ( 
  "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in" 
  "${PROJECT_BINARY_DIR}/TutorialConfig.h " 
  ) 
 
# 将二叉树添加到包含文件的搜索路径中
# 这样我们就可以找到TutorialConfig.h 
include_directories("${PROJECT_BINARY_DIR}") 
 
# 添加可执行文件
add_executable(Tutorialtutorial.cxx)

由于配置文件必须写到binary tree中,因此我们必须将这个目录添加到头文件搜索目录中。我们接下来在源码目录中创建了TutorialConfig.h.in文件,其内容如下:

代码语言:javascript
复制
// 教程的配置选项和设置
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@ 
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

当CMake配置了这个头文件, @Tutorial_VERSION_MAJOR@ 和 @Tutorial_VERSION_MINOR@ 的值将会被改变。接下来,我们修改了tutorial.cxx来包含配置的头文件并且使用版本号。最终的源代码如下所示:

代码语言:javascript
复制
// 一个计算数字平方根的简单程序
#include <stdio.h> 
#include <stdlib.h> 
#include <math.h> 
#include "TutorialConfig.h" 
 
int main (int argc, char *argv []) 
{ 
  if (argc < 2) 
    { 
    fprintf(stdout,"%s 版本 %d.%d\n", 
            argv[0], 
            Tutorial_VERSION_MAJOR, 
            Tutorial_VERSION_MINOR); 
    fprintf(stdout,"用法: %s 数字\n",argv[0]);
    返回1;
    双
  输入值 = atof(argv[1]);
  双输出值 = sqrt(输入值); 
  fprintf(stdout,"%g 的平方根是 %g\n", 
          inputValue, outputValue);
  返回0;
}

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

添加库(步骤 2)

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

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

代码语言:javascript
复制
add_library(MathFunctions mysqrt.cxx)

原文件mysqrt.cxx有一个叫做mysqrt的函数可以提供与编译器的sqrt相似的功能。为了使用新的库,我们需要在顶层的CMakeLists 文件中添加add_subdirectory的调用。我们也要添加一个另外的头文件搜索目录,使得MathFunctions/mysqrt.h可以被搜索到。最后的改变就是将新的库加到可执行程序中。顶层的CMakeLists 文件现在看起来是这样:

代码语言:javascript
复制
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions") 
add_subdirectory (MathFunctions) 
 
# 添加可执行
文件 add_executable (教程tutorial.cxx) 
target_link_libraries (教程MathFunctions)

现在我们来考虑如何使得MathFunctions库成为可选的。虽然在这个教程当中没有什么理由这么做,然而如果使用更大的库或者当依赖于第三方的库时,你或许希望这么做。第一步是要在顶层的CMakeLists文件中加上一个选择项。

代码语言:javascript
复制
# 我们应该使用我们自己的数学函数吗?选项(USE_MYMATH “使用教程提供的数学实现” ON )

这个选项会显示在CMake的GUI,并且其默认值为ON。当用户选择了之后,这个值会被保存在CACHE中,这样就不需要每次CMAKE都进行更改了。下面一步条件构建和链接MathFunctions库。为了达到这个目的,我们可以改变顶层的CMakeLists文件,使得其看起来像这样:

代码语言:javascript
复制
# 添加 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.cxx) 
target_link_libraries (教程${EXTRA_LIBS) })

这里使用了USE_MYMATH来决定MathFunctions是否会被编译和使用。注意这里变量EXTRA_LIBS的使用方法。这是保持一个大的项目看起来比较简洁的一个方法。源代码中相应的变化就比较简单了:

代码语言:javascript
复制
// 一个计算数字平方根的简单程序
#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 版本 %d.%d\n", argv[0], 
            Tutorial_VERSION_MAJOR, 
            Tutorial_VERSION_MINOR); 
    fprintf(stdout,"用法: %s 数字\n",argv[0]);
    返回1;
    双
 
  输入值 = atof(argv[1]); 
 
#ifdef USE_MYMATH
  双输出值 = mysqrt(inputValue); 
#别的
  双输出值 = sqrt(输入值); 
#endif 
 
  fprintf(stdout,"%g 的平方根是 %g\n", 
          inputValue, outputValue);
  返回0;
}

在源代码中我们同样使用了USE_MYMATH这个宏。它由CMAKE通过配置文件TutorialConfig.h.in来提供给源代码。

代码语言:javascript
复制
#cmake定义 USE_MYMATH

安装和测试(步骤 3)

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

代码语言:javascript
复制
安装(TARGETS MathFunctions DESTINATION bin)
安装(文件 MathFunctions.h DESTINATION 包括)

对于应用程序,我们只需要在顶层CMakeLists 文件中如此配置即可以安装可执行程序和配置了的头文件:

代码语言:javascript
复制
# 添加安装目标
install (TARGETS Tutorial DESTINATION bin) 
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"         
         DESTINATION include)

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

代码语言:javascript
复制
# 应用程序是否运行
add_test (TutorialRuns Tutorial 25) 
 
# 是否运行 25 的 sqrt 
add_test (TutorialComp25 Tutorial 25) 
 
set_tests_properties (TutorialComp25 
  PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5") 
 
# 是否处理负数
add_test (TutorialNegative Tutorial -25) 
set_tests_properties (TutorialNeg)主动的
  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 教程) 
set_tests_properties (TutorialUsage
  属性
  PASS_REGULAR_EXPRESSION "用法:.*number")

第一个测试简单地确认应用是否运行,没有段错误或者其它的崩溃问题,并且返回0。这是CTest的最基本的形式。下面的测试都使用了PASS_REGULAR_EXPRESSION测试属性来确认输出的结果中是否含有某个字符串。如果你需要添加大量的测试来判断不同的输入值,则你需要考虑创建一个类似于下面的宏:

代码语言:javascript
复制
#定义一个宏来简化添加测试,然后使用它
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的任意一次调用,就有另一个测试被添加到工程中。

添加系统自省(步骤 4)

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

代码语言:javascript
复制
# 这个系统提供log和exp功能吗?
包括 (CheckFunctionExists.cmake) 
check_function_exists (log HAVE_LOG) 
check_function_exists (exp HAVE_EXP)

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

代码语言:javascript
复制
// 平台是否提供exp和log函数?
#cmakedefine HAVE_LOG 
#cmakedefine HAVE_EXP

重要的一点是,对tests和log的测试必须要在配置文件命令前完成。配置文件命令会使用CMake中的配置立马配置文件。最后在mysqrt函数中我们提供了两种实现方式:

代码语言:javascript
复制
// 如果我们同时有 log 和 exp 则使用它们
#if Defined (HAVE_LOG) && Defined (HAVE_EXP) 
  result = exp(log(x)*0.5); 
#else // 否则使用迭代方法
  。 。 。

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

在这一节当中,我们会告诉你如何将一个生成的源文件加入到应用程序的构建过程中。在此例中,我们会创建一个预先计算好的平方根的表,并将这个表编译到应用程序中去。为了达到这个目的,我们首先需要一个程序来生成这样的表。在MathFunctions这个子目录下一个新的叫做MakeTable.cxx的源文件就是用来干这个的。

代码语言:javascript
复制
// 一个构建 sqrt 表的简单程序
#include <stdio.h> 
#include <stdlib.h> 
#include <math.h> 
 
int main (int argc, char *argv[]) 
{ 
  int i;
  双重结果;
 
  // 确保我们有足够的参数
  if (argc < 2) 
    { 
    return 1; 
    } 
  
  // 打开输出文件
  FILE *fout = fopen(argv[1],"w"); 
  if (!fout) 
    {
    返回 1; 
    } 
  
  // 创建一个包含平方根表的源文件
  fprintf(fout,"double sqrtTable[] = {\n"); 
  for (i = 0; i < 10; ++i) 
    {
    结果 = sqrt(static_cast<double>(i)); 
    fprintf(fout,"%g,\n",结果);
    } 
 
  // 以零关闭表
  fprintf(fout,"0};\n"); 
  fclose(fout);
  返回0;
}

注意到这张表是由一个有效的C++代码产生的,并且输出文件的名字是由参数代入的。下一步就是添加合适的命令到MathFunctions的CMakeLists文件中来构建MakeTable这个可执行程序,并且作为构建过程中的一部分。完成它需要一些命令,如下:

代码语言:javascript
复制
# 首先我们添加生成表的可执行文件 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。这是通过往MathFunctions这个库里面添加生成的Table.h来实现的。我们也需要添加当前的生成目录到搜索路径中,从而Table.h可以被mysqrt.cxx找到。

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

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

代码语言:javascript
复制
cmake_minimum_required (VERSION 2.6)
项目 (教程) 
 
# 版本号。
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) 
 
# 我们应该使用我们自己的数学函数
选项(USE_MYMATH 
  "使用教程提供的数学实现" 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.cxx) 
target_link_libraries (教程${EXTRA_LIBS} ) 
 
# 添加安装目标
install (TARGETS Tutorial DESTINATION bin) 
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"        
         DESTINATION include) 
 
# 应用程序是否运行
add_test (TutorialRuns 教程 25) 
 
# 使用消息有效吗?
add_test (TutorialUsage Tutorial) 
set_tests_properties (TutorialUsage 
  PROPERTIES 
  PASS_REGULAR_EXPRESSION "Usage:.*number" 
  ) 
 
 
#定义一个宏来简化添加测试
宏 (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 是 2.645") 
do_test(25 "25 是 5") 
do_test(-25“-25为0”)
do_test (0.0001 "0.0001 是 0.01")

TutorialConfig.h是这样的:

代码语言:javascript
复制
// 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文件看起来是这样的:

代码语言:javascript
复制
# 首先我们添加生成表的可执行文件
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 (文件 MathFunctions.h 目标包括)

构建安装程序(第 6 步)

最后假设我们想要把我们的工程发布给别人从而让他们去使用。我们想要同时给他们不同平台的二进制文件和源代码。这与步骤3中的install略有不同,install是安装我们从源代码中构建的二进制文件。而在此例中,我们将要构建安装包来支持二进制安装以及cygwin,debian,RPMs等的包管理特性。为了达到这个目的,我们会使用CPack来创建平台相关的安装包。具体地说,我们需要在顶层CMakeLists.txt文件中的底部添加数行。

代码语言:javascript
复制
# 构建 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_MIN"或}")
设置 (CPACK_PACKAGE_VERSION_PATCH "$ {Tutorial_VERSION_PATCH}")
包括 (CPack)

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

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

代码语言:javascript
复制
cpack --config CPackConfig.cmake

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

代码语言:javascript
复制
cpack --config CPackSourceConfig.cmake

添加对仪表板的支持(步骤 7)

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

代码语言:javascript
复制
# 启用仪表板脚本
包括 (CTest)

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

代码语言:javascript
复制
设置(CTEST_PROJECT_NAME“教程”)

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

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

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

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

1.2CMake指南教程(官方地址)

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

(第1步)基本起点

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

代码语言:javascript
复制
cmake_minimum_required(版本3.10)# 设置项目名称project(教程)# 添加可执行文件add_executable(教程tutorial.cxx)

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

添加版本号和配置头文件

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

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

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.10) 

# 设置项目名称和版本
project(Tutorial VERSION 1.0) 

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

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

代码语言:javascript
复制
configure_file(TutorialConfig.h.in TutorialConfig.h)

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

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

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

使用您喜欢的编辑器在源码目录中创建TutorialConfig.h.in,内容如下:

代码语言:javascript
复制
// 教程的配置选项和设置
#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来打印出版本号,如下所示:

代码语言:javascript
复制
if (argc < 2) { 
    // 报告版本
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "." 
              << Tutorial_VERSION_MINOR << std::endl; 
    std::cout << "用法:" << argv[0] << "数字" << std::endl;
    返回1;
  }

完整的CMakeLists.txt如下:

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.10) 

#设置项目名称和版本
project(Tutorial VERSION 1.0) 

configure_file(TutorialConfig.h.in TutorialConfig.h) 

#添加可执行
文件 add_executable(Tutorialtutorial.cxx) 

target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}" )

指定c++标准

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

代码语言:javascript
复制
const double inputValue = std::stod(argv[1]);

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

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.10) 
# 设置项目名称和版本
project(Tutorial VERSION 1.0) 
# 指定C++标准
集(CMAKE_CXX_STANDARD 11) 
set(CMAKE_CXX_STANDARD_REQUIRED True)

构建和测试

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

代码语言:javascript
复制
mkdir Step1_build 
cd Step1_build 
cmake ../Step1 
cmake --build .

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

代码语言:javascript
复制
教程 4294967296
教程 10
教程

(第2步)添加库

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

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

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

代码语言:javascript
复制
add_library(MathFunctions mysqrt.cxx)

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

代码语言:javascript
复制
# 添加 MathFunctions 库
add_subdirectory(MathFunctions) 

# 添加可执行
文件 add_executable(Tutorialtutorial.cxx) 

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

###早期版本的写
###target_link_libraries(Tutorial MathFunction) s) 

#添加二进制文件树到包含文件的搜索路径,以便我们找到TutorialConfig.h 
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")

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

代码语言:javascript
复制
option(USE_MYMATH "使用教程提供的数学实现" ON) 
# 配置头文件以将一些 CMake 设置传递到源代码
configure_file(TutorialConfig.h.in TutorialConfig.h)

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

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

代码语言:javascript
复制
if(USE_MYMATH) 
  add_subdirectory(MathFunctions) 
  list(APPEND EXTRA_LIBS MathFunctions) 
  list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions") 
endif() 

# 添加可执行
文件 add_executable(Tutorialtutorial.cxx) 

# 之后必须位于
add_executabletarget_link_lib raries(教程公开${EXTRA_LIBS}) 

# 将二叉树添加到包含文件的搜索路径中,以便我们找到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.cxx) 
###target_link_libraries(教程${EXTRA_LIBS})

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

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

代码语言:javascript
复制
#ifdef USE_MYMATH 
# 包含“MathFunctions.h” 
#endif

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

代码语言:javascript
复制
#ifdef USE_MYMATH 
  const 双输出值 = mysqrt(inputValue); 
#else 
  const double outputValue = sqrt(inputValue); 
#万一

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

代码语言:javascript
复制
#cmake定义 USE_MYMATH

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

完整的CMakeLists.txt文件如下:

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.5)                                                                                   
# 设置项目名称和版本
project(Tutorial VERSION 1.0) 
 
# 指定C++标准
集(CMAKE_CXX_STANDARD 11) 
set(CMAKE_CXX_STANDARD_REQUIRED True) 
 
option(USE_MYMATH "Use Tutorialprovided mathimplementation" ON) 
 
# 配置头文件将一些 CMake 设置传递到源代码
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_executable(Tutorial教程.cxx) 

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

# 将二叉树添加到包含文件的搜索路径中,以便我们找到TutorialConfig.h 
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR} “ ${EXTRA_INCLUDES})

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

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

  • 目标编译定义
  • 目标编译选项
  • 目标包含目录
  • 目标链接库

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

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

代码语言:javascript
复制
target_include_directories(MathFunctions 接口 ${CMAKE_CURRENT_SOURCE_DIR)

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

代码语言:javascript
复制
if(USE_MYMATH) 
  add_subdirectory(MathFunctions) 
  list(APPEND EXTRA_LIBS MathFunctions) 
endif() 
... ... 
... ... 

target_include_directories(教程 PUBLIC "${PROJECT_BINARY_DIR}")

(第4步)安装与测试

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

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

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

代码语言:javascript
复制
安装(TARGETS MathFunctions DESTINATION lib)
安装(文件 MathFunctions.h DESTINATION 包括)

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

代码语言:javascript
复制
安装(目标教程目标 bin)
安装(文件“$ {PROJECT_BINARY_DIR} /TutorialConfig.h”目标包括)

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

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

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

测试支持

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

代码语言:javascript
复制
enable_testing() 

# 应用程序是否运行
add_test(NAME Runs COMMAND Tutorial 25) 

# 是否为 25 的 sqrt 
add_test (NAME Comp25 COMMAND Tutorial 25) 
set_tests_properties (Comp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5") 

# 使用消息是否有效?
add_test(NAME 使用命令教程) 
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number") 

# 定义一个函数来简化添加测试
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_test(教程 4 "4 是 2") 
do_test(教程 9 "9 是 3") 
do_test(教程 5 "5 是 2.236") 
do_test(教程 7 "7 是 2.645") 
do_test(教程 25 "25 是 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教程25 
)### 
set_tests_properties (tutorialcomp25属性*数字”) ### ####定义一个宏来简化添加测试,然后使用它




###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 是 2") 
###do_test(9 "9 是 3") 
###do_test(5 "5 是 2.236") 
###do_test(7 "7 是 2.645") 
# ##do_test(25 "25 是 5") 
###do_test(-25 "-25 是 [-nan|nan|0]") 
###do_test(0.0001 "0.0001 是 0.01")

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

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

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

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

(第5步)添加系统自检

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

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

代码语言:javascript
复制
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 (日志 HAVE_LOG) 
###check_function_exists (exp HAVE_EXP)

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

代码语言:javascript
复制
// 平台是否提供exp和log函数?
#cmakedefine HAVE_LOG 
#cmakedefine HAVE_EXP

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

代码语言:javascript
复制
#if 已定义(HAVE_LOG) && 已定义(HAVE_EXP) 
  double 结果 = exp(log(x) * 0.5); std::cout << "            使用 log 和 exp 将
  " << x << " 的 sqrt 计算为 " << result << " " << std::endl; #else  双结果 = x;

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

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

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

代码语言:javascript
复制
target_include_directories(MathFunctions 接口 ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_BINARY_DIR})

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

哪个函数现在可以提供更好的结果,sqrt或mysqrt?

指定编译定义

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

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

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

代码语言:javascript
复制
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步)添加自定义命令和生成的文件

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

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

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

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

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

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

代码语言:javascript
复制
add_executable(MakeTable MakeTable.cxx)

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

代码语言:javascript
复制
add_custom_command(
  输出 ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  命令 MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  取决于 MakeTable 
  )

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

代码语言:javascript
复制
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)

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

代码语言:javascript
复制
target_include_directories(MathFunctions
          接口 ${CMAKE_CURRENT_SOURCE_DIR} 
          PRIVATE ${CMAKE_CURRENT_BINARY_DIR} 
          )

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

代码语言:javascript
复制
双 mysqrt(双 x) 
{ 
  if (x <= 0) { 
    return 0; 
  } 

  // 使用表格来帮助找到初始值
  double result = x; 
  if (x >= 1 && x < 10) { 
    std::cout << "使用表格帮助找到初始值 " << std::endl;
    结果 = sqrtTable[static_cast<int>(x)]; 
  } 

  // 进行十次迭代
  for (int i = 0; i < 10; ++i) { 
    if (result <= 0) { 
      result = 0.1; } 
    } 
    double delta = x - (结果 * 结果);
    结果 = 结果 + 0.5 * 增量 / 结果;
    std::cout << "计算 " << x << " 的 sqrt 为 " << result << std::endl;
  返回

  结果;
}

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

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

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

代码语言:javascript
复制
包括(InstallRequiredSystemLibraries)
设置(CPACK_RESOURCE_FILE_LICENSE“${CMAKE_CURRENT_SOURCE_DIR}/License.txt”)
设置(CPACK_PACKAGE_VERSION_MAJOR“${Tutorial_VERSION_MAJOR}”)
设置(CPACK_PACKAGE_VERSION_MINOR“${Tutorial_VERSION_MINOR}”)
包括(CPack)

这就是全部需要做的事。我们首先包含InstallRequiredSystemLibraries。该模块将包括项目当前平台所需的任何运行时库。接下来,我们将一些CPack变量设置为存储该项目的许可证和版本信息的位置。版本信息是在本教程的前面设置的,并且license.txt已包含在此步骤的顶级源目录中。

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

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

代码语言:javascript
复制
软件包

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

代码语言:javascript
复制
cpack -G ZIP -C 调试

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

代码语言:javascript
复制
cpack --config CPackSourceConfig.cmake

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

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

(第8步)添加Dashboard支持

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

代码语言:javascript
复制
# 启用仪表板脚本
包括(CTest)

替换

代码语言:javascript
复制
# 启用测试启用_测试()

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

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

代码语言:javascript
复制
设置(CTEST_PROJECT_NAME“CMakeTutorial”)
设置(CTEST_NIGHTLY_START_TIME“00:00:00 EST”)

设置(CTEST_DROP_METHOD“http”)
设置(CTEST_DROP_SITE“my.cdash.org”)
设置(CTEST_DROP_LOCATION“/submit.php?project = CMakeTutorial” )
设置(CTEST_DROP_SITE_CDASH TRUE)

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

代码语言:javascript
复制
ctest [-VV] -D 实验

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

代码语言:javascript
复制
ctest [-VV] -C 调试 -D 实验

或者,从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的开始部分更新为:

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.10) 

# 设置项目名称和版本
project(Tutorial VERSION 1.0) 

# 指定 C++ 标准
集(CMAKE_CXX_STANDARD 11) 
set(CMAKE_CXX_STANDARD_REQUIRED True) 

# 控制静态库和共享库的构建位置,以便在 Windows 上
我们不需要不需要修改运行可执行文件的路径
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“使用共享库构建" ON) 

# 配置头文件仅传递版本号
configure_file(TutorialConfig.h.在TutorialConfig.h中)

# 添加 MathFunctions 库
add_subdirectory(MathFunctions) 

# 添加可执行
文件 add_executable(Tutorialtutorial.cxx) 
target_link_libraries(Tutorial PUBLIC MathFunctions)

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

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

代码语言:javascript
复制
# 添加运行add_library(MathFunctions MathFunctions.cxx)的库

# 声明任何链接到我们的人都需要包含当前源目录
# 才能找到 MathFunctions.h,而我们不需要。
target_include_directories(MathFunctions 
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} 
                           ) 

# 我们应该使用自己的数学函数
选项(USE_MYMATH "使用教程提供的数学实现" ON) 
if(USE_MYMATH) 

  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH") 

  # 首先我们添加生成table 
  add_executable(MakeTable MakeTable.cxx) 

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

  # 只执行 sqrt 
  add_library(SqrtLibrary STATIC 
              mysqrt.cxx 
              ${CMAKE_CURRENT_BINARY_DIR}/Table.h 
              ) 

  # 声明我们依赖的库我们的二进制目录来查找 Table.h 
  target_include_directories(SqrtLibrary PRIVATE 
                             ${CMAKE_CURRENT_BINARY_DIR} 
                             ) 

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary) 
endif() # 定义符号,表明我们在Windows 上构建

时使用 declspec(dllexport)

target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH") 

# 安装规则
install(TARGETS MathFunctions DESTINATION lib) 
install(FILES MathFunctions.h DESTINATION include)

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

代码语言:javascript
复制
#include <iostream> 

#include "MathFunctions.h" 

// 包含生成的表
#include "Table.h"

命名空间 mathfunctions 
{
命名空间细节
{ 
// 使用简单操作进行平方根计算
double mysqrt(double x) 
{ 
  if ( x <= 0)
    返回 0; 

  // 使用表格帮助找到初始值
  double result = x; 
  if (x >= 1 && x < 10) 
  { 
    std::cout << "使用表格帮助找到初始值 " << std::endl;
    结果 = sqrtTable[static_cast<int>(x)]; 
  } 

  // 进行十次迭代
  for (int i = 0; i < 10;++i) 
  { 
    if (结果 <= 0)
      结果 = 0.1;
    双增量 = x - (结果 * 结果);
    结果 = 结果 + 0.5 * 增量 / 结果;
    std::cout << "计算 " << x << " 的 sqrt 为 " << result << std::endl;
  返回

  结果;
} } 
}

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

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

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

代码语言:javascript
复制
#if Defined(_WIN32) 
# if Defined(EXPORTING_MYMATH) 
# 定义 DECLSPEC __declspec(dllexport) 
# else 
# 定义 DECLSPEC __declspec(dllimport) 
# endif 
#else // 非 Windows 
# 定义 DECLSPEC 
#endif

命名空间 mathfunctions { 
double DECLSPEC sqrt(double x );
}

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

代码语言:javascript
复制
# 声明当默认为共享库时 SqrtLibrary 需要 PIC 
  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。

所以,下面的代码:

代码语言:javascript
复制
# 指定 C++ 标准
集(CMAKE_CXX_STANDARD 11) 
set(CMAKE_CXX_STANDARD_REQUIRED True)

将被替换为:

代码语言:javascript
复制
add_library(tutorial_compiler_flags 接口)
target_compile_features(tutorial_compiler_flags 接口 cxx_std_11)

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

代码语言:javascript
复制
设置(gcc_like_cxx“$ <COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU>”)
设置(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_flagstarget_link_libraries()调用。

(第11步)增加输出配置

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

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

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

代码语言:javascript
复制
安装(目标 MathFunctionstutorial_compiler_flags 
        DESTINATION lib 
        EXPORT MathFunctionsTargets)
安装(文件 MathFunctions.h 目标包括)

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

代码语言:javascript
复制
安装(导出 MathFunctionsTargets
  文件 MathFunctionsTargets.cmake
  目标 lib/cmake/MathFunctions 
)

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

代码语言:javascript
复制
目标“MathFunctions”INTERFACE_INCLUDE_DIRECTORIES 属性包含
路径:

  “/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions”,

其前缀在源目录中。

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

代码语言:javascript
复制
target_include_directories(MathFunctions 
                           INTERFACE 
                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> 
                            $<INSTALL_INTERFACE:include> 
                           )

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

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

代码语言:javascript
复制
@PACKAGE_INIT@ 
include (“${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake”)

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

代码语言:javascript
复制
install(EXPORT MathFunctionsTargets 
  FILE MathFunctionsTargets.cmake 
  DESTINATION lib/cmake/MathFunctions 
) 

include(CMakePackageConfigHelpers) 
# 生成包含导出的配置文件 configure_package_config_file 
(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in 
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunction sConfig.cmake " 
  INSTALL_DESTINATION "lib/cmake/example" 
  NO_SET_AND_CHECK_MACRO 
  NO_CHECK_REQUIRED_COMPONENTS_MACRO 
  ) 
# 生成配置文件的版本文件
write_basic_package_version_file( 
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake" 
  VERSION "${Tutorial_VERSION_MA JOR}.${Tutorial_VERSION_MINOR}"
  兼容性 AnyNewerVersion 
) 

# 安装配置文件
install(FILES 
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake 
  DESTINATION lib/cmake/MathFunctions 
  )

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

代码语言:javascript
复制
导出(导出 MathFunctionsTargets
  文件“${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下创建一个debug和release目录。最后,您应该具有如下布局:

代码语言:javascript
复制
─ multi_config 
    ├── 调试
    └── 发布

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

代码语言:javascript
复制
cmake -DCMAKE_BUILD_TYPE=调试../../MultiPackage/ 
cmake --build 。
cd ../release 
cmake -DCMAKE_BUILD_TYPE=Release ../../MultiPackage/ 
cmake --build 。
光盘..

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

代码语言:javascript
复制
cpack --config ../../MultiPackage/MultiCPackConfig.cmake

二、Cmake入门案例

2.1单个源文件

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

代码语言:javascript
复制
#include <stdio.h> 
#include <stdlib.h> 

/** 
* power - 计算数字的幂。
* @param base:基值。
* @param exponent:指数值。
* 
* @return 基数的幂指数。
*/
双幂(双基数, int 指数) 
{ 
    int 结果 = 基数;
    整数我;
    
    for(i = 1; i < 指数; ++i){
        结果 = 结果 * 基数;
    返回

    结果;
} 

int main(int argc, char *argv[]) 
{ 
    if (argc < 3){ 
        printf("用法:%s 基数指数 \n", argv[0]);
        返回1;
    }
    双基 = atof(argv[1]);
    int 指数 = atoi(argv[2]);
    双结果 = 幂(底数,指数);
    printf("%g ^ %d 是 %g\n", 底数, 指数, 结果);
    返回0;
}

编写 CMakeLists.txt  

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

代码语言:javascript
复制
# 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 可执行文件。

代码语言:javascript
复制
[ehome@xman 演示1]$ cmake . 
-- C 编译器标识为 GNU 4.8.2 
-- CXX 编译器标识为 GNU 4.8.2 
-- 检查是否工作 C 编译器:/usr/sbin/cc 
-- 检查是否工作 C 编译器:/usr/sbin/cc -- 有效
-- 检测 C 编译器 ABI 信息
-- 检测 C 编译器 ABI 信息 - 完成
-- 检查是否工作 CXX 编译器:/usr/sbin/c++ 
-- 检查是否工作 CXX 编译器:/usr/sbin/c++ -- 有效
-- 检测 CXX 编译器 ABI 信息
-- 检测 CXX 编译器 ABI 信息 - 完成
-- 配置完成
-- 生成完成
-- 构建文件已写入:/home/ehome/Documents/programming/C/power/Demo1 
[ehome@ xman Demo1]$ make
扫描目标Demo的依赖
[100%] 构建 C 对象 CMakeFiles/Demo.dir/main.cc.o
链接 C 可执行文件演示
[100%] 构建目标演示
[ehome@xman Demo1]$ ./Demo 5 4 
5 ^ 4 为 625 
[ehome@xman演示1]$ ./演示 7 3 
7 ^ 3 为 343 
[ehome@xman 演示1]$ ./演示 2 10 
2 ^ 10 为 1024

2.2多个源文件

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

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

代码语言:javascript
复制
/演示2 
    | 
    +--- main.cc 
    | 
    +--- MathFunctions.cc 
    | 
    +--- MathFunctions.h

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

代码语言:javascript
复制
# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo2)

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

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

代码语言:javascript
复制
aux_source_directory(<目录> <变量>)

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

代码语言:javascript
复制
# 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 和 MathFunctions.cc 文件移动到 math 目录下。

代码语言:javascript
复制
./演示3 
    | 
    +--- main.cc 
    | 
    +--- 数学/ 
          | 
          +--- MathFunctions.cc 
          | 
          +--- MathFunctions.h

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

根目录中的 CMakeLists.txt :

代码语言:javascript
复制
# 项目信息
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:

代码语言:javascript
复制
# 查找当前目录下的所有源文件
# 并将名称保存到 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 文件中添加该选项:

代码语言:javascript
复制
# 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(演示 ${DIR_SRCS}) 
target_link_libraries (演示 ${EXTRA_LIBS})

其中:

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

修改 main.cc 文件

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

代码语言:javascript
复制
#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("用法:%s 基数指数 \n", argv[0]);
        返回1;
    }
    双基 = atof(argv[1]); 
    int 指数 = atoi(argv[2]); 
    
#ifdef USE_MYMATH 
    printf("现在我们使用我们自己的数学库。\n");
    双结果 = 幂(底数,指数);
#else 
    printf("现在我们使用标准库。\n");
    双结果 = pow(底数, 指数);
#万一
    printf("%g ^ %d 是 %g\n", 底数, 指数, 结果);
    返回0;
}

编写 config.h.in 文件

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

代码语言:javascript
复制
#cmake定义 USE_MYMATH

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

编译项目

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

添加图片注释,不超过 140 字(可选)

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

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

USE_MYMATH 为 ON

运行结果:

代码语言:javascript
复制
[ehome@xman Demo4]$ ./Demo
现在我们使用我们自己的 MathFunctions 库。
7 ^ 3 = 343.000000 
10 ^ 5 = 100000.000000 
2 ^ 10 = 1024.000000

此时 config.h 的内容为:

代码语言:javascript
复制
#定义USE_MYMATH

USE_MYMATH 为 OFF

运行结果:

代码语言:javascript
复制
[ehome@xman Demo4]$ ./Demo
现在我们使用标准库。
7 ^ 3 = 343.000000 
10 ^ 5 = 100000.000000 
2 ^ 10 = 1024.000000

此时 config.h 的内容为:

代码语言:javascript
复制
/* #undef USE_MYMATH */

2.5安装和测试

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

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

定制安装规则

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

代码语言:javascript
复制
# 指定 MathFunctions 库的安装路径
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

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

代码语言:javascript
复制
# 指定安装路径
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 变量的值来指定这些文件应该拷贝到哪个根目录。

代码语言:javascript
复制
[ehome@xman Demo5]$ sudo make install 
[ 50%] 构建目标 MathFunctions 
[100%] 构建目标 Demo
安装项目... 
-- 安装配置: "" 
-- 安装: /usr/local/bin/Demo 
- - 安装:/usr/local/include/config.h 
-- 安装:/usr/local/bin/libMathFunctions.a 
-- 最新:/usr/local/include/MathFunctions.h 
[ehome@xman Demo5 ]$ ls /usr/local/bin
演示 libMathFunctions.a 
[ehome@xman Demo5]$ ls /usr/local/include 
config.h MathFunctions.h

为工程添加测试

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

代码语言:javascript
复制
# 启用测试
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 用来测试输出是否包含后面跟着的字符串。

让我们看看测试的结果:

代码语言:javascript
复制
[ehome@xman Demo5]$ make test
运行测试...
测试项目 /home/ehome/Documents/programming/C/power/Demo5 
    Start 1: test_run 
1/4 Test #1: test_run ........ ......................通过了 0.00 秒
    开始 2:test_5_2 
2/4 测试 #2:test_5_2 ........................ .......通过 0.00 秒
    开始 3:test_10_5 
3/4 测试#3:test_10_5 ................................通过 0.00 秒
    开始4: test_2_10 
4/4 测试#4: test_2_10 ................................通过 0.00 秒100% 测试通过,总共

4 次中有 0 次测试失败

测试时间(实际)= 0.01 秒

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

代码语言:javascript
复制
# 定义一个宏,用来简化测试工作
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 选项:

代码语言:javascript
复制
设置(CMAKE_BUILD_TYPE“调试”)
设置(CMAKE_CXX_FLAGS_DEBUG“$ENV{CXXFLAGS} -O0 -Wall -g -ggdb”)
设置(CMAKE_CXX_FLAGS_RELEASE“$ENV{CXXFLAGS} -O3 -Wall”)

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

2.6添加环境检查

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

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

添加 CheckFunctionExists 宏

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

代码语言:javascript
复制
# 检查系统是否支持 pow 函数
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (pow HAVE_POW)

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

预定义相关宏变量

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

代码语言:javascript
复制
// 平台是否提供pow功能?
#cmake定义HAVE_POW

在代码中使用宏和函数

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

代码语言:javascript
复制
#ifdef HAVE_POW 
    printf("现在我们使用标准库。\n");
    双结果 = pow(底数, 指数); 
#else 
    printf("现在我们使用我们自己的数学库。\n");
    双结果 = 幂(底数,指数);
#万一

2.8添加版本号

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

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

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

代码语言:javascript
复制
设置(Demo_VERSION_MAJOR 1)
设置(Demo_VERSION_MINOR 0)

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

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

代码语言:javascript
复制
// 教程的配置选项和设置
#define Demo_VERSION_MAJOR @Demo_VERSION_MAJOR@ 
#define Demo_VERSION_MINOR @Demo_VERSION_MINOR@

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

代码语言:javascript
复制
#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){ 
        // 打印版本信息
        printf("%s Version %d.%d\n", 
            argv[0], 
            Demo_VERSION_MAJOR, 
            Demo_VERSION_MINOR); 
        printf("用法: %s 基数指数 \n", argv[0]);
        返回1;
    }
    双基 = atof(argv[1]); 
    int 指数 = atoi(argv[2]); 
    
#if Defined (HAVE_POW) 
    printf("现在我们使用标准库。\n");
    双结果 = pow(基数,指数); 
#别的
    printf("现在我们使用我们自己的数学库。\n");
    双结果 = 幂(底数,指数);
#endif 
    
    printf("%g ^ %d 是 %g\n", 基数, 指数, 结果);
    返回0;
}

2.9生成安装包

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

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

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

代码语言:javascript
复制
# 构建 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}")
包括 (CPack)

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

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

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

生成二进制安装包:

代码语言:javascript
复制
cpack -C CPackConfig.cmake

生成源码安装包

代码语言:javascript
复制
cpack -C CPackSourceConfig.cmake

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

代码语言:javascript
复制
[ehome@xman Demo8]$ cpack -C CPackSourceConfig.cmake 
CPack:使用 STGZ 创建包
CPack:安装项目
CPack: - 运行预安装目标:Demo8 
CPack: - 安装项目:Demo8 
CPack:创建包
CPack: - 包:/home生成 /ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.sh。
CPack:使用 TGZ 创建包
CPack:安装项目
CPack: - 运行预安装目标:Demo8 
CPack: - 安装项目:Demo8 
CPack:创建包
CPack: - 包:/home/ehome/Documents/programming/C/power/Demo8/生成了 Demo8-1.0.1-Linux.tar.gz。
CPack:使用 TZ 创建包
CPack:安装项目
CPack:- 运行预安装目标:Demo8
CPack: - 安装项目:Demo8 
CPack:创建包
CPack: - 生成包:/home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.tar.Z。

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

代码语言:javascript
复制
[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 自动生成的交互式安装界面:

代码语言:javascript
复制
[ehome@xman Demo8]$ sh Demo8-1.0.1-Linux.sh 
Demo8 安装程序版本:1.0.1,版权所有 (c) Humanity
这是一个自解压存档。
存档将被解压到: /home/ehome/Documents/programming/C/power/Demo8

如果您想停止解压,请按 <ctrl-C>。
麻省理工学院许可证 (MIT)

版权所有 (c) 2013 Joseph Pan(http://hahack.com)

特此免费向获得
本软件和相关文档文件(“软件”)副本的任何人授予许可,不受限制地处理
本软件,包括但不限于
使用、复制、修改、合并、发布、分发、再许可和/或出售软件副本的权利
软件,并允许软件提供者这样做,但须
满足以下条件:

上述版权声明和本许可声明应包含在
软件的所有副本或主要部分中。

本软件按“原样”提供,不提供任何明示或
暗示的保证,包括但不限于适销性、
特定用途的适用性和不侵权的保证。在任何情况下,作者或版权持有者均不对因本软件或本软件中的使用或其他交易而产生或与之相关的任何索赔、损害或
其他责任负责,无论是合同、侵权行为还是其他行为。软件。

你接受许可吗? [yN]: 
y
默认情况下 Demo8 将安装在:
  “/home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux”
您是否要包含子目录 Demo8-1.0.1 -Linux?
说“否”将安装在:“/home/ehome/Documents/programming/C/power/Demo8”[Yn]:
y

使用目标目录:/home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0。 1-Linux
正在解压,请稍候...

解压成功完成

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

代码语言:javascript
复制
[ehome@xman Demo8]$ ./Demo8-1.0.1-Linux/bin/Demo 5 2
现在我们使用我们自己的数学库。
5^2 是 25

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

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

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

自动工具

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

qmake

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

视觉工作室

  • 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 平台。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是 CMake
    • 1.1CMake教程
      • 1.2CMake指南教程(官方地址)
      • 二、Cmake入门案例
        • 2.1单个源文件
          • 2.2多个源文件
            • 2.3多个目录,多个源文件
              • 2.4自定义编译选项
                • 2.5安装和测试
                  • 2.6添加环境检查
                    • 2.8添加版本号
                      • 2.9生成安装包
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档