CMake教程
已废弃,请查看新的CMake教程。
翻译自官方教程https://cmake.org/cmake/help/latest/guide/tutorial/index.html
本教程提供了一个步进(step-by-step)的指导,它覆盖了CMake能够解决的常见构建系统的问题。在一个示例项目中查看各个主题如何协同工作非常有帮助。该教程中所有案例的文件和源代码可以在Help/guide/tutorial
中找到(GitHub链接:https://github.com/Kitware/CMake/tree/master/Help/guide/tutorial)。每一步都有独立的子目录,其中包含了可能会用到的代码。教程示例是渐进式的,因此每个步骤都为上一步提供了完整的解决方案。
文章目录
第1步 开始
最基本的项目是将源代码文件构建为可执行文件。对于一个简单的项目,只需要一个三行的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
文件以设置版本号:
cmake_minimum_required(VERSION 3.10)
# 设置项目名和版本号
project(Tutorial VERSION 1.0)
然后,配置头文件以将版本号传递给源代码:
configure_file(TutorialConfig.h.in TutorialConfig.h)
由于配置的文件将写入binary tree目录下(CMakeCache.txt
所在的目录),所以我们需要将该目录添加到include搜索路径列表中。将以下行添加到CMakeLists.txt
文件的末尾:
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
在源代码目录(Step1
目录)中创建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) {
// 打印版本号
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}
指定C++标准
接下来,我们将在tutorial.cxx
中用std::stod
替换atof
,并将一些C++ 11特性添加到我们的项目中。同时,删除#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_minimum_required(VERSION 3.10)
# 设置项目名和版本号
project(Tutorial VERSION 1.0)
# 指定c++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
构建和测试
运行cmake或cmake-gui来配置(config)项目,然后使用你所选的生成工具生成(build)它。
例如,在命令行中,导航到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
相似的功能。
创建并增加下面这行内容到MathFunctions
目录中的CMakeLists.txt
文件中
add_library(MathFunctions mysqrt.cxx)
要利用这个库,我们需要增加一个add_subdirectory
命令在根目录(Step2
目录)下的CMakeLists.txt
文件中,以便该库可以被构建。增加这个新的库作为可执行目标,然后添加MathFunctions
目录作为include目录以便mqsqrt.h
头文件能被找到。根目录下CMakeLists.txt
文件的最后几行现在应该如下所示:
# add the MathFunctions library
add_subdirectory(MathFunctions)
# add the executable
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)
现在让我们将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,用户可以更改该值。此设置将存储在缓存中,以便用户不必每次在build目录上运行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)
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
${EXTRA_INCLUDES}
)
注意:变量EXTRA_LIBS
收集任何可选库,以便之后链接到可执行文件中。变量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
运行cmake或cmake-gui去配置并构建项目,然后运行Tutorial.exe
使用ccmake或CMake GUI去更新USE_MYMATH
的值。重新构建并运行。sqrt和mysqrt哪个的结果更准确?
第3步 添加库的使用需求
使用需求(Usage requirements)让我们对库或可执行文件的链接和include行进行更好的控制,同时也让我们可以对CMake中目标的可传递属性进行更多的控制。使用需求的主要命令是:
- target_compile_definitions
- target_compile_options
- target_include_directories
- target_link_libraries
现在使用现代的CMake方式(使用需求)重构Step2。首先声明任何链接到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}"
)
完成这些操作之后,运行cmake或cmake-gui来配置项目,然后从build目录构建项目。
第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
)
这就是创建教程的基本本地安装所需的全部内容。
运行cmake或cmake-gui配置并构建项目。运行安装步骤(install step)通过输入cmake --install .
(3.15中引入, 旧版的CMake必须使用make install
)在命令行上,或者构建INSTALL
目标在IDE中(Visual Studio IDE中会出现一个INSTALL
项目)。这将安装头文件、库和可执行文件
Windows系统上会出现问题,原因是权限不够,可以用管理员身份打开Visual Studio IDE,然后再构建项目即可
CMake变量CMAKE_INSTALL_PREFIX
用于检测被安装的根目录的位置。如果使用cmake --install
,可以通过--prefix
参数指定一个自定义的安装目录。
测试支持(Testing Support)
接下来让我们测试我们的应用程序。在根目录下的CMakeLists.txt
文件中启用测试,然后添加一些基本测试去验证应用程序是否正确工作。
enable_testing()
# does the application run
add_test(NAME Runs COMMAND Tutorial 25)
# does the usage message work?
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
)
# define a function to simplify adding tests
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endfunction(do_test)
# do a bunch of result based tests
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is [-nan|nan|0]")
do_test(Tutorial 0.0001 "0.0001 is 0.01")
第一个测试只验证了程序的运行,是否崩溃,是否返回0值。这是一个CTest测试的基本形式。
下一个测试利用PASS_REGULAR_EXPRESSION
测试属性验证输出是否包含某些字符串。在这个情况下,验证当参数数量错误时是否打印了用法信息。
最后,定义了一个do_test
的函数,他运行这个程序并且验证给定输入的平方根是否正确。每一次do_test
的调用都会使用名字、输入和预期的结果增加一个测试。
重新构建这个程序然后进入binary directory(就是build目录),运行ctest -N
和ctest -VV
。对于多配置的生成器(例如Visual Studio IDE),必须指定配置类型。例如,要在调试模式下运行测试,在build目录下使用ctest -C Debug -vv
(不是Debug目录),或者,从IDE构建RUN_TESTS
。
第5步 增加系统自省(System Introspection)
考虑增加一些目标平台不具备特性的代码(依赖于平台的代码)到我们的项目中。例如,我们将增加一些代码,他们依赖或者不依赖目标平台是否有log
和exp
函数。当然,几乎所有平台都有这些函数,但在这个教程中假定某些平台没有。
如果目标平台有log
和exp
函数,那么我们将使用他们在mysqrt
函数中计算平方根。首先在根目录下的CMakeLists.txt
文件中使用CheckSymbolExists
模块来测试这些函数是否可用。
include(CheckSymbolExists)
set(CMAKE_REQUIRED_LIBRARIES "m")
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)
然后增加以下定义到TutorialConfig.h.in
以便我们能在mysqrt.cxx
中使用他们:
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
修改mysqrt.cxx
包含cmath。然后在mysqrt
函数相同的文件下提供一个log
和exp
的代替实现:
#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配置并构建项目,然后运行。
你会注意到我们不能使用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
文件,也许他在当前系统不可用?
指定编译器定义
是否有比TutorialConfig.h
中更好的地方来保存HAVE_LOG
和HAVE_EXP
的值?让我们尝试使用target_compile_defintions
。
首先,从TutorialConfig.h.in
中移除定义。在mysqrt.cxx
或者MathFunctions/CMakeLists.txt
中不需要再包含TutorialConfig.h
。
在MathFunctions/CMakeLists.txt
中移除对HAVE_LOG
和HAVE_EXP
的检查,然后将这些值添加到编译器定义。
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()
继续构建并运行该项目。
第6步 增加自定义命令和生成文件
假设我们决定我们不使用平台上的log
和exp
函数,而是在mysqrt
函数中使用预先计算好的一张表。本节将创建这张表作为build过程的一部分,然后将这张表编译进我们的程序。
首先,在MathFunctions/CMakeLists.txt
中移除log
和exp
函数的检查,然后从mysqrt.cxx
中移除``HAVE_LOG和
HAVE_EXP的检查。同时还可以移除
#include `。
在子目录MathFunctions
中,创建一个MakeTable.cxx
文件去生成这张表。
检查完这些文件后,我们可以发现这张表通过C++代码产生并输出,输出的文件名被作为参数传递。
下一步是给MathFunctions/CMakeLists.txt
添加合适的命令以build可执行文件MakeTable,并且作为build过程的一部分去运行它。需要一些命令去完成这个任务。
首先,在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
include。
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
)
使用被生成的表。首先,修改mysqrt.cxx
以includeTable.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.h
的mysqrt.cxx
将被编译成MathFunctions库。
运行并检验结果。
第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
模块将include在当前平台需要的所有运行时库。然后设置CPack变量去表达我们想存储此项目的许可证和版本信息的位置。版本信息在之前就被设置过了,license.txt
已经被include到根目录下的源代码目录下了。
最后我们include了CPack模块,他将使用这些变量和一些当前系统的属性去建立一个安装器。
下一步是build这个项目然后运行CPack。要创建一个二进制版本,在binary目录运行:
cpack
使用-G
选项指定生成器。对于多配置build(Visual Studio IDE),使用-C
选项去指定配置,例如:
cpack -G ZIP -C Debug
要创建一个源代码版本,输入:
cpack --config CPackSourceConfig.cmake
你也可以运行make package
或在IDE中buildPackage
目标达到同样的目的。
运行在binary目录下找到的安装程序,然后运行并验证他的工作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wm0y4EK4-1581420966770)(C:\Users\cc\Documents\work_spaces\markdown_work\CMake\生成的zip.png)]
ck变量去表达我们想存储此项目的许可证和版本信息的位置。版本信息在之前就被设置过了,license.txt
已经被include到根目录下的源代码目录下了。
最后我们include了CPack模块,他将使用这些变量和一些当前系统的属性去建立一个安装器。
下一步是build这个项目然后运行CPack。要创建一个二进制版本,在binary目录运行:
cpack
使用-G
选项指定生成器。对于多配置build(Visual Studio IDE),使用-C
选项去指定配置,例如:
cpack -G ZIP -C Debug
要创建一个源代码版本,输入:
cpack --config CPackSourceConfig.cmake
你也可以运行make package
或在IDE中buildPackage
目标达到同样的目的。
运行在binary目录下找到的安装程序,然后运行并验证他的工作。