CMake 教程


title: CMake 教程
date: 2021-06-28 19:11:35
tags: [C++, CMake]


CMake 教程

本文翻译自CMake 教程

练习素材下载地址或官方github

概述

CMake Tutorial提供了一个分步的指南(一步步的完善一个CMake工程),涵盖了常见的CMake能够解决的编译系统的问题。在一个示例工程中查看各个主题如何协同工作对学习CMake的使用非常有帮助。指南中使用的源码在CMake源码树的Help/guide/tutorial目录下,每个步骤都有自己的子目录包含相应的源码。指南的各个步骤是渐进式的,即后一步骤的内容在前一步骤的基础上进行。

Step1 起航

对于简单的项目,只需要一个三行CMakeLists.txt文件,在Step1目录中创建一个CMakeLists.txt文件,其内容如下所示:

# 指定 CMake 的最低版本
cmake_minimum_required(VERSION 3.10)

# 设置项目名
project(Tutorial)

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

源文件tutorial.cxx也位于Step1目录下,其中的程序可以计算一个数的平方根。值得一提的是,这里我们使用了小写字母来书写CMake语句,实际上,CMake 支持大写、小写和大小写混合命令

添加一个版本号和配置头文件

我们将添加的第一个功能是为我们的项目和可执行文件提供一个版本号,尽管我们可以在源码里实现,但使用CMakeLists.txt来提供版本号更具灵活性。首先,修改CMakeLists.txt,使用project命令来设置项目的名称和版本号:

cmake_minimum_required(VERSION 3.10)

# 设置项目名以及版本号
project(Tutorial VERSION 1.0)

然后,配置一个头文件用于向源码传递版本号:

configure_file(TutorialConfig.h.in TutorialConfig.h)

头文件TutorialConfig.h将由CMake自动创建,且创建在PROJECT_BINARY_DIR目录下,原文中将该目录称为binary tree。这个目录实际上指的是编译目录,也就是编译产生的文件存放的目录,这个目录可以等同于源码目录(PROJECT_SOURCE_DIR), 也可以是另外的目录,比如专门建立一个 build 目录以保持源码目录的整洁。需要注意的是,项目的配置和编译都需要在编译目录下进行。

通常,我们在源码中需要包含该头文件,这样才能使用版本号(本质是宏定义),因此我们需要将该头文件所在的目录添加为项目的包含目录。具体的,在CMakeLists.txt的末尾添加:

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

至于为什么强调在末尾添加,原文没有解释,这里我简单解释一下:

target_include_directories可以为target指定一个或多个包含目录,这里的target通常就是我们的project,使用project(name)可以将我们的项目命名为name。但仅有项目名称时,还不可以使用target_include_directories(name …)来指定包含目录, 需要先使用add_executable或add_library来创建target,之后才可以为其指定包含目录。因此把target_include_directories放到末尾比较安全。否则放到前面的话,target还未被创建,CMake会报错。

我们需要自行创建文件TutorialConfig.h.in,内容如下:

// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

该文件是生成TutorialConfig.h的原料,其中@Tutorial_VERSION_MAJOR@@Tutorial_VERSION_MINOR@会被替换为之前在project命令中为项目指定的版本号。需要注意的是@xxx_VERSION_MAJOR/MINOR@中的xxx必须是project为项目指定的名称。

至此,版本号就添加完了。我们可以在源码中包含TutorialConfig.h,然后使用宏Tutorial_VERSION_MAJORTutorial_VERSION_MINOR就可以使用主版本号和次版本号了,比如在源码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;
  }

指定C++标准

接下来,让我们为项目添加一些C++11的特性,将tutorial.cxx中的atof替换为std::stod,同时移除#include <cstdlib>

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

我们还需要在CMakeLists.txt中显式的声明按照C++11标准进行编译。在CMake中使能对某个特定的C++标准的支持时,可以使用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来配置项目,然后就可以使用你所选择的编译工具(比如GCC、VC等)来编译你的项目。举例来说,我们可以通过命令行进入到CMake源码树的Help/guide/tutorial目录下,然后运行下面的命令:

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

然后可以运行如下命令来测试编译得到的可执行程序:

Tutorial 4294967296
Tutorial 10
Tutorial

Step2 添加一个库

在本步骤,我们将为我们的项目(Tutorial)添加一个库,这个库将包含我们自己实现的求平方根的程序,这样一来,可执行程序就可以使用我们自己的实现替代编译器提供的标准库的求平方根函数。

我们将把这个库的实现放在一个名为MathFunctions目录下面(这个目录本身位于step2目录下),库的具体内容为一个头文件,MathFunctions.h,和一个源文件mysqrt.cxx。源文件内有一个函数,名为mysqrt,用于计算平方根。此外,还需要在MathFunctions目录下创建一个仅有一行的CMakeLists.txt

add_library(MathFunctions mysqrt.cxx)

为了使用这个库,我们需要在顶层(也就是 Step2 目录下的)的CMakeLists.txt中做如下事情:

  1. 调用add_subdirectory来告知 CMake 这个新增的库的位置,如此该库才能得到编译。
  2. 调用target_link_libraries来告知链接器,需要链接名为MathFunctions的库。
  3. 调用target_include_directories来指定MathFunctions为包含目录,这样mqsqrt.h才能被找到。

至此,顶层的CMakeLists.txt应该书写如下:

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC 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"
                          )

我们也可以使用CMake的命令来实现将MathFunctions库设置为可选的,即使用 CMake 来配置项目时,通过设置某些配置项实现是否使用该库,如果不使用该库则还是利用标准库的函数来求平方根。这种提供配置项的做法在大型项目中是非常常见的。具体怎么做呢?首先,在顶层的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 来配置项目,那么不难发现,选项USE_MYMATH将会出现在 GUI 界面中,其默认值为ON,也就是会使用该库。当然,我们可以修改这个配置项来禁用该库。我们最终的配置会被缓存起来,从而不需要每次在编译目录下运行 CMake 都重新配置。

接下来还需要将MathFunctions库的编译和链接设置为可选的:

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)

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

其中,EXTRA_LIBS变量用于收集所有被选择的库,进而通过target_link_libraries来链接这些库。EXTRA_INCLUDES变量的作用也是相似的,该变量负责收集所有被选择的包含目录。这种实现程序可配置的方式非常经典,我们会在后续的步骤中涵盖更为现代的做法。

实现程序的可配置,除了CMakeLists.txt需要改变,源码也需要相应的改变,在tutorial.cxx中,做如下修改:

#ifdef USE_MYMATH
#  include "MathFunctions.h"
#endif
......
#ifdef USE_MYMATH
  const double outputValue = mysqrt(inputValue);
#else
  const double outputValue = sqrt(inputValue);
#endif

不难看出,源码使用了配置宏来实现可配置,这里的配置宏为USE_MYMATH,和相应的选项同名,不过这个宏不是直接通过配置项生成的,那么在哪里定义这个宏呢?在揭晓答案之前,先解释一下CMake的命令configure_file的作用,这个命令的基础用法如下:

configure_file(<input> <output>)

可以实现将input中的内容按照一定的规则进行替换,并将替换的结果输出到output。具体的替换规则主要有:

  1. 形式为${VAR}@VAR@的文本将被替换为CMake中变量名为VAR的变量的值,如果这个变量未被定义,那么替换为空。
  2. 形式为#cmakedefine VAR的文本将会被替换为#define VAR或者/* #undef VAR */,具体是前者还是后者,则取决于CMake中选项VAR是否被设置为ON
  3. 形式为#cmakedefine01 VAR的文本将会被替换为#define VAR 1#define VAR 0,类似于 2。

到这里,不用说也能猜到,配置宏USE_MYMATH不会直接定义,而是在TutorialConfig.h.in中添加如下文本:

#cmakedefine USE_MYMATH

注意
在顶层CMakeLists.txt中,我们必须将option语句放到configure_file语句前面:

option(USE_MYMATH "Use tutorial provided math implementation" ON)
......
configure_file(TutorialConfig.h.in TutorialConfig.h)

只有先设置好选项USE_MYMATH的值,后面处理#cmakedefine USE_MYMATH时,才能够按照我们的意图生成相应的配置宏。

step3 添加库的使用要求

使用要求可以更好地控制库或可执行文件的链接以及头文件包含,也可以更好的控制CMake中target的transitive property。相关的几个基础命令如下:

  • target_compile_definitions()
  • target_compile_options()
  • target_include_directories()
  • target_link_libraries()

我们在step2的基础上修改代码,用CMake的命令来实现库的使用要求。我们首先声明任何需要链接MathFunctions库的项目都需要将当前源码目录设置为包含目录,而该库本身不需要。这是一个INTERFACE使用要求INTERFACE意味着消费者需要而生产者不需要。具体做法是在MathFunctions/CMakeLists.txt的末尾添加一句:

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          )

现在,我们已经为MathFunctions库指定了使用要求,因此可以移除顶层CMakeLists.txtEXTRA_INCLUDES变量相关的语句,具体如下:

if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
endif()
......
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

之后,我们就可以使用CMake的GUI或命令配置、编译我们的项目了,可以通过配置项选择是否使用MathFunctions库。

step4 安装与测试

现在我们可以开始为我们的项目添加安装规则测试支持

安装规则

安装规则非常简单:对于MathFunctions库,我们想要安装库文件头文件;对于应用程序,我们想要安装可执行文件配置头文件。具体的,在MathFunctions/CMakeLists.txt的末尾添加:

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

在顶层CMakeLists.txt的末尾添加:

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

完成上述修改后,即可配置项目并编译之。编译完成后,可以使用cmake命令的install选项进行安装(3.15版本时引入,之前的版本需要运行make install命令),执行完该命令,则相应的头文件、库文件、可执行文件就会得到安装。具体的,创建并进入编译目录

mkdir step5_build
cd step5_build

配置:

cmake ../Step5

编译:

cmake --build .

最后安装,不妨将安装目录设置为step5_build/install

# --config Debug指定安装的是Debug版的,当前的配置就是Debug,没有Release版的编译结果
# 而默认是安装Release版的,会出现文件不存在的问题,因此指定安装Debug版的
# --install后面紧跟的目录./指明了cmake_install.cmake文件所在的目录
cmake --install ./ --prefix ./install --config Debug

安装完成后,安装目录下的所有文件的目录结构如下:

├─install
  ├─bin
  │      Tutorial.exe
  │
  ├─include
  │      MathFunctions.h
  │      TutorialConfig.h
  │
  └─lib
          MathFunctions.lib

CMake变量CMAKE_INSTALL_PREFIX用于指定安装目录,如果未定义该变量,则step5_build/cmake_install.cmake(安装时会读取该文件)会指定默认的安装目录:

# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
  set(CMAKE_INSTALL_PREFIX "C:/Program Files (x86)/Tutorial")
endif()

使用cmake --install命令安装时,可以通过选项--prefix来指定安装目录。同时,可以使用--config指定安装时的一些配置。

测试支持

接下来,让我们测试编译得到的可执行文件,在顶层CMakeLists.txt的末尾使能测试并添加一些基本测试用例以验证程序能否正确运行:

enable_testing()

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

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

第一个测试只是验证程序能够运行,没有段错误或其他崩溃,并且返回值为零。这是CTest测试的基本形式。接下来的测试使用了PASS_REGULAR_EXPRESSION测试属性来验证该测试的输出是否含有特定的字符串。如此,就可以验证当参数的数量不对时是否会打印出用法提示信息。

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

完成上述修改后,重新编译并切换到二进制目录(也就是可执行程序所在的目录)下,然后运行:

ctest -N
ctest -VV

对于多配置生成器(如Visual Studio),必须指定配置类型。例如,要在Debug模式下运行测试,须在编译目录下(而不是Debug子目录,可执行程序在该子目录下)运行:

ctest -C Debug -VV

如果你使用的是IDE,则可以不用通过命令行运行测试,直接在IDE中构建RUN_TESTS目标即可。

step5 添加系统自省

在这一步骤中,让我们向项目中添加一些代码,这些代码依赖于目标平台是否具备相应的功能(如果不具备则这些代码无法运行)。这里,我们将添加一些代码,这些代码依赖于目标平台是否拥有logexp函数。当然,几乎所有的平台都有这些函数,只是拿它们举例子而已。

如果平台具有logexp函数,那么我们将在mysqrt函数中使用这两个函数计算平方根,我们首先在顶层CMakeLists.txt中使用CheckSymbolExists模块来测试上述函数是否可得,之后在TutorialConfig.h.in中添加新的配置宏。不要忘记,在configure_file前面执行check_symbol_exists,这个在之前已经强调过了。

具体的,在顶层CMakeLists.txt中添加:

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

在文件TutorialConfig.h.in中添加:

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

如此,CMake会根据变量HAVE_LOGHAVE_EXP的值在头文件TutorialConfig.h中生成相应的同名配置宏,生成的配置宏就可以供我们在源码中使用。如果系统提供了函数logexp,那么我们将在mysqrt函数中使用它们计算平方根。源码中需要修改MathFunctions/mysqrt.cxx文件的mysqrt函数:

#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;
  ......
#endif

此外,还需要在mysqrt.cxx中包含相应的头文件:

#include <cmath>

完成上述修改后,配置并编译我们的项目,你将注意到logexp函数没有被使用,尽管我们认为这两个函数是可以获得的。为什么会这样呢?仔细检查一下就会发现,我们忘记了在mysqrt.cxx中包含TutorialConfig.h,而相应的配置宏正是生成在该头文件里。当然,并不是简单包含一下上述头文件就可以的,我们还需要将头文件的位置告知编译器,否则编译器找不到这个头文件,具体做法是在MathFunctions/CMakeLists.txt中做如下修改:

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

做完上述修改,我们再次编译项目,如果系统提供了函数logexp,那么TutorialConfig.h中或出现相应的配置宏,mysqrt函数再被调用时也会执行这两个函数。

指定Compile Definition

通过上文可以看出,要在MathFunctions库里面包含配置宏还是比较麻烦的,那么有没有更好的办法得到配置宏呢?当然有,让我们使用target_compile_definitions来完成这一工作。首先,移除TutorialConfig.h.in文件中的相关定义,我们不再需要在mysqrt.cxx中包含头文件TutorialConfig.h,因此MathFunctions/CMakeLists.txt中的相应修改也要移除。

接下来,将顶层CMakeLists.txt中的对HAVE_LOGHAVE_EXP的校验移至MathFunctions/CMakeLists.txt中,并将相应的配置宏指定为PRIVATEcompile definitions

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

做完上述修改后,再次编译并运行,你会发现上述修改和修改之前的效果是一样的。

译者补
这种做法显然更好,因为相关的配置宏仅仅是库所需要的,应该由库的CMakeLists.txt负责执行相应的检查并定义相应的配置宏,而不应该把这些细节暴露给顶层的CMakeLists.txt。

step6 添加自定义命令与生成文件

假如我们不再使用平台提供的logexp函数,而是希望生成一个存放有预先计算好数值的表,然后在mysqrt函数中通过查表得方式快速的得到部分输入的平方根。在本节中,我们将在项目的编译过程中创建这个表,然后将这个表编译进我们的应用程序中。

首先,让我们删除MathFunctions/CMakeLists.txt中对logexp函数的检查。然后从mysqrt.cxx中删除对HAVE_LOGHAVE_EXP的检查。同时,我们可以删除头文件包含#include <cmath>。(不需要使用logexp函数了,所以相关内容都可以移除)

MathFunctions子目录中,提供了一个名为MakeTable.cxx的新的源文件来帮助生成上述的预计算值表。这个源文件的内容如下:

// A simple program that builds a sqrt table
#include <cmath>
#include <fstream>
#include <iostream>

int main(int argc, char* argv[])
{
  // make sure we have enough arguments
  if (argc < 2) {
    return 1;
  }

  std::ofstream fout(argv[1], std::ios_base::out);
  const bool fileOpen = fout.is_open();
  if (fileOpen) {
    fout << "double sqrtTable[] = {" << std::endl;
    for (int i = 0; i < 10; ++i) {
      fout << sqrt(static_cast<double>(i)) << "," << std::endl;
    }
    // close the table with a zero
    fout << "0};" << std::endl;
    fout.close();
  }
  return fileOpen ? 0 : 1; // return 0 if wrote the file
}

很明显,源码的作用是根据执行时传入的参数(一个pathname)创建一个文件,然后在文件中写入一个C风格的数组sqrtTable

接下来要做的是在MathFunctions/CMakeLists.txt中添加合适的命令,从而编译得到相应的可执行程序,然后在整个项目的编译过程中运行这个可执行程序。具体的,在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(源文件会包含该头文件)。这可以通过把该头文件添加到MathFunctions库的源码列表来实现。具体的,修改add_library命令:

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

既然涉及到头文件包含,那么我们就必须为MathFunctions库添加相应的包含目录Table.h所在的目录),这样头文件才能被编译器找到。根据上文,这个包含目录是当前二进制目录(CMAKE_CURRENT_BINARY_DIR),这里的CMAKE_CURRENT_BINARY_DIR实际上是编译目录下的MathFunctions目录,注意区别于源码目录下的MathFunctions目录。具体的,修改命令target_include_directories

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

现在我们就可以使用生成的Table.h了。我们需要在mysqrt.cxx包含该头文件,然后重写函数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;
}

昨晚上述修改后,配置并编译整个项目。在编译过程中,MakeTable会首先被编译进而得到相应的可执行程序,然后该可执行程序会被运行以生成Table.h。接下来,包含该头文件的mysqrt.cxx会被编译以产生MathFunctions库。最终编译得到Tutorial可执行程序,运行该可执行程序,不难发现我们创建的预计算值表得到了使用。

step7 构建安装程序

接下来,假设我们想将项目分发给其他人,以便他们可以使用它。我们希望在各种平台上提供二进制和源代码分发。这与我们之前在step4中所做的安装有些不同,彼时,我们安装的是根据源代码构建的二进制文件。在本节中,我们将构建支持二进制安装和包管理功能的安装包。为此,我们将使用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则位于此步骤的*源目录(也就是step7目录)。最后,我们包含了CPack模块,这个模块将使用上述变量以及当前系统的其他一些属性来设置安装程序。

做完上述修改后,像之前一样配置并编译项目,然后运行cpack可执行程序来制作分发包。要构建二进制分发包,则需要在二进制目录(编译目录)下运行cpack命令。要指定生成器,请使用-G选项。对于多配置构建,请使用-C选项来指定配置。例如:

cpack -G ZIP -C Debug

若要创建源码分发包,你可以输入如下命令:

cpack --config CPackSourceConfig.cmake

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

构建完安装程序后,安装程序可以在二进制目录下找到,你可以运行它进行安装并运行被安装的可执行程序来验证其是否正常工作。

step8 添加对Dashboard的支持

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

# enable testing
enable_testing()

替换为

# enable dashboard scripting
include(CTest)

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

除此之外,我们还需要在顶层目录(即stepx目录x为步骤编号)创建一个名为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可执行程序被运行时会读取CTestConfig.cmake文件。要创建一个简单的Dashboard,你可以运行cmake命令或通过cmake-gui来配置项目,但不要编译,而是切换到编译目录,然后运行如下命令:

ctest [-VV] -D Experimental

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

ctest [-VV] -C Debug -D Experimental

或者从IDE中构建Experimental目标(如果你使用了IDE)。

ctest可执行文件将编译并测试项目,并将结果提交到Kitware的公共Dashboard: https://my.cdash.org/index.php?project=CMakeTutorial。

step 9 混用静态库和动态库

在本节中我们将展示如何使用BUILD_SHARED_LIBS变量来控制add_library的默认行为以及没有显式类型(STATICSHAREDMODULEOBJECT)的库如何被构建。

为此,我们需要将BUILD_SHARED_LIBS添加到顶层的CMakeLists.txt,并使用option命令,因为它允许用户设置选项的值为ONOFF。接下来,我们将重构MathFunctions,从而对mysqrtsqrt进行封装,然后在这个库里面利用USE_MYMATH来控制到底调用哪个函数计算平方根,进而在tutorial.cxx中只需固定调用库提供的sqrt函数。这也意味着USE_MYMATH不再控制库的构建,而是控制库的行为。(这么说可能不太好懂,接着看下文)

具体的,我们首先要更新顶层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

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

译者补
这里简单介绍一下上述代码中的dllexportdllimport,在动态库提供的头文件中声明函数,对于动态库来说是向外导出,因此使用dllexport;而在项目中使用动态库,包含动态库提供的头文件,对于项目来说头文件中声明的函数是导入,因此使用dllimport。同一个头文件,怎么实现两种声明呢?上述代码表达的很清楚,使用EXPORTING_MYMATH,这个宏在动态库中是有定义的,具体在MathFunctions/CMakeLists.txt

target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")

而对于项目来说,这个宏是没定义的,因此实现了同一个头文件,两种声明。

至此,如果对项目进行配置和编译,你会发现链接时会失败,因为我们将没有位置无关码的静态库(SqrtLibrary)与具有位置无关码的库(MathFunctions)组合在一起。解决方法是无论构建类型如何,都将SqrtLibraryPOSITION_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)

step10 添加生成器表达式

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

在许多目标属性(例如LINK_LIBRARIESINCLUDE_DIRECTORIESCOMPLIE_DEFINITIONS等)的上下文中都允许使用生成器表达式。在使用命令填充这些属性(例如target_link_librariestarget_include_directoriestarget_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_flagstarget_link_libraries调用。

step11 添加导出配置

step4中,我们为CMake添加了安装库和项目的头文件的功能。在step7中,我们添加了制作安装包的功能,从而可以把我们的项目分发给其他人。接下来,我们将添加必要的信息,以便其他的CMake项目能够使用我们的项目,从构建目录、本地安装或打包等方面。

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

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.

上述错误信息想要表达的是,在生成导出信息的过程中,它将导出与当前机器绑定的路径,在其他机器上无效。解决方案是更新MathFunctionstarget_include_directories,以了解从编译目录和安装/软件包中使用它时需要不同的INTERFACE位置。这意味着将MathFunctionstarget_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" )

然后,为了正确配置并安装MathFunctionsConfig.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"
)

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

step12 同时打包Debug和Release

注意:此示例对单配置生成器有效,而不适用于多配置生成器(例如VS)。

默认情况下,CMake的模型是编译目录仅包含一个配置,可以是DebugReleaseMinSizeRelRelWithDebInfo。但是,可以将CPack设置为捆绑多个编译目录,并构建一个包含同一项目的多个配置的软件包。

首先,我们要确保Debug版本和Release版本对要安装的可执行文件和库使用不同的名称,不妨使用d作为Debug版本可执行文件和库的后缀。具体的,在顶层CMakeLists.txt文件的开头附近设置CMAKE_DEBUG_POSTFIX

set(CMAKE_DEBUG_POSTFIX d)

add_library(tutorial_compiler_flags INTERFACE)

同时设置tutorial可执行程序的DEBUG_POSTFIX属性:

add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})

target_link_libraries(Tutorial PUBLIC MathFunctions)

我们还要将版本号添加到MathFunctions库中。具体的,在MathFunctions/CMakeLists.txt中,设置VERSIONSOVERSION属性:

set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")

Step12目录下创建debugrelease子目录,目录的层次关系如下:

- Step12
   - debug
   - release

现在我们需要进行DebugRelease版本的配置编译工作,我们可以使用CMAKE_BUILD_TYPE来设置配置类型:

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

DebugRelease版本都编译完后,我们就可以使用自定义配置文件将两个版本打包到一起。具体的,在Step12目录中,创建一个名为MultiCPackConfig.cmake的文件。在此文件中,首先包括由cmake可执行文件创建的默认配置文件。接下来,使用CPACK_INSTALL_CMAKE_PROJECTS变量来指定要安装的项目。在这里,我们要同时安装DebugRelease版本:

include("release/CPackConfig.cmake")

set(CPACK_INSTALL_CMAKE_PROJECTS
    "debug;Tutorial;ALL;/"
    "release;Tutorial;ALL;/"
    )

Step12目录运行cpack,使用config选项指定我们的自定义配置文件:

cpack --config MultiCPackConfig.cmake
上一篇:十一、三星平台framebuffer驱动


下一篇:nVivo highlight code中的文本