CMake教程

CMake教程

CMake是一个构建系统,可以用于构建跨平台的C和C++项目。其它的构建系统有makefile、MSBuild以及qmake。

使用CMake作为C或C++的构建系统已经变得越来越流行了,例如:

在使用一个第三方库时,例如Libevent。无论是在Linux还是Windows中,我们只需要进入它的源代码目录,然后在命令行中执行:

mkdir build
cd build
cmake ..
cmake --build .
cmake --install .

之后在我们自己的项目中,只需要在CMakeLists.txt中添加:

find_package(Libevent REQUIRED core)
target_link_libraries(你的可执行目标 libevent::core)

即可使用该库。

对于一部分人来说,了解上面这些就足够了。但如果想进一步地学习CMake构建系统,可以查看CMake官方文档CMake Wiki

下面是CMake官方文档中的一篇教程,你可以跟着这些步骤一步一步地操作,并学习CMake构建系统。同时,我们还能感受到C或C++为了跨平台而付出的努力。

文章目录

引入

本教程是循序渐进的,它包括CMake能解决的常见的构建系统问题。在一个示例项目中查看不同的主题之间如何协同工作,对于理解CMake非常有帮助。该教程中所有案例的源代码可以在CMake源码下的Help/guide/tutorial目录中找到(GitHub链接)。每一步都有独立的子目录,其中包含了可能会用到的代码。教程示例是渐进式的,因此每个步骤都为上一步提供了完整的解决方案。

第1步 开始

最基础的项目是将源代码构建成可执行文件。对于一个简单的项目,只需要一个3行的CMakeLists.txt文件即可。在Step1目录下创建一个CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.10)

# 设置项目名
project(Tutorial)

# 添加可执行目标
add_executable(Tutorial tutorial.cxx)

注意:CMake支持大写、小写或大小写混合命令。但在这些例子中,每个CMakeLists.txt文件都使用小写命令。tutorial.cxx中的源代码放在Step1目录下,它用于计算一个数的平方根。

添加版本号并且配置头文件

我们将添加的第一个特性是:为我们的可执行文件和项目提供一个版本号。尽管我们可以在源代码中做到这一点,但是使用CMakeLists.txt会更加灵活。

首先,修改CMakeLists.txt文件,使用project()命令设置项目名和版本号:

cmake_minimum_required(VERSION 3.10)

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

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

configure_file(TutorialConfig.h.in TutorialConfig.h)

因为配置的文件将被写到binary tree目录下(CMakeCache.txt所在的目录,即build目录),所以我们必须将那个目录添加到include搜索路径列表中。将下面这行添加到CMakeLists.txt文件的末尾:

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

使用你最喜欢的编辑器,在源代码目录(Step1目录)中创建TutorialConfig.h.in,其中包含以下内容:

// 该教程的配置选项和设置
#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) {
    // 打印版本号
    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中用std::stod替换atof。同时,删除#include<cstdlib>

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

在CMake中指定C++标准最简单的方法是使用CMAKE_CXX_STANDARD变量。将CMakeLists.txt文件中的CMAKE_CXX_STANDARD变量设置为11,并将CMAKE_CXX_STANDARD_REQUIRED设置为True。确保将CMAKE_CXX_STANDARD声明添加到add_executable的上面:

cmake_minimum_required(VERSION 3.10)

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

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

构建和测试

运行 cmakecmake-gui 来配置(configure)项目,然后使用你所选的构建工具构建(build)它。

例如,在命令行中,我们应该导航到CMake源码中的Help/guide/tutorial目录中,然后创建构建目录:

mkdir Step1_build

接下来,导航到构建目录并运行CMake,让CMake配置项目并生成一个本地构建系统:

cd Step1_build
cmake ../Step1

然后调用那个构建系统来编译、链接该项目:

cmake --build .

最后,尝试使用新生成可执行文件Tutorial,你可以运行以下命令:

Tutorial 4294967296
Tutorial 10
Tutorial

第2步 添加一个库

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

我们把这个库放进一个名为MathFunctions的子目录中,这个目录已经包含了一个头文件MathFunctions.h和一个源文件mysqrt.cxx。这个源文件有一个名为mysqrt的函数,它提供了和标准库中的sqrt相似的功能。

MathFunctions目录中创建CMakeLists.txt文件,并将下面这行添加到MathFunctions目录中的CMakeLists.txt文件中:

add_library(MathFunctions mysqrt.cxx)

要利用这个库,我们需要在*目录(Step2目录)下的CMakeLists.txt文件中添加一个add_subdirectory()调用,以便该库可以被构建。我们在可执行目标中添加这个新的库,然后添加MathFunctions目录作为include目录以便头文件mqsqrt.h能够被找到。根目录(Step2目录)下CMakeLists.txt文件的最后几行现在应该如下所示:

# 添加MathFunctions库
add_subdirectory(MathFunctions)

# 添加可执行目标
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC MathFunctions)

# 添加binary tree目录到include文件的搜索路径
# 以便我们能找到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)

# 配置一个头文件,传递一些CMake设置到源代码
configure_file(TutorialConfig.h.in TutorialConfig.h)

这个选项将显示在 cmake-guiccmake 中,使用默认值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_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# 添加binary tree目录到include文件的搜索路径
# 以便我们能找到TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           ${EXTRA_INCLUDES}
                           )

注意:使用变量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?如果我们将两者的顺序对调会发生什么。

运行 cmakecmake-gui 配置并构建项目,然后运行生成的可执行文件。

现在让我们更新USE_MYMATH的值。最简单的方式是使用 cmake-guiccmake ,如果你在终端中的话。或者,如果你想通过命令行更改选项的话,你可以:

cmake ../Step2 -DUSE_MYMATH=OFF

然后构建并重新运行。

sqrt和mysqrt,哪个结果更准确?

第3步 为库添加使用需求

使用需求(Usage requirements)让我们可以对库或可执行文件的链接和include行进行更好的控制,同时也让我们对CMake中目标的可传递属性进行更多的控制。利用使用需求的主要命令是:

现在,让我们使用现代的CMake方式(通过使用需求)重构第2步。首先声明任何链接到MathFunctions的人都需要包含当前的源目录,而MathFunctions本身不需要。所以这可以成为一个INTERFACE使用需求。(相关内容请参考CMake构建系统https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html

记住,INTERFACE意味着库的使用者需要,但库的设计者不需要。添加下面这行到MathFunctions/CMakeLists.txt文件尾:

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          )

现在,我们已经指定了MathFunctions的使用需求,我们可以从根目录安全的删除EXTRA_INCLUDES变量的使用:

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

还有这里:

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

完成这些操作之后,运行 cmakecmake-gui 来配置并构建项目。

第4步 安装(Installing)和测试(Testing)

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

安装规则(Install Rules)

安装规则相当简单:对于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
  )

这就是创建一个基本的本地安装规则所需的全部内容。

现在,运行 cmakecmake-gui 配置并构建项目。

然后,在命令行中使用cmake 命令的安装选项(CMake 3.15中引入,旧版的CMake必须使用make install等本地的构建命令)。对于多配置的工具,不要忘了使用--config参数时指定配置。如果使用一个IDE,你只需要构建INSTALL这个目标。这将安装合适的头文件、库和可执行文件。例如:

cmake --install.

默认情况下,需要以管理员身份运行,否则可能会权限不足。

CMake变量CMAKE_INSTALL_PREFIX用于决定这些文件被安装到哪里,它是一个安装前缀,即一个目录。如果使用cmake --install命令,可以通过--prefix参数重写安装前缀。例如:

cmake --install . --prefix "/home/myuser/installdir"

导航到安装目录,然后验证被安装的Tutorial程序。

测试支持(Testing Support)

接下来让我们测试我们的应用程序。在*目录下的CMakeLists.txt文件中启用测试,然后添加一些基本测试样例去验证应用程序是否正确工作。

enable_testing()

# 运行程序
add_test(NAME Runs COMMAND Tutorial 25)

# 是否能打印程序的使用方式?
add_test(NAME Usage COMMAND Tutorial)
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(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")

第一个测试只验证了程序的运行,是否崩溃,是否返回0值。这是一个CTest测试的基本形式。

下一个测试利用PASS_REGULAR_EXPRESSION测试属性验证输出是否包含某些字符串。在这个情况下,验证当参数数量错误时是否打印了用法信息。

最后,定义了一个函数do_test,它运行这个程序并且验证给定输入的平方根是否正确。每一次调用do_test都会使用名字、输入和预期的结果去添加一个测试。

重新构建这个程序然后进入构建目录,运行ctest -Nctest -VV。对于多配置的生成器(例如Visual Studio),必须指定配置类型。例如,要在调试模式下运行测试,在构建目录下使用ctest -C Debug -vv,或者,在IDE中构建RUN_TESTS目标。

第5步 添加系统自省(System Introspection)

考虑添加一些依赖于平台的代码到我们的项目中。例如,我们将添加一些代码,他取决于目标平台是否有logexp函数。当然,几乎所有平台都有这些函数,但在这个教程中假定它们不常见。

如果目标平台有logexp函数,那么我们将使用它们在mysqrt函数中计算平方根。首先在MathFunctions目录下的CMakeLists.txt文件中使用CheckSymbolExists 模块来测试这些函数是否可用。在某些平台(例如Linux)需要链接库m。如果第一次没有找到logexp,我们需要链接库m并重试。

我们将在TutorialConfig.h.in中使用新的定义,因此确保在那个文件被配置之前设置它们。

include(CheckSymbolExists)
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)
if(NOT (HAVE_LOG AND HAVE_EXP))
  unset(HAVE_LOG CACHE)
  unset(HAVE_EXP CACHE)
  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_link_libraries(MathFunctions PRIVATE m)
  endif()
endif()

然后增加以下定义到TutorialConfig.h.in以便我们能在mysqrt.cxx中使用他们:

// 目标平台提供了exp和log函数吗?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

如果logexp在当前系统中可用,那么我们将在mysqrt函数中使用它们计算平方根。添加以下代码到MathFunctions/mysqrt.cxx下的mysqrt函数中(在返回结果前别忘了添加#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;

我们也需要在mysqrt.cxx中包含cmath

#include <cmath>

运行 cmakecmake-gui 配置并构建项目,然后运行。

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

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

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

在更新之后,继续构建并运行项目。如果logexp仍然不能使用,打开生成的TutorialConfig.h文件,也许他在当前系统不可用?

哪个函数得出了更准确的结果,sqrt还是mysqrt?

指定编译器定义

是否有比TutorialConfig.h更好的地方来保存HAVE_LOGHAVE_EXP的值?让我们尝试使用target_compile_defintions

首先,从TutorialConfig.h.in中移除定义。在mysqrt.cxx或者MathFunctions/CMakeLists.txt中不需要再包含TutorialConfig.h

接下来,在MathFunctions/CMakeLists.txt中移除对HAVE_LOGHAVE_EXP的检查,然后将这些值添加到PRIVATE编译器定义。

include(CheckSymbolExists)
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)
if(NOT (HAVE_LOG AND HAVE_EXP))
  unset(HAVE_LOG CACHE)
  unset(HAVE_EXP CACHE)
  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_link_libraries(MathFunctions PRIVATE m)
  endif()
endif()

# 添加表编译器定义
if(HAVE_LOG AND HAVE_EXP)
  target_compile_definitions(MathFunctions
                             PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()

继续构建并测试该项目。

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

我们还需要添加当前的binary(就是build文件夹下的MathFunctions)目录到include目录列表,以便Table.h可以被找到并被mysqrt.cxx包含。

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

  // 使用表来帮助找到一个初始值
  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)];
  }

  // 进行10次迭代
  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;
}

运行cmakecmake-gui配置并构建项目。

当项目被构建时,它会首先构建可执行目标MakeTable。然后运行MakeTable去生成Table.h。最后,包含Table.hmysqrt.cxx将被编译成MathFunctions库。

运行可执行文件并检验是否使用了这张表。

第7步 构建一个安装程序

接下来,假定我们想将我们的项目发布给其他人。我们希望提供在多种平台上的二进制文件和源代码。这和我们在第4步讲的安装有一些不同,在第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。要构建一个二进制版本,在binary目录(build文件夹内)运行:

cpack

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

cpack -G ZIP -C Debug

要创建一个源代码版本,输入:

cpack --config CPackSourceConfig.cmake

同样,你也可以运行make package或在IDE中构建Package目标达到相同的目的。

运行在binary目录下找到的安装程序,然后运行被安装的可执行程序并验证它的工作。

注意:在Windows上,你可能需要安装NSIS才能正常打包。

第8步 添加仪表盘支持

我们很容易将测试结果提交到一个仪表盘上。我们已经为我们的项目定义了测试编号。现在,我们只需要运行那些测试并将它们提交到一个仪表盘。为了包含对仪表盘的支持,我们在*目录下的CMakeLists.txt中包含了CTest模块。

将:

# 启用测试
enable_testing()

替换为:

# 启用仪表盘脚本
include(CTest)

CTest模块将自动调用enable_testing(),所以我们可以从CMake文件中移除它。

我们也需要在*目录下创建一个CTestConfig.cmake文件,在这个文件中指定项目名和要提交到的仪表盘。

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程序将在运行时读取该文件。为了创建一个简单的仪表盘,你可以运行cmakecmake-gui来配置项目,但不要构建它。作为替换,将目录切换到binary tree,然后运行:

ctest [-VV] -D Experimental

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

ctest [-VV] -C Debug -D Experimental

或者从IDE中构建Experimental目标。

ctest将构建和测试该项目,然后提交测试结果到Kitware的公共仪表盘

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

在这步中,我们将展示如何使用变量BUILD_SHARED_LIBS来控制add_library()的默认行为,并控制没有显式类型(STATIC, SHARED, MODULEOBJECT)的库将如何被构建。

为了完成这些,我们需要将BUILD_SHARED_LIBS添加到*目录下的CMakeLists.txt文件中。通过option()命令让用户选择某个值是为ON还是OFF。

接下来我们将重构MathFunctions,让它变成一个更加实际的库,它封装了mysqrtsqrt,而不是让调用代码实现这个逻辑。这也意味着USE_MYMATH将不再控制构建MathFunctions,而是控制库的行为。

首先将*CMakeLists.txt的起始部分更新为:

cmake_minimum_required(VERSION 3.10)

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

# 指定C++标准
set(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 "Build using shared libraries" ON)

# 配置一个头文件,只传递版本号
configure_file(TutorialConfig.h.in TutorialConfig.h)

# 添加MathFunctions库
add_subdirectory(MathFunctions)

# 添加可执行目标
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)

注意,我们现在使MathFunctions总是被使用,我们还需要更新这个库的逻辑。所以,在MathFunctions/CMakeLists.txt文件中,我们需要创建一个SqrtLibrary,它将在USE_MYMATH被启用时构建和安装。现在,我们显式地让SqrtLibrary被静态构建。

最终的MathFunctions/CMakeLists.txt文件应该为:

# 添加库
add_library(MathFunctions MathFunctions.cxx)

# 表示任何链接到这个库的人都需要包含当前的源代码目录
# 以找到头肩问MathFunctions.h,但这个库本身不需要
target_include_directories(MathFunctions
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )

# 是否使用我们自己的数学函数
option(USE_MYMATH "Use tutorial provided math implementation" ON)
if(USE_MYMATH)

  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

  # 首先添加生成表的可执行目标
  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
    )

  # 计算平方根的库
  add_library(SqrtLibrary STATIC
              mysqrt.cxx
              ${CMAKE_CURRENT_BINARY_DIR}/Table.h
              )

  # 表示查找Table.h文件时依赖于binary目录
  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")

# 安装规则
set(installable_libs MathFunctions)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

然后更新MathFunctions/mysqrt.cxx,使用mathfunctions和命名空间detail

#include <iostream>

#include "MathFunctions.h"

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

namespace mathfunctions {
namespace detail {
// 一个简单的计算平方根算法
double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  // 使用表来帮助找到一个初始值
  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)];
  }

  // 进行10次迭代
  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 // 不是Windows
#  define DECLSPEC
#endif

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

此时,如果你构建该项目,你可能看到一个链接错误,因为我们将一个没有PIC的静态库与一个有PIC的库相结合。解决方案是设置SqrtLibrary 的POSITION_INDEPENDENT_CODE目标属性为True,并删除构建类型。

  # 表示当默认为共享库时,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_LIBRARIESINCLUDE_DIRECTORIESCOMPILE_DEFINITIONS以及其他的属性。

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

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

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

生成器表达式的一个常见的用法是有条件地添加编译器标志,例如语言级别或警告。一个良好的模式是将这些信息关联到一个INTERFACE目标,这个目标使这些信息可传递。让我们构造一个INTERFACE目标并指定C++标准为11,而不是直接使用CMAKE_CXX_STANDARD

因此,这些代码:

# 指定C++标准
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生成器表达式去控制它们:

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()调用。

第11步 添加导出配置

在第4步(安装和测试)中,我们使项目可以安装库和头文件。在第7步(构建一个安装程序)中,我们我们使项目可以将其信息打包起来并分发给其他人。

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

第一步是更新我们的install(TARGETS)命令,使它指定一个DESTINATION和一个EXPORTEXPORT关键词会生成并安装一个CMake文件,这个文件包含了导入所有被列在安装命令中的目标的代码。所以,更新MathFunctions/CMakeLists.txt中的install 命令,使其EXPORT MathFuncions库:

set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs}
        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想表达的是,在生成导出信息时,它将导出一个路径,该路径和当前机器有内在联系,并且不能在其他机器上生效。解决方案是更新MathFunctions的target_include_directories(),告诉他需要不同的INTERFACE位置,取决于当前来自构建目录内部,还是来自安装后或一个包内。这意味着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)
# 生成配置文件,它包含了导出内容
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
  )
# 为配置文件生成版本文件
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  COMPATIBILITY AnyNewerVersion
)

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

此时,我们已经为我们的项目生成了一个可重定位的配置,它可以用于之后的安装或打包。如果我们希望我们的项目也能从构建目录中使用,我们只需要添加下面这行到*CMakeLists.txt的底部:

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

调用export将生成Targets.cmake,允许构建目录中的MathFunctionsConfig.cmake被其他项目使用,而不需要安装它。

第12步 打包调试和发布

注意:这个例子只能用于单配置生成器,不能工作于多配置生成器(例如Visual Studio)。

默认形况下,CMake的构建目录只能包含一个单独的配置,可以是Debug, Release, MinSizeRel, 或 RelWithDebInfo。然而可以让CPack打包多个构建目录,并构造一个包含了当前项目的多个配置的包。

首先,我们要确保debug和release构建为可执行目标和库目标使用不同的名字。我们为调试的可执行目标和库目标添加后缀d

在*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

现在,我们需要设置debug和release构建。我们可以使用CMAKE_BUILD_TYPE去设置配置类型:

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

现在,debug和release构建都已经完成,我们可以使用一个自定义配置文件去统一发布这两个构建。在Step12目录中,创建一个名为MultiCPackConfig.cmake的文件。在这个文件中,先包含被cmake创建默认的配置文件。

然后,使用CPACK_INSTALL_CMAKE_PROJECTS变量去指定要安装的项目。此时,我们希望同时安装debug和release。

include("release/CPackConfig.cmake")

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

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

cpack --config MultiCPackConfig.cmake
上一篇:【转】php curl 伪造IP来源的实例代码


下一篇:CMake 入门实战