Update: 2021.02.03
Translator: shendeguize@github
Origin: https://cmake.org/cmake/help/latest/guide/tutorial/index.html
Version: 3.19.4
Link: https://github.com/shendeguize/CMakeTutorialCN

目录

介绍

这份渐进式的CMake教程覆盖了构建系统时CMake来处理的一些常见的问题.在一个样例项目中来探讨不同主题是如何结合应用是非常有助于理解的.教程文档和源码可以从CMake源路径下的Help/guide/tutorial文件夹中获得(译者注:已同步至当前Git).每一步都有其独立的子目录,这些子目录也包含可以作为出发点的代码.教程样例是循序渐进的,每一步都提供了前一步的完整解决方案.

Step1: 一个基本出发点

最基础的项目是基于源代码的一个可执行构建.对于简单项目.三行的CMakeLists.txt就满足了全部需要的内容.这就是这篇教程的开始点.在Step1路径下创建一个CMakeLists.txt文件如下:

  1. cmake_minimum_required(VERSION 3.10)
  2. # set the project name
  3. project(Tutorial)
  4. # add the executable
  5. add_executable(Tutorial tutorial.cxx)

注意在CMakeLists.txt文件中的命令都使用了小写.CMake支持大小写混用命令.tutorial.cxx的源代码在Step1文件夹下,可用以计算平方根.

添加版本号 & 配置头文件

首个添加的特性是给我们的项目和可执行文件提供版本号.尽管我们可以在源代码中添加版本号,但是使用CMakeLists.txt是更灵活的方式.

首先,修改CMakeLists.txt,使用[project()](https://cmake.org/cmake/help/latest/command/project.html#command:project)命令来设定项目名和版本号.

  1. cmake_minimum_required(VERSION 3.10)
  2. # set the project name and version
  3. project(Tutorial VERSION 1.0)

然后制定一个头文件来将版本号传递到源码里,在CMakeLists.txt中的project后面添加如下一行:

  1. configure_file(TutorialConfig.h.in TutorialConfig.h)

因为指定的文件会被写入二进制结构中,我们必须将这一目录添加到搜索include文件的列表中.在CMakeLists.txt文件结尾写入:

  1. target_include_directories(Tutorial PUBLIC
  2. "${PROJECT_BINARY_DIR}"
  3. )

使用你喜欢的IDE,在源路径下创建TutorialConfig.h.in,并写入下述内容:

  1. // the configured options and settings for Tutorial
  2. #define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
  3. #define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

当CMake生成这个头文件时,@Tutorial_VERSION_MAJOR@@Tutorial_VERSION_MINOR@的值会被自定替换.

接下来调整tutorial.cxx来包含头文件TutorialConfig.h.

  1. #include "TutorialConfig.h"

最后,更新tutorial.cxx如下以打印可执行文件名和版本号:

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

指定C++标准

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

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

我们需要在CMake代码中显式地声明以使用正确的配置.最简单的方式是在CMake中通过使用CMAKE_CXX_STANDARD以启用对特定版本C++标准的支持.对于本篇教程.将CMakeLists.txt中的CMAKE_CXX_STANDARD设为11,CMAKE_CXX_STANDARD_REQUIRED设为True.并将CMAKE_CXX_STANDARD声明置于add_executable前.

  1. cmake_minimum_required(VERSION 3.10)
  2. # set the project name and version
  3. project(Tutorial VERSION 1.0)
  4. # specify the C++ standard
  5. set(CMAKE_CXX_STANDARD 11)
  6. set(CMAKE_CXX_STANDARD_REQUIRED True)

构建与测试

运行cmake可执行文件,或者cmake-gui来配置项目,然后使用所选的构建工具来构建它.

例如,从命令行中,我们要进入Help/guide/tutorial目录下并建立一个build目录:

  1. mkdir Step1_build

之后,进入build目录,然后运行CMake来配置项目,并生成原生构建系统:

  1. cd Step1_build
  2. cmake ../Step1

然后调用这个构建系统来实际编译/链接项目:

  1. cmake --build .

最后,尝试用下述命令来使用新构建的Tutorial:

  1. Tutorial 4294967296
  2. Tutorial 10
  3. Tutorial

Step2: 添加库

现在我们会向我们的项目中添加一个库.这个库会包含我们计算数字平方根的实现.可执行文件就可以使用库而非编译器提供的平方根函数.

本篇教程里,我们会把库放在一个叫做MathFunctions的子文件夹下.这个目录已经包含了一个头文件MathFunctions.h,也包含了一个源文件mysqrt.cxx. 源文件中包含一个名为mysqrt的函数,提供了编译器中sqrt相近功能.

MathFunctions文件夹中新增下面的单行CMakeLists.txt文件:

  1. add_library(MathFunctions mysqrt.cxx)

为了使用新的库,我们在顶级的CMakeLists.txt中加入add_subdirectory()来构建库.我们向可执行文件加入新的库,并将MathFunctions添加为包含目录,这样就可以查询得到mqsqrt.h头文件了.顶级CMakeLists.txt的最后几行应该如下:

  1. # add the MathFunctions library
  2. add_subdirectory(MathFunctions)
  3. # add the executable
  4. add_executable(Tutorial tutorial.cxx)
  5. target_link_libraries(Tutorial PUBLIC MathFunctions)
  6. # add the binary tree to the search path for include files
  7. # so that we will find TutorialConfig.h
  8. target_include_directories(Tutorial PUBLIC
  9. "${PROJECT_BINARY_DIR}"
  10. "${PROJECT_SOURCE_DIR}/MathFunctions"
  11. )

接下来我们让MathFunctions库可以作为可选项.尽管本次教程不需要这样,但大型项目中这很常见.第一步是在顶层CMakeLists.txt中增加选项:

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

这一选项会在cmake-guiccmake中显示,默认值为ON,也可被用户修改.这一选项会被存储在缓存中,用户无需每次都设定.

下一项更改是将MathFunctions库的构建和链接设定成可选的.我们在顶级CMakeLists.txt的结尾做如下修改:

  1. if(USE_MYMATH)
  2. add_subdirectory(MathFunctions)
  3. list(APPEND EXTRA_LIBS MathFunctions)
  4. list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
  5. endif()
  6. # add the executable
  7. add_executable(Tutorial tutorial.cxx)
  8. target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
  9. # add the binary tree to the search path for include files
  10. # so that we will find TutorialConfig.h
  11. target_include_directories(Tutorial PUBLIC
  12. "${PROJECT_BINARY_DIR}"
  13. ${EXTRA_INCLUDES}
  14. )

变量EXTRA_LIBS收集了之后可以在可执行文件中链接的可选库.变量EXTRA_INCLUDES也相应的用于收集可选的头文件.在处理很多可选项时,这是一种经典的处理方式.下一步我们会用新方式来做.

相应的源代码改动就比较直接了.首先,在tutorial.cxx中,如果需要则包含MathFunctions.h:

  1. #ifdef USE_MYMATH
  2. # include "MathFunctions.h"
  3. #endif

然后在同一个文件中,让USE_MYMATH变量控制函数的选择:

  1. #ifdef USE_MYMATH
  2. const double outputValue = mysqrt(inputValue);
  3. #else
  4. const double outputValue = sqrt(inputValue);
  5. #endif

因为现在源代码中需要USE_MYMATH变量,我们可以在TutorialConfig.h.in文件中加入下述这行:

  1. #cmakedefine USE_MYMATH

练习: 为什么我们在选项USE_MYMATH后配置TutorialConfig.h.in.如果我们把这两条交换会发生什么.

运行cmake或者cmake-gui来配置项目,然后构建,在运行构建出的可执行文件.

现在让我们更新USE_MYMATH的值.最简单的方式是使用cmake-gui或终端中的ccmake.或者如果想在命令行中修改这一选项:

  1. cmake ../Step2 -DUSE_MYMATH=OFF

重新构建然后运行.

哪个函数结果更好,sqrt还是mysqrt?

Step3: 对库添加使用依赖

使用依赖能够让我们更好地控制库或者可执行程序使用的链接和包含.也提供了对CMake内的可及属性的更充分的控制.控制使用依赖的首要命令包括:

让我们用现代CMake的方式重构Step2中的使用依赖的部分. 我们首先明确任何链接到MathFunctions的对象都需要包含当前源目录(译者注:指MathFunctions目录),除了MathFunctions本身.所以这可以作为一个INTERFACE使用依赖.

记住INTERFACE指的是那些消费者需要而生产者不需要的东西.在MathFunctions/CMakeLists.txt的结尾加入:

  1. target_include_directories(MathFunctions
  2. INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
  3. )

现在我们已经指定了MathFunctions的使用依赖,我们就可以安全地移除顶级CMakeLists.txt文件中的EXTRA_INCLUDES变量:

  1. if(USE_MYMATH)
  2. add_subdirectory(MathFunctions)
  3. list(APPEND EXTRA_LIBS MathFunctions)
  4. endif()

以及:

  1. target_include_directories(Tutorial PUBLIC
  2. "${PROJECT_BINARY_DIR}"
  3. )

完成后,运行cmake或者cmake-gui来配置项目并通过在build目录下cmake --build .构建运行即可.

Step4: 安装与测试

现在我们开始给项目添加安装规则和测试支持.

安装规则

安装规则非常简单: 对于MathFunctions,我们希望安装库和头文件,对于应用,我们希望安装可执行文件和配置头.

所以在MathFunctions/CMakeLists.txt的结尾我们添加:

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

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

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

这就是建立一个tutorial的基础本地安装所需要的全部内容.

现在我们运行cmake或者cmake-gui来配置项目并构建.

然后通过在命令行中运行cmakeinstall选项来执行安装步骤(3.15引入,早先版本必须使用make install).对于多配制工具,要记得使用--config来指定配置.如果使用IDE,直接构建INSTALL目标即可.这一步会安装适合的头文件,库和可执行文件:

  1. camke --install .

CMake变量CMAKE_INSTALL_PREFIX用于确定文件安装的根目录.如果使用cmake --install命令,安装前驻可以被--prefix参数覆写:

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

浏览安装目录然后验证安装的Tutorial可以运行.

测试支持

接下来让我们测试我们的应用,在顶级CMakeLists.txt的结尾,我们可以打开测试功能然后加一些基本测试来验证安装正确.

  1. enable_testing()
  2. # does the application run
  3. add_test(NAME Runs COMMAND Tutorial 25)
  4. # does the usage message work?
  5. add_test(NAME Usage COMMAND Tutorial)
  6. set_tests_properties(Usage
  7. PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
  8. )
  9. # define a function to simplify adding tests
  10. function(do_test target arg result)
  11. add_test(NAME Comp${arg} COMMAND ${target} ${arg})
  12. set_tests_properties(Comp${arg}
  13. PROPERTIES PASS_REGULAR_EXPRESSION ${result}
  14. )
  15. endfunction(do_test)
  16. # do a bunch of result based tests
  17. do_test(Tutorial 4 "4 is 2")
  18. do_test(Tutorial 9 "9 is 3")
  19. do_test(Tutorial 5 "5 is 2.236")
  20. do_test(Tutorial 7 "7 is 2.645")
  21. do_test(Tutorial 25 "25 is 5")
  22. do_test(Tutorial -25 "-25 is [-nan|nan|0]")
  23. do_test(Tutorial 0.0001 "0.0001 is 0.01")

第一个测试简单验证了应用运行,没有段错误或其他崩溃发生.并有一个0返回值.这是CTest测试的基本格式.

下一个测试使用了PASS_REGULAR_EXPRESSION测试属性来验证测试输出包含特定字符串.用于在参数输入数量不对时打印使用信息.

最后,我们有一个叫做do_test的函数来运行应用并验证计算平方根的结果对于给定输出是正确的.对于每次do_test的调用,项目中就会被加入一个带有指定的名字,输入和期待结果的测试.

重新构建应用然后进入二进制目录并运行ctest可执行文件:ctest -Nctest -VV(译者注:注意是两个V).对于多配置生成器(例如Visual Studio),配置类型必须通过-C <mode>来指定.例如,如果想要在Debug模式下运行测试,则需要在构建目录下(而非Debug目录下)执行ctest -C Debug -VV.在同样的目录下,使用-C Release则可以以Release模式运行.或者也可以在IDE中构建RUN_TESTS目标.

Step5: 增加系统自检

现在我们想项目中增加一些代码,而这些代码依赖的特性可能是目标平台没有的.例如,我们要加入的代码依赖于目标平台是否有logexp函数.当然,对于每个平台而言,这些功能都是有的,但是在本篇教程中,我们假定这些功能不是都存在的.

如果平台有logexp那么我们就在mysqrt函数里使用.我们首先在顶层CMakeLists.txt里用CheckSymbolExists来测试这些函数的可用性.在一些平台,我们会需要连接到m库如果logexp没有被招到,就需要在m库里再试试.

  1. include(CheckSymbolExists)
  2. check_symbol_exists(log "math.h" HAVE_LOG)
  3. check_symbol_exists(exp "math.h" HAVE_EXP)
  4. if(NOT (HAVE_LOG AND HAVE_EXP))
  5. unset(HAVE_LOG CACHE)
  6. unset(HAVE_EXP CACHE)
  7. set(CMAKE_REQUIRED_LIBRARIES "m")
  8. check_symbol_exists(log "math.h" HAVE_LOG)
  9. check_symbol_exists(exp "math.h" HAVE_EXP)
  10. if(HAVE_LOG AND HAVE_EXP)
  11. target_link_libraries(MathFunctions PRIVATE m)
  12. endif()
  13. endif()

现在让我们给TutorialConfig.h.in添加一些定义,这样我们就可以在mysqrt.cxx里使用了:

  1. // does the platform provide exp and log functions?
  2. #cmakedefine HAVE_LOG
  3. #cmakedefine HAVE_EXP

如果logexp在系统上可用,那么我们就在mysqrt里使用它们.在MathFunctions/mysqrt.cxx里的mysqrt里添加下述代码(别忘了返回值之前加#endif):

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

我们也需要修改mysqrt.cxx来包含cmath:

  1. #include <cmath>

运行cmake或者cmake-gui来配置项目,然后构建并执行Tutorial.

会注意到我们没有使用logexp,即使我们认为它们应该是可用的.我们应该很容易发现,我们忘记在mysqrt.cxx中包含TutorialConfig.h了.

我们也需要更新MathFunctions/CMakeLists.txt,这样mysqrt.cxx才能够定位文件:

  1. target_include_directories(MathFunctions
  2. INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
  3. PRIVATE ${CMAKE_BINARY_DIR}
  4. )

这样更新后,继续并构建项目,然后运行Tutorial.如果logexp仍然没有被使用,打开构建目录下的生成的Tutorial.h文件,可能他们在当前系统下不可用的.

那个函数目前结果更好呢,sqrt还是mysqrt?

指定编译定义

除了在TutorialConfig.h中存储HAVE_LOGHAVE_EXP值以外更好的地方么?让我们试试使用target_compile_definitions().

首先将定义从TutorialConfig.h中移除,我们不再需要从mysqrt.cxx中包含TutorialConfig.h或者在MathFunctions/CMakeLists.txt中额外包含了.

接下来我们可以把HAVE_LOGHAVR_EXP的检查移动到MathFunctions/CMakeLists.txt中,然后把这些值设定为PRIVATE编译定义.

  1. include(CheckSymbolExists)
  2. check_symbol_exists(log "math.h" HAVE_LOG)
  3. check_symbol_exists(exp "math.h" HAVE_EXP)
  4. if(NOT (HAVE_LOG AND HAVE_EXP))
  5. unset(HAVE_LOG CACHE)
  6. unset(HAVE_EXP CACHE)
  7. set(CMAKE_REQUIRED_LIBRARIES "m")
  8. check_symbol_exists(log "math.h" HAVE_LOG)
  9. check_symbol_exists(exp "math.h" HAVE_EXP)
  10. if(HAVE_LOG AND HAVE_EXP)
  11. target_link_libraries(MathFunctions PRIVATE m)
  12. endif()
  13. endif()
  14. # add compile definitions
  15. if(HAVE_LOG AND HAVE_EXP)
  16. target_compile_definitions(MathFunctions
  17. PRIVATE "HAVE_LOG" "HAVE_EXP")
  18. endif()

这样调整更新后,重新构建项目,再运行Tutorial并确认结果和此前一致.

Step6: 添加自定义命令和生成文件

假设对于本次教程而言,我们决定我们不再想使用平台的logexp函数,并希望生成一些会在mysqrt函数里使用到的预计算值表.在本节,我们会建立这个表并作为构建的一步,然后编译到我们的应用中.

首先,我们移除MathFunctions/CMakeLists.txt中的对logexp的检查.然后移除mysqrt.cxx中对HAVE_LOGHAVR_EXP的检查.同事,我们也可以移除#include <cmath>

MathFunctions子目录下,有一个新文件MakeTable.cxx用于生成表格.

浏览这个文件可以发现,表格是C++代码生成的并且输出文件名是通过参数传入的.

下一步是在MathFunctions/CMakeLists.txt中添加合适的命令来构建MakeTable可执行文件,然后作为构建流程的一部分来运行.需要一些命令来完成这一步.

首先,在MathFunctions/CMakeLists.txt的开头,添加MakeTable为可执行文件目标.

  1. add_executable(MakeTable MakeTable.cxx)

然后我们添加一项定义命令来指定怎么通过运行MakeTable创建表格.

  1. add_custom_command(
  2. OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  3. COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  4. DEPENDS MakeTable
  5. )

接下来我们要让CMake知道mysqrt.cxx依赖创建的Table.h.这是通过将生成Table.h添加到MathFunctions库的源列表.

  1. add_library(MathFunctions
  2. mysqrt.cxx
  3. ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  4. )

现在让我们用上生成的表,首先,修改mysqrt.cxx来包含Table.h然后我们可以重写mysqrt函数以使用这张表:

  1. double mysqrt(double x)
  2. {
  3. if (x <= 0) {
  4. return 0;
  5. }
  6. // use the table to help find an initial value
  7. double result = x;
  8. if (x >= 1 && x < 10) {
  9. std::cout << "Use the table to help find an initial value " << std::endl;
  10. result = sqrtTable[static_cast<int>(x)];
  11. }
  12. // do ten iterations
  13. for (int i = 0; i < 10; ++i) {
  14. if (result <= 0) {
  15. result = 0.1;
  16. }
  17. double delta = x - (result * result);
  18. result = result + 0.5 * delta / result;
  19. std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  20. }
  21. return result;
  22. }

运行cmake或者cmake-gui来配置项目并构建.

当项目被构建时,首先被构建的是MakeTable,然后会运行MakeTable并创建Table.h.最后会编译包含了Table.hmysqrt.cxx来创建MathFunctions库.

运行Tutorial可执行文件然后验证使用了表格.

Step7: 构建安装器.

下一步,我们假定我们想要发布我们的项目,以便其他人可以使用我们的项目.我们想在多种平台上发布二进制和源代码.这和我们在第四步里所做的有所不同.第四步里我们安装的是从源代码构建的二进制.在本例中,我们会构建支持二进制安装和包管理特性的安装包.为此,我们会使用CPack来生成对应平台的安装器.具体而言,我们需要在我们顶级的CMakeLists.txt底添加几行:

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

就这样就可以.我们通过包含InstallRequiredSystemLibraries来开始.这一模块会包含任何项目所需的当前平台的运行库.下一步我们设定一些CPack变量到我们存储项目许可和版本信息的位置.版本信息早先在本篇教程里设定好了.license.txt在这一步被包含在顶级源目录中.

最后,我们包含CPack module.CPack模块会使用这些变量和当前系统的其他变量来配置安装器.

下一步是和正常一样构建项目然后运行cpack)可执行文件.从binary目录下运行以下命令以构建二进制发布:

  1. cpack

为了指定生成器,使用-G选项,对于多配置构建,使用-C来指定配置,例如:

  1. cpack -G ZIP -C Debug

为了构建一个源代码发布,可以使用:

  1. cpack --config CPackSourceConfig.cmake

或者运行make package,或者在IDE中右键Package目录然后Build Project.

运行在二进制文件夹中的安装器,然后运行安装的可执行文件并验证可以运行.

Step8: 增加对Dashboard的支持

添加对测试提交到仪表盘的支持是很简单的.我们在测试支持一步中已经给我们的项目定义了一系列测试.现在我们只需要运行这些测试并将他们提交到仪表盘上即可.为了包含仪表盘的支持,我们在顶级CMakeLists.txt里包含CTest模块.

  1. # enable testing
  2. enable_testing()

替换为

  1. # enable dashboard scripting
  2. include(CTest)

CTest模块会自动调用enable_testing(),所以我们就可以从CMake文件里移除这一语句.

我们也需要在顶级目录下(我们指定项目名和提交到面板的目录)建立一个CTestConfig.cmake文件.

  1. set(CTEST_PROJECT_NAME "CMakeTutorial")
  2. set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
  3. set(CTEST_DROP_METHOD "http")
  4. set(CTEST_DROP_SITE "my.cdash.org")
  5. set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")
  6. set(CTEST_DROP_SITE_CDASH TRUE)

ctest可执行文件会在运行时读取这个文件.可以运行cmake或者cmake-gui来配置项目但是不构建项目来建立一个简单的面板.切换到二进制树目录下然后运行:

  1. ctest [-VV] -C Debug -D Experimental

或者在IDE中构建Experimental目标.

ctest可执行文件会构建和测试项目并提交结果到Kitware的公共面板:https://my.cdash.org/index.php?project=CMakeTutorial.

Step9: 混合静态和共享

在本节,我们会展示BUILD_SHARED_LIBS变量是怎么样用于控制add_library()的表现.并且容许控制没有显式类型(STATIC, SHARED MODULE或者OBJECT)的库的构建.

我们需要在顶级CMakeLists.txt里增加BUILD_SHARED_LIBS.我们用option()命令来让用户可以选开或者关.

下一步我们要重构MathFunctions来使其成为一个封装了调用mysqrt或者sqrt的真实的库,而非需要调用代码来实现这个逻辑.这也意味着USE_MYMATH不再控制构建MathFunctions而是控制库的行为.

第一步是更新顶级CMakeLists.txt的第一节如下:

  1. cmake_minimum_required(VERSION 3.10)
  2. # set the project name and version
  3. project(Tutorial VERSION 1.0)
  4. # specify the C++ standard
  5. set(CMAKE_CXX_STANDARD 11)
  6. set(CMAKE_CXX_STANDARD_REQUIRED True)
  7. # control where the static and shared libraries are built so that on windows
  8. # we don't need to tinker with the path to run the executable
  9. set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
  10. set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
  11. set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
  12. option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
  13. # configure a header file to pass the version number only
  14. configure_file(TutorialConfig.h.in TutorialConfig.h)
  15. # add the MathFunctions library
  16. add_subdirectory(MathFunctions)
  17. # add the executable
  18. add_executable(Tutorial tutorial.cxx)
  19. target_link_libraries(Tutorial PUBLIC MathFunctions)

既然我们已经让MathFunctions总被使用.我们需要更新库的逻辑.因此在MathFunctions/CMakeLists.txt里我们需要建立一个SqrtLibrary.这个库会在USE_MYMATH启用的条件下构建并安装.现在,因为这只是一篇教程,我们显式地要求SqrtLibrary构建为静态库就可以了.

结果是MathFunctions/CMakeLists.txt应该如下:

  1. # add the library that runs
  2. add_library(MathFunctions MathFunctions.cxx)
  3. # state that anybody linking to us needs to include the current source dir
  4. # to find MathFunctions.h, while we don't.
  5. target_include_directories(MathFunctions
  6. INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
  7. )
  8. # should we use our own math functions
  9. option(USE_MYMATH "Use tutorial provided math implementation" ON)
  10. if(USE_MYMATH)
  11. target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
  12. # first we add the executable that generates the table
  13. add_executable(MakeTable MakeTable.cxx)
  14. # add the command to generate the source code
  15. add_custom_command(
  16. OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  17. COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  18. DEPENDS MakeTable
  19. )
  20. # library that just does sqrt
  21. add_library(SqrtLibrary STATIC
  22. mysqrt.cxx
  23. ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  24. )
  25. # state that we depend on our binary dir to find Table.h
  26. target_include_directories(SqrtLibrary PRIVATE
  27. ${CMAKE_CURRENT_BINARY_DIR}
  28. )
  29. target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
  30. endif()
  31. # define the symbol stating we are using the declspec(dllexport) when
  32. # building on windows
  33. target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")
  34. # install rules
  35. set(installable_libs MathFunctions)
  36. if(TARGET SqrtLibrary)
  37. list(APPEND installable_libs SqrtLibrary)
  38. endif()
  39. install(TARGETS ${installable_libs} DESTINATION lib)
  40. install(FILES MathFunctions.h DESTINATION include)

下一步更新MathFunctions/mysqrt.cxx以使用mathfunctionsdetail命名空间:

  1. #include <iostream>
  2. #include "MathFunctions.h"
  3. // include the generated table
  4. #include "Table.h"
  5. namespace mathfunctions {
  6. namespace detail {
  7. // a hack square root calculation using simple operations
  8. double mysqrt(double x)
  9. {
  10. if (x <= 0) {
  11. return 0;
  12. }
  13. // use the table to help find an initial value
  14. double result = x;
  15. if (x >= 1 && x < 10) {
  16. std::cout << "Use the table to help find an initial value " << std::endl;
  17. result = sqrtTable[static_cast<int>(x)];
  18. }
  19. // do ten iterations
  20. for (int i = 0; i < 10; ++i) {
  21. if (result <= 0) {
  22. result = 0.1;
  23. }
  24. double delta = x - (result * result);
  25. result = result + 0.5 * delta / result;
  26. std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  27. }
  28. return result;
  29. }
  30. }
  31. }

我们也需要在tutorial.cxx中进行调整,所有不再使用USE_MYMATH:

  1. 总是包含MathFunctions.h
  2. 总是使用mathfunctions::sqrt
  3. 不包含cmath

最后更新MathFunctions/MathFunctions.h来用dll导出定义:

  1. #if defined(_WIN32)
  2. # if defined(EXPORTING_MYMATH)
  3. # define DECLSPEC __declspec(dllexport)
  4. # else
  5. # define DECLSPEC __declspec(dllimport)
  6. # endif
  7. #else // non windows
  8. # define DECLSPEC
  9. #endif
  10. namespace mathfunctions {
  11. double DECLSPEC sqrt(double x);
  12. }

这时,如果你构建任何东西,可能会注意到链接失败,因为我们在试图将一个不包含位置无关代码(PIC)(译者注:指生成的代码中无绝对跳转指令,跳转都为相对跳转,详见维基百科)的静态库(译者注:指SqrtLibrary)和另一个包含位置无关代码(PIC)的库(译者注:指MathFunctions)组合在一起.解决方案是显式地将SqrtLibrary的POSITION_INDEPENDENT_CODE目标属性设定为True,不管是什么构建类型.

  1. # state that SqrtLibrary need PIC when the default is shared libraries
  2. set_target_properties(SqrtLibrary PROPERTIES
  3. POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
  4. )
  5. target_link_libraries(MathFunctions PRIVATE SqrtLibrary)

练习: 我们修改MathFunctions.h来使用dll导出定义.使用CMake文档你能否招到一个辅助模块来简化么?

Step10: 增加生成表达式

生成器表达式)是在构建系统生成期间执行以生成对于每一特定配置专有的信息.

生成表达式可以在许多目标属性内容中使用,诸如LINK_LIBRARIES,INCLUDE_DIRECTORIES,COMPILE_DEFINITIONS和其他一些属性.生成表达式也可以在使用命令丰富这些属性的时候使用,例如target_link_libraries(),target_include_directories(),target_compile_definitions()和其他命令.

生成表达式可用于启用条件链接,在编译时的条件定义,条件包含目录等等.这些条件可能基于构建配置,目标属性,平台信息或者其他可查询信息.

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

逻辑表达式用于创建条件输出.基本表达式是01表达式,$<0:...>结果是一个空字符串,$<1:...>结果是"..."的内容.它们也同样是可嵌套的.

生成表达式的普遍用法是依据不同条件添加编译器标志,例如语言级别的或者警告.一个好的模式是把这些信息和允许传播这些信息的INTERFACE目标关联起来.让我们从构建一个INTERFACE目标并指定需要的C++标准级别是11而非CMAKE_CXX_STANDARD开始.

故下述代码:

  1. # specify the C++ standard
  2. set(CMAKE_CXX_STANDARD 11)
  3. set(CMAKE_CXX_STANDARD_REQUIRED True)

被替换为:

  1. add_library(tutorial_compiler_flags INTERFACE)
  2. target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

下一步我们添加项目所需的预期的编译器警告标志.因为警告标志基于编译器,我们用COMPILE_LAND_AND_ID生成器表达式来控制在给定的语言和一系列编译器id下,哪些标志被使用.如下所示:

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

我们能发现警告信息被封装在BUILD_INTERFACE条件内.这样可以让安装我们项目的用户不会继承我们的警告标志.

练习:修改MathFunctions/CMakeLists.txt,使得所有的目标都有target_link_libraries()来调用tutorial_compiler_flags.

Step11 增加导出配置

在第四步中,我们为CMake增加了安装库和头文件的功能.在第七步我们增加了打包这些信息以便可以发布给给其他人的能力.

下一步是增加必要信息使得其他的CMake项目可以使用我们的项目,无论是基于构建目录,本地安装还是作为软件包使用.

第一步是更新我们的[install(TARGETS)](https://cmake.org/cmake/help/latest/command/install.html#command:install)命令来不仅仅指定DESTINATION也指定EXPORT.EXPORT关键字生成一个CMake文件,其中含有能够导入安装树中安装命令所列出的所有目标的代码.于是我们可以通过更新MathFunctions/CMakeLists.txt里的install命令来显式地导出(EXPORT)MathFunctions库:

  1. set(installable_libs MathFunctions tutorial_compiler_flags)
  2. if(TARGET SqrtLibrary)
  3. list(APPEND installable_libs SqrtLibrary)
  4. endif()
  5. install(TARGETS ${installable_libs}
  6. DESTINATION lib
  7. EXPORT MathFunctionsTargets)
  8. install(FILES MathFunctions.h DESTINATION include)

现在我们已经导出了MathFunctions,我们也需要显式地安装生成的MathFunctionsTargets.cmake文件.这是通过在顶级CMakeLists.txt的底部添加:

  1. install(EXPORT MathFunctionsTargets
  2. FILE MathFunctionsTargets.cmake
  3. DESTINATION lib/cmake/MathFunctions
  4. )

这时应该试着运行CMake.如果设置都正确的话,CMake应该会报错如下:

  1. Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
  2. path:
  3. "/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions"
  4. which is prefixed in the source directory.

CMake报错描述的是在生成导出信息期间,会导出内在绑定到当前设备的路径,在其他设备上可能无效.解决方案是更新MathFunctions的target_include_directories(),以明确在从构建目录使用和从安装包使用时需要不同的INTERFACE位置.这意味着MathFunctions的target_include_directories()调用应改为如下:

  1. target_include_directories(MathFunctions
  2. INTERFACE
  3. $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
  4. $<INSTALL_INTERFACE:include>
  5. )

这一项更新后,我们就可以重新运行CMake并验证不再有警告了.

到这里,我们已经将CMake配置打包好了所需的目标信息.但是我们也仍然需要生成MathFunctionsConfig.cmake以使得CMake的find_package()能够找到我们的项目.所以我们继续在顶级项目下建立名为Config.cmake.in的文件并写入下述代码:

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

然后为了正确配置安装这个文件.将下述内容写入顶级CMakeLists.txt的结尾:

  1. install(EXPORT MathFunctionsTargets
  2. FILE MathFunctionsTargets.cmake
  3. DESTINATION lib/cmake/MathFunctions
  4. )
  5. include(CMakePackageConfigHelpers)
  6. # generate the config file that is includes the exports
  7. configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
  8. "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
  9. INSTALL_DESTINATION "lib/cmake/example"
  10. NO_SET_AND_CHECK_MACRO
  11. NO_CHECK_REQUIRED_COMPONENTS_MACRO
  12. )
  13. # generate the version file for the config file
  14. write_basic_package_version_file(
  15. "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  16. VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  17. COMPATIBILITY AnyNewerVersion
  18. )
  19. # install the configuration file
  20. install(FILES
  21. ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
  22. DESTINATION lib/cmake/MathFunctions
  23. )

这里,我们已经生成了一个可重定位的CMake配置并是可以在项目安装或者打包后使用的.如果我们想让我们的项目也能够在构建目录使用.我们只需要在顶级CMakeLists.txt的结尾添加下述内容:

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

有这一导出后,我们现在生成一个Targets.cmake,使得在构建目录里配置好的MathFunctionsConfig.cmake可以被其他项目使用而无需安装.

STep12: 打包Debug和Release

注意: 这个例子对于单配置生成器有效.而对于多配置生成器无效(例如Visual Studio)

默认情况下,CMake模型是一个构建目录只包含一个配置,也就是Debug,Release,MinSizeRel或者RelWithDebInfo等的构建目录.但是是可以通过CPack打包多个构建目录以及构建一个包含同一项目多个配置的包.

首先,我们想要确认debug和release的构建使用不同的可执行和库名字.让我们使用d作为调试的可执行和库的后缀.

在顶级CMakeLists.txt文件的开头设定CMAKE_DEBUG_POSTFIX:

  1. set(CMAKE_DEBUG_POSTFIX d)
  2. add_library(tutorial_compiler_flags INTERFACE)

给tutorial的可执行文件添加DEBUG_POSTFIX属性:

  1. add_executable(Tutorial tutorial.cxx)
  2. set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})
  3. target_link_libraries(Tutorial PUBLIC MathFunctions)

让我们也给MathFunctions库添加版本号.在MathFunctions/CMakeLists.txt设定VERSIONSOVERSION属性:

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

Step12目录,创建debugrelease子目录,结构如下:

  1. - Step12
  2. - debug
  3. - release

现在我们需要设定debug和release构建.我们可以用CMAKE_BUILD_TYPE来设定配置类型:

  1. cd debug
  2. cmake -DCMAKE_BUILD_TYPE=Debug ..
  3. cmake --build .
  4. cd ../release
  5. cmake -DCMAKE_BUILD_TYPE=Release ..
  6. cmake --build .

现在debug和release的构建都完成了.我们可以用自定义配置文件来将两个构建都打包到同一个发布包中.在Step12目录里,创建一个名为MultiConfig.cmake的文件.在文件中,首先包含通过cmake创建的默认配置文件.

接下来,用CPACK_INSTALL_CMAKE_PROJECTS变量来指定安装哪个项目.这时,我们想安装debug和release两个版本:

  1. include("release/CPackConfig.cmake")
  2. set(CPACK_INSTALL_CMAKE_PROJECTS
  3. "debug;Tutorial;ALL;/"
  4. "release;Tutorial;ALL;/"
  5. )

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

  1. cpack --config MultiCPackConfig.cmake