万字总结简化跨平台编译利器CMake,从入门到项目实战演练!(中)

简介: 万字总结简化跨平台编译利器CMake,从入门到项目实战演练!(中)

1.2CMake指南教程(官方地址

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

(第1步)基本起点

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

cmake_minimum_required(VERSION 3.10)

# set the project name project(Tutorial)

# add the executable add_executable(Tutorial tutorial.cxx)

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

添加版本号和配置头文件

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

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

cmake_minimum_required(VERSION 3.10)

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

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

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

configure_file(TutorialConfig.h.in TutorialConfig.h)

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

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

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

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

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

当CMake配置这个头文件时,@Tutorial_VERSION_MAJOR@和@Tutorial_VERSION_MINOR@的值将被替换。接下来,修改tutorial.cxx以包括配置的头文件TutorialConfig.h。最后,通过更新tutorial.cxx来打印出版本号,如下所示:

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

完整的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.10)

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

configure_file(TutorialConfig.h.in TutorialConfig.h)

#add the executable
add_executable(Tutorial tutorial.cxx)

target_include_directories(Tutorial PUBLIC  "${PROJECT_BINARY_DIR}"  )

指定c++标准

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

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

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

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

构建和测试

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

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

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

Tutorial 4294967296
Tutorial 10
Tutorial

(第2步)添加库

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

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

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

add_library(MathFunctions mysqrt.cxx)

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

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

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

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

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

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

option(USE_MYMATH "Use tutorial provided math implementation" ON)
# configure a header file to pass some of the CMake settings to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

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

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

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

# add the executable
add_executable(Tutorial tutorial.cxx)

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

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

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

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

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

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

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

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

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

#cmakedefine USE_MYMATH

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

完整的CMakeLists.txt文件如下:

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

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

option(USE_MYMATH "Use tutorial provided math implementation" ON)

# configure a header file to pass some of the CMake settings to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

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

# add the executable
add_executable(Tutorial tutorial.cxx)

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

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

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

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

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

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

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

target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR)

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

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

target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")

(第4步)安装与测试

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

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

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

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

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

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

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

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

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

测试支持

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

enable_testing()

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

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

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

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

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

###早期版本的写法
###include(CTest)
###add_test (TutorialRuns Tutorial 25)
###
###add_test (TutorialComp25 Tutorial 25)
###set_tests_properties (TutorialComp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")
###
##add_test (TutorialUsage Tutorial)
###set_tests_properties (TutorialUsage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")
###
####define a macro to simplify adding tests, then use it
###macro (do_test arg result)
##add_test (TutorialComp${arg} Tutorial ${arg})
###set_tests_properties (TutorialComp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
###endmacro (do_test)
###
###do_test(4 "4 is 2")
###do_test(9 "9 is 3")
###do_test(5 "5 is 2.236")
###do_test(7 "7 is 2.645")
###do_test(25 "25 is 5")
###do_test(-25 "-25 is [-nan|nan|0]")
###do_test(0.0001 "0.0001 is 0.01")

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

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

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

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

(第5步)添加系统自检

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

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

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

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

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

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

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

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

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

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

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

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

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

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

指定编译定义

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

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

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

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

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

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

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

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

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

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

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

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

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

add_executable(MakeTable MakeTable.cxx)

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

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

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

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

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

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

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

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

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

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

 return result;
}

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

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

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

include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include(CPack)

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

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

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

cpack

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

cpack -G ZIP -C Debug

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

cpack --config CPackSourceConfig.cmake

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

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

(第8步)添加Dashboard支持

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

# enable dashboard scripting
include(CTest)

替换

# enable testing

enable_testing()

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

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

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

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

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

ctest [-VV] -D Experimental

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

ctest [-VV] -C Debug -D Experimental

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

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

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

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

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

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

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

cmake_minimum_required(VERSION 3.10)

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

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

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

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

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

# add the MathFunctions library
add_subdirectory(MathFunctions)

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

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

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

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

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

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

 target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

 # first we add the executable that generates the table
 add_executable(MakeTable MakeTable.cxx)

 # add the command to generate the source code
 add_custom_command(
   OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
   COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
   DEPENDS MakeTable
   )

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

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

 target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()

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

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

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

#include <iostream>

#include "MathFunctions.h"

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

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

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

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

 return result;
}
}
}

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

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

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

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

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

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

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

 target_link_libraries(MathFunctions PRIVATE SqrtLibrary)

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

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

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

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

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

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

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

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

所以,下面的代码:

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

将被替换为:

add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

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

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

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

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

(第11步)增加输出配置

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

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

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

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

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

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

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

Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
path:

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

which is prefixed in the source directory.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

─ multi_config
   ├── debug
   └── release

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

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

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

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

相关文章
|
1月前
|
Unix Shell Linux
赞!优雅的Python多环境管理神器!易上手易操作!
赞!优雅的Python多环境管理神器!易上手易操作!
|
5月前
|
开发框架 移动开发 小程序
强烈推荐:绝对是好用的小程序开源框架
强烈推荐:绝对是好用的小程序开源框架
133 0
|
3月前
|
传感器 Python
Python实战开发演练之牲畜智能饮水机
Python实战开发演练之牲畜智能饮水机
|
5月前
|
NoSQL 测试技术 Shell
万字总结简化跨平台编译利器CMake,从入门到项目实战演练!(下)
万字总结简化跨平台编译利器CMake,从入门到项目实战演练!(下)
|
5月前
|
Unix Linux 编译器
万字总结简化跨平台编译利器CMake,从入门到项目实战演练!(上)
万字总结简化跨平台编译利器CMake,从入门到项目实战演练!
《阿里巴巴Java开发手册》IDEA插件使用,提升代码质量的利器
《阿里巴巴Java开发手册》IDEA插件使用,提升代码质量的利器
359 0
|
Java 测试技术 应用服务中间件
测试利器 | 一款开源的Diffy自动化测试框架:超详细实战教程讲解
测试利器 | 一款开源的Diffy自动化测试框架:超详细实战教程讲解
514 0
测试利器 | 一款开源的Diffy自动化测试框架:超详细实战教程讲解
|
小程序 JavaScript API
01-小程序:开发入门篇
01-小程序:开发入门篇
356 0
01-小程序:开发入门篇
|
编译器 C++ 前端开发
带你读《LLVM编译器实战教程》之三:工具和设计
本书的前半部分将向您介绍怎么样去配置、构建、和安装LLVM的不同软件库、工具和外部项目。接下来,本书的后半部分将向您介绍LLVM的各种设计细节,并逐步地讲解LLVM的各个编译步骤:前段、中间表示(IR)、后端、即时编译(JIT)引擎、跨平台编译和插件接口。本书包含有大量翔实的示例和代码片段,以帮助读者平稳顺利的掌握LLVM的编译器开发环境。
从零开始搭建Java开发环境第四篇:精选IDEA中十大提高开发效率的插件!
Lombok 知名的插件,无需再写那么多冗余的get/set代码 JRebel 热部署插件 alibaba java coding guide 阿里巴巴代码规范插件,自动检查代码规范问题 GenerateAllSetter 当你进行对象之间赋值的时候,你会发现好麻烦呀,能不能有一个更好的办法呢~ .