CMake 教程 01

介绍

这篇CMake教程提供了手把手的指导,涵盖了CMake中大部分构建问题。查看示例项目中的不同主题一起工作非常有用的。这个教程文档和示例可以在CMake提供的源代码的Help/guide/tutorial目录中找到。每个步骤都是包含着代码的子目录构成,可以作为学习的启动步骤。这个教程的例子是循序渐进的,每一步都为前一步提供解决方案。

第1步,基础起始点

最基础的工程是从源代码构建的可执行文件。对于简单工程,CMakeLists只需要三行代码即可,这会作为我们教程的起始点。在Step1目录中创建如下的CMakeLists文件:

cmake_minimum_required(VERSION 3.10)# set the project nameproject(Tutorial)# add the executableadd_executable(Tutorial tutorial.cxx)

可以注意到上面的例子在CMakeLists.txt中使用了小写的命令字。CMake支持大写,小写和混合的命令。tutorial.cxx在Step1目录里提供,可以用来计算某个数的平方根。

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

我们添加的第一个特性是给我们的运行程序和工程提供一个版本号。当然我们也可以在源代码中定义,使用CMakeLists.txt可以提供更多的便利。

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

cmake_minimum_required(VERSION 3.10)# set the project and versionproject(Tutorial VERSION 1.0)

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

configure_file(TutorialConfig.h.in TutorialConfig.h)

这个配置文件会被写入二进制树,我们必须将那个目录添加到查找包含文件的路径中。在CMakeLists.txt文件中增加如下行:

target_include_directories(Tutorial PUBLIC

"${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;

}

指定C++标准

接下来,让我们添加一些C++11的标准,在tutorial.cxx中使用std::stod来替代atof。同时,移除掉#include。

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之上。

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

编译并测试

运行cmake命令或者使用cmake-gui来配置项目,然后使用你选择的编译工具编译。

例如,在命令行中,可以导航到CMake源代码的Help/guide/tutorial目录,创建一个编译目录:

mkdir Step1_build

接下来,进入到编译目录中,运行CMake命令来配置项目同时生成一个原生的编译系统:

cd Step1_build

cmake ../Step1

然后调用编译命令来实际编译和链接项目:

cmake --build .

最后,使用新编译的Tutorial运行程序运行如下命令:

Tutorial 4294967296

Tutorial 10

Tutorial

第2步,添加库

现在,我们会向工程中添加库。这个库会包含我们对于计算某个数的平方根的实现代码。运行程序可以使用库提供的方法来代替编译器提供的方法。

对于教程而言,我们会将库代码放在MathFunctions的子目录中。这个目录已经包含了一个头文件MathFunctions.h,和一个源代码文件mysqrt.cxx。源文件有一个mysqrt的函数提供计算平方根功能。

在MathFunctions目录下的CMakeLists.txt文件中添加如下行:

add_library(MathFunctions mysqrt.cxx)

为了使用新添加的库,我们需要在顶层的CMakeLists.txt文件中添加add_subdirectory()调用,这样新库才会被构建。我们将新库添加到运行文件,同时将MathFunctions添加到包含目录中,这样mysqrt.h文件就可以被找到。顶层的CMakeLists.txt文件最后几行应该是下面这样:

# add the MathFunctions libraryadd_subdirectory(MathFunctions)# add the executableadd_executable(Tutorial tutorial.cxx)target_link_libraries(Tutorial PUBLIC MathFunctions)# add the binary tree toe the search path for include files# so that we will find TutorialConfig.htarget_include_directories(Tutorial PUBLIC

"${PROJECT_BINARY_DIR}"

"${PROJECT_SOURCE_DIR}/MathFunctions"

)

现在我们可以将MathFunctions库作为可选项。尽管对于我们的教程而言,这样做毫无必要,但是在大项目中,这是常见的情况。第一步是在顶层的CMakeLists.txt中添加option选项。

option(USE_MYMATH "Use tutorial provided math implementation" ON)# configure a header file to pass some of the CMake settings# to the source codeconfigure_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 executableadd_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.htarget_include_directories(Tutorial PUBLIC

"${PROJECT_BINARY_DIR}"

${EXTRA_INCLUDES}

)

注意我们使用了变量EXTRA_LIBS来收集需要连接到运行程序的可选库。变量EXTRA_INCLUDES用来收集可选的头文件。这是处理很多可选控件的典型做法,我们会在后面介绍更现代的处理方法。

对于源代码的处理更为直接。首先,在tutorial.cxx文件中,只在需要包含MathFunction.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_MATH,我们可以将其添加到TutorialConfig.h.in中:

#cmakedefine USE_MATH练习:为什么设置TutorialConfig.h.in要在定义USE_MYMATH选项之后?如果我们颠倒这两者会发生什么?

运行cmake或者使用cmake_gui,然后构建出运行程序并且运行。

然后,尝试改变USE_MYMATH的值,最方便的方法是使用cmake_gui或者ccmake。如果你想通过命令行直接指定,可以用下面的命令:

cmake ../Step2 -DUSE_MYMATH=OFF

重新编译和运行程序。

那个函数提供更好的答案,sqrt 还是 mysqrt ?

第3步,给库添加使用依赖

使用依赖允许给库或者运行文件的链接提供更好的控制。主要命令有:target_compile_definitions()

target_compile_options()

target_include_directories()

target_link_libraries()

我们现在使用更加现代的方法对第二步增加库进行重构。我们首先声明任何链接MathFunctions都需要包含当前的源代码目录,然而MathFunctions本身不需要。所以这就变成了一个INTERFACE使用依赖。

记住INTERFACE意味着消费者需要但是生产者不需要。在MathFunctions/CMakeLists.txt文件末尾增加如下的行:

target_include_directories(MathFunctions

INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}

)

现在我们已经为MathFunctions指定了使用依赖,我们可以在顶层的CMakeLists.txt中移除掉EXTRA_INCLUDES变量,如下:

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

还有:

target_include_directories(Tutorial PUBLIC

"${PROJECT_BINARY_DIR}"

)

完成上面的步骤后,运行cmake执行程序或者cmake-gui配置项目,然后使用选择的编译工具进行编译,或者在build目录中执行cmake --build .。

第4步,安装和测试

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

安装规则

安装规则非常简单:对于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或者cmake-gui来配置工程,使用编译工具进行编译。

接着运行cmake命令中的install选项执行安装步骤(在3.15版本中引入,更老的版本需要使用make install)。对于多配置的工具,不要忘记使用--config参数来指定配置。如果使用IDE,只需要编译INSTALL目标。这一步会安装合适的头文件,库以及执行文件。例如:

cmake --install .

CMake变量CMAKE_INSTALL_PREFIX用来制定这些文件会被安装的根目录。如果使用cmake --install命令,安装的prefix可以通过--prefix参数覆盖。例如:

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

导航到安装目录并且验证安装的Tutorial正常运行。

测试支持

接下来测试应用。在顶层的CMakeLists.txt文件最后,我们可以启用测试,然后增加基础测试来验证应用程序是否正常工作。

enable_testing()# does the application runadd_test(NAME Runs COMMAND Tutorial 25)# does the usage message workadd_test(NAME Usage COMMAND Tutorial)set_tests_properties(Usage

PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"

)#define a function to simplify adding testsfunction(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 testsdo_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命令:ctest -N 和 ctest -VV。对于多个配置的生成器,配置类型必须指定。为了在调试模式下测试,例如,在编译目录(注意不是Debug目录)中调用ctest -C Debug -VV,或者在IDE中编译RUN_TEST目标。

第5步,添加系统自测

现在为我们的项目增加一些依赖某些特性的代码,但是某些不妙平台可能不支持这些特性。对于这个例子,我们会考虑增加一些依赖目标平台是否支持log或者exp函数的代码。当然,几乎对于所有平台而言这些函数都会支持,但是我们这里假设一下。

如果目标平台包含log 和 exp,我们会使用在mysqrt代码中调用这两个函数来计算平方根。我们首先在顶层的 CMakeLists.txt 文件中使用 CheckSymbolExists 模块确认这两个函数是否可以调用。在某些平台,我们会需要链接到m库。如果 log 和 exp 找不到,那我们会使用m 库来代替。

我们会在TutorialConfig.h中使用新的定义,所以首先确保设置对。

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中使用这些:

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

如果系统可以访问到log和exp,我们可以在mysqrt函数中使用这两个函数来计算平方根。将下面的代码加入到MathFunctions/mysqrt.cxx中的mysqrt函数(不要忘记在返回结果之前加上#endif)。

```cpp

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`库。

```cpp

#include

运行cmake执行文件或者cmake-gui来配置工程,编译之后运行Tutorial执行文件。

你会注意到我们并没有使用log和exp,即使我们认为他们可以使用的时候。我们应该快速意识到,我们忘记了在mysqrt.cxx中包含TutorialConfig.h文件了。

我们同时也需要更新MathFunctions/CMakeLists.txt文件,这样mysqrt.cxx就会知道这些文件的位置了。

target_include_directories(MathFunctions

INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}

PRIVATE ${CMAKE_BINARY_DIR}

)

更新完这些之后,重新编译工程,如果log和exp还是没有调用了,可以查看生成的TutorialConfig.h文件,获取所在的平台真得不支持这两个函数。

那个函数提供了更好的结果,sqrt还是mysqrt?

指定编译定义

有没有不在TutorialConfig.h文件中存储HAVE_LOG和HAVE_EXP,而在其他地方存储的方法?我们可以尝试使用target_compile_definitions()。

首先,移除TutorialConfig.h.in中的定义,我们不需要在mysqrt.cxx中包含TutorialConfig.h文件,也不需要在MathFunctions/CMakeLists.txt中包含额外的内容。

接下来,我们可以将对HAVE_LOG和HAVE_EXP移动到MathFunctions/CMakeLists.txt中,然后将这些值指定为PRIVATE编译定义。

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()# add compile definitionsif(HAVE_LOG AND HAVE_EXP) target_compile_definitions(MathFunctions

PRIVATE "HAVE_LOG" "HAVE_EXP")endif()

改好这些之后,重新之前编译的步骤即可。

第6步,添加自定义命令和生成文件

假设,对于教程这个文件而言,我们决定不使用平台提供的log和exp函数,而是在mysqrt函数中使用一个预先计算好的表。在这一节中,我们会在编译过程中创建这个表,然后将这个表编译到我们的程序中。

首先,让我们移除掉MathFunctions/CMakeLists.txt中关于log和exp的检查。然后移除掉mysqrt.cxx中关于HAVE_LOG

和 HAVE_EXP 的检查。同时,也移除掉 #include 。

在 MathFunctions 子目录中,创建一个新的源文件 MakeTable.cxx 用来生产表。

看完这个文件之后,我们可以看到这个表作为合法的C++代码,输出文件名作为参数进行传递。

接下来需要在 MathFunctions/CMakeLists.txt中加入合适的命令来编译MathTable成可执行文件,然后在整个构建过程中调用这个文件。需要额外加入一些命令来达到这个目的。

首先,在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

)

我们也需要将现在的二进制目录加入到包含目录列表中,这样 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(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。最后,将mysqrt.cxx包含Table.h 来生成MathFunctions库。

运行Tutorial文件来证实它使用了表。

第7步,构建安装器

接下来假设我们想要分发我们的工程让其他人也可以使用。我们想要将二进制和源文件提供到平台。这和我们在第四步,安装和测试有些不同,我们是从源代码构建并安装二进制文件。在这个例子中,我们会构建安装包来支持二级制安装和包管理特性。为了达到这个目的,我们会使用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变量来记录存储证书的位置以及项目的版本号。

最后我们包含 CPack module ,它会用到这些变量和一些当前平台的其他属性来设置安装器。

接下来是用通常的方式来构建项目,最后运行cpack命令。想要构建一个二进制发行文件,在二进制目录运行:

cpack

指定generator,使用-G选项。对于多配置的构建,使用-C来指定配置。例如:

cpack -G ZIP -C Debug

为了创建一个源代码发行文件,使用下面的命令:

cpack --config CPackSourceConfig.cmake

或者,运行make package或者在IDE中右键Pakcage选项和Build Project。

运行二进制目中的安装器。然后运行安装后的可执行文件,验证其是否正常工作。

第8步,添加仪表盘支持

添加将我们的测试结果提交给仪表板的支持是非常容易的。我们已经在测试支持部分添加了一系列的测试。现在,我们只需要运行这些测试,并将它们提交到仪表盘即可。为了包含仪表盘的支持,在顶层的CMakeLists.txt中添加CTest模块。

替换下面的内容:

# enable testingenable_testing()

改成:

# enable dashboard scriptinginclude(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.dash.org")set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")set(CTEST_DROP_SITE_CDASH TRUE)

ctest执行文件在运行的时候会读取这个文件。为了创建一个简单的仪表盘,可以运行cmake或者cmake-gui来配置工程,但是先不要构建。而是将目录切换到二进制树,然后运行:

ctest [-VV] -D Experimental

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

ctest [-VV] -C Debug -D Experimental

或者,在IDE中手动构建Experimental目标。

第9步,混合静态库和共享库

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

为了完成这个任务,我们需要在顶层的CMakeLists.txt中加入BUILD_SHARED_LIBS变量。我们使用option()命令,这个命令允许用户可以设置这个值为ON或者OFF。

接下来,我们将会重构MathFunctions成为一个真正的库,将使用mysqrt还是sqrt进行封装,而不是通过直接调用代码来实现。这也意味着USE_MYMATH不来用来控制构建MathFunctions,而是用来控制库的行为。

第一步是更新顶层的CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.10)# set the project name and versionproject(Tutorial VERSION 1.0)# specify the C++ standardset(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 executableset(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 onlyconfigure_file(TutorialConfig.h.in TutorialConfig.h)# add the MathFunction libraryadd_subdirectory(MathFunctions)# add the executableadd_executable(Tutorial tutorial.cxx)target_link_libraries(Tutorial PUBLIC MathFunctions)

现在,我们已经使得MathFunctions一定会被用到,我们也需要更新库的逻辑。因此,在MathFunctions/CMakeLists.txt中,我们需要创建一个SqrtLibrary来根据USE_MATH的开关进行选择性编译。现在按照这个教程,我们会让SqrtLibrary构建成一个静态库。

MathFunctions/CMakeLists.txt应该是:

# add the library that runsadd_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 functionsoption(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 library dir to find Table.h target_include_directories(SqrtLibrary PRIVATE

${CMAKE_CURRENT_BINARY_DIR}

) target_link_libraries(MathFunctions PRIVATE SqrtLibary)endif()# define the symbol stating we are using the declspec(dllexport) when# building on windowstarget_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")# install rulesset(installable_libs MathFunctions)if(TARGET SqrtLibrary) list(APPEND installable_libs SqrtLibrary)endif()install(TARGET ${installable_libs} DESTINATION lib)install(FILES MathFunctions.h DESTINATION include)

接下来,更新MathFunctions/mysqrt.cxx文件使用mathfunction和detail命名空间。

#include

#include "MathFunction.h"

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

namespace mathfunctions {

namespace detail {

// a hack square root calculation using simple operationsdouble 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(x)];

}

// do ten iteration 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:一定要包含MathFunction.h

一定要使用mathfunction::sqrt

不需要包含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。

# state that SqrtLibrary need PIC when the default is shared librariesset_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,COMPILE_DEFINITIONS等等。它们也可以被用在产生这些属性的命令中,例如target_link_libraries(), target_include_directories(), target_compile_definitions()等等。

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

有不同类型的生成器表达式,包括基于逻辑的,包含信息的以及输出表达式。

逻辑表达式用来创建条件化的输出。基础表达式是0和1表达式。${0:...}代表空字符串,${1:...}代表字符串...。这些可以互相嵌套。

生成器表达式最常见的用法是根据条件来添加编译器标志,例如语言等级或者警告等。比较好的模式是将这个信息和INTERFACE目标进行关联,来传递这些信息。让我们从构建INTERFACE目标开始,指定C++标准为11替代之前的CMAKE_CXX_STANDARD。

下面的代码可以替换成:

旧代码

# specify the C++ standardset(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 "$")set(msvc_cxx "$")target_compile_options(tutorial_compiler_flags INTERFACE

"$>"

"$>"

)

现在看起来,警告标志已经包含在BUILD_INTERFACE条件中。现在我们提供的安装程序的顾客不会继承我们警告标志。练习:修改MathFunctions/CMakeLists.txt实现所有的目标都会有一个target_link_libraries()来调用tutorial_compiler_flags。

第11步,添加导出配置

在第4步的安装和测试,我们使用CMake添加了安装库和工程头文件的功能。在第7步,构建和安装中,我们添加了将这些信息打包的功能,可以分发给其他人。

接下来是添加一些必要的信息来让其它的CMake工程可以使用我们的工程。

首先是更新我们的install(TARGETS) 命令,不仅指定DESTINATION,也需要使用EXPORT。EXPORT关键字可以生成和安装一个包含代码的CMake,导入所有在install命令中包含的目标。现在让我们对MathFunctions进行一些修改:

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 containspath: "/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions"which is prefixed in the source directory.

CMake想要表达的是在生成导出信息的阶段,它会导出一个路径和现在的机器绑定在一起,但是在其他机器上不会生效。解决这个问题的办法是更新MathFunctions的target_include_directories()来理解它需要不同的INTERFACE位置,当在build目录或者在install/package中,需要将target_include_directories()调用。

target_include_directories(MathFunctions

INTERFACE

$

$

)

当完成这一步之后,我们可以重新运行CMake了。

现在,我们使得CMake已经可以成功打包目标信息,但是我们仍然需要生成一个MathFunctionsConfig.cmake使得CMake的find_package() 命令可以找到我们的工程。让我们在顶层的Config.make.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 exportsconfigure_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 filewrite_basic_package_version_file(

"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"

VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"

COMPATIBILITY AnyNewerVersion

)# install the configuration fileinstall(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可以被其他工程使用,而不需要安装。

第12步,打包Debug和Release

注意:这个例子只对单配置生成器生效,不会对多配置生成器生效(例如Visual Studio)

默认情况下,CMake的模型是一个编译目录下只包含单个配置,为Debug、Release、MinSizeRel和RelWithDebInfo。虽然可以将CPack设置成包含多个构建目录,构成一个包由多个配置组成。

首先,我们想要确保debug和release构建不同名称的执行文件和库。让我们添加d作为debug版本的执行文件和库的后缀。

在顶层的CMakeLists.txt靠近开始的地方设置CMAKE_DEBUG_POSTFIX:

set(CMAKE_DEBUG_POSTFIX d)add_library(tutorial_compiler_flags INTERFACE)

在执行文件上添加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中,设置VERSION和SOVERSION属性:

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

在Step12目录中,创建debug和release子目录。目录结构如下:

- 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("releasef/CPackConfig.cmake")set(CPACK_INSTALL_CMAKE_PROJECTS

"debug;Tutorial;ALL;/"

"release;Tutorial;ALL;"

)

在Step12目录中,运行cpack指定我们的自定义配置文件。

cpack --config MultiCPackConfig.cmake

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐