CMake 教程 Step by Step
本教程涉及的源码可在CMake源码的Help/guide/tutorial目录中找到,每个步骤对应一个以该步骤命名的目录,可以用这些目录作为各个步骤的起始点。
基本起始点 (Step1)
一个最基本的项目:从源文件构建可执行程序。
在Step1
目录中创建一个CMakeLists.txt
文件:
cmake_minimum_required(VERSION 3.10)
# set the project name
project(Tutorial)
# add the executable
add_executable(Tutorial tutorial.cxx)
tutorial.cxx
可在Step1
目录中找到,它实现了计算平方根。
添加项目版本号和配置头文件
首先,修改CMakeLists.txt
文件,设置项目版本号:
cmake_minimum_required(VERSION 3.10)
# set the project name and version
project(Tutorial VERSION 1.0)
然后,设置配置一个头文件,用于接收版本号:
configure_file(TutorialConfig.h.in TutorialConfig.h)
在二进制树(binary tree)中将会生成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特性。
修改atof
为std::stod
,同时移除#include <cstdlib>
这一行:
const double inputValue = std::stod(argv[1]);
在CMake中,使用CMAKE_CXX_STANDARD
变量来指定C++版本。对于本例,设置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)
构建、执行
执行下列命令构建项目:
mkdir Step1_build
cd Step1_build
cmake ../Step1
cmake --build .
执行程序:
$ ./Tutorial 300
The square root of 300 is 17.3205
$ ./Tutorial
./Tutorial Version 1.0
Usage: ./Tutorial number
添加库 (Step2)
现在要给项目添加库(library)。在库中实现平方根函数,可执行程序使用库中的平方根函数替代标准库中的。
我们把库的实现放在MathFunctions
子目录中,包括一个头文件MathFunctions.h
和一个源文件mysqrt.cxx
。
在MathFunctions
目录中添加CMakeLists.txt
文件:
add_library(MathFunctions mysqrt.cxx)
为了使MathFunctions
中的CMakeLists.txt
在构建的时候被执行,需要在顶层的CMakeLists.txt
中调用add_subdirectory
添加库到可执行程序,添加MathFunctions
目录到头文件搜索路径。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"
)
现在,我们把MyFunctions
库设置为可选的。对于本教程这么简单的例子,其实没有必要这样做,但对于一个很大的项目,经常会这么做。第一步是在顶层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 code
configure_file(TutorialConfig.h.in TutorialConfig.h)
这个选项会在CMake GUI 和 ccmake显示,默认值是ON。用户设置的值会保存在缓存(cache)中,再次运行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
运行以下命令编译可执行程序:
mkdir Step1_build
cd Step1_build
cmake -DUSE_MYMATH=ON ../Step1
cmake --build .
watermark: this document is translated by xianchen.peng
为库添加使用要求 (Step3)
使用要求库对库和可执行程序的连接、包含命令行提供了更好的控制,也使CMake内传递目标属性更加可控。会影响到使用要求的主要命令有:
target_compile_definitions
target_compile_options
target_include_directories
target_link_libraries
让我们通过使用要求来重构Step2
中的代码。首先要说明一下,任何使用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)
## list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions") ## 删除这行
endif()
和这里:
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
## ${EXTRA_INCLUDES} ## 删除这行
)
做完了上面的事情之后,就可以执行cmake配置、构建项目啦。
安装和测试 (Step4)
现在,我们将向项目中添加安装规则和测试支持。
安装规则
对于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 --install
来执行安装,将会安装合适的头文件、库、可执行文件。
测试支持
现在,让我们测试应用程序。在顶层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
调用通过参数来指定测试的程序名称、输入、和期望输出的结果,每个do_test
调用都会往项目中添加测试。
重新构建项目,切换到二进制目录,然后运行ctest -N
和ctest -vv
。
添加系统内省 (Step5)
让我们在源码中添加一些目标平台可能不支持的特性。下面的这个例子,我们将会根据目标平台是否支持log
和exp
函数来选择是否包含一些代码到项目中。虽然几乎所有的平台都支持这两个函数,我们还是假设一下某个特殊的平台没有这两个函数。
如果平台支持log
和exp
函数,我们将在mysqrt
函数中使用它们来计算平方根。我们首先在顶层CMakeLists.txt
中使用CheckSymboExists
模块来测试函数的可用性。
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
添加定义:
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
修改mysqrt.cxx
使其包含cmath
,然后还是在这个文件中,我们根据平台是否支持log
和exp
函数来提供一个替代的实现,使用下面的代码(不要忘了在最终返回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配置、构建程序,然后运行程序。你将会发现程序没有使用log
和exp
,即使平台实际上支持这两个函数。很快,我们意识到我们忘了在msqrt.cxx
中包含TutorialConfig.h
。
我们还需要更新MathFunctions/CMakeLists.txt
,使得mysqrt.cxx
知道TutorialConfig.h
这个文件的位置:
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_definitions
。
首先,删除TutorialConfig.h.in
中的HAVE_LOG
和HAVE_EXP
定义,不再需要在mysqrt.cxx
中包含TutorialConfig.h
了,也不需要在MathFunctions/CMakeLists.txt
中指定该头文件的路径了。
然后,移动顶层CMakeLists.txt
中检查HAVE_LOG
和HAVE_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()
在做了这些修改之后,重新构建项目。运行程序,发现结果与前面的相同。
添加自定义命令、生成文件 (Step6)
假如我们根本就不想使用平台自带的log
和exp
函数,而是想通过预先生成的一个数据表来计算平方根。在这里,我们将在构建的过程中生成这个表格,然后把这个表格编译到最终的程序中。
首先,让我们移除MathFunctions/CMakeLists.txt
中检查log
和exp
的部分,然后移除mysqrt.cxx
中检查HAVE_LOG
和HAVE_EXP
宏的代码,同时,也移除mysqrt.cxx
中#include <cmath>
。
在MathFunctions
目录中,提供了一个用来生成该表格的源码文件MakeTable.cxx
。
通过分析该源代码,可以发现该源码用于生成数据表,输出的文件名称通过命令行参数来指定。
下一步,让我们在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
,通过在MathFunctions的源文件列表中添加Table.h
来实现。
add_library(MathFunctions
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
我们还需要把当前二进制目录添加到包含路径中,使得Table.h
可以被搜索到。
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
)
现在,我们在mysqrt.cxx
中使用Table.h
。首先,在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重新配置、构建项目。
当项目构建时,首先会构建MakeTable
程序,然后运行MakeTable
生成Table.h
,最后编译mysqrt.cxx
生成MathFunctions库。
好了,请运行Tutorial程序,验证一下它是否是使用数据表来计算平方根的。
生成一个安装器 (Step7)
假如我们想把项目发布给其他人使用,我们想在多个不同平台同时提供二进制和源码包,这和我们在“Step4:安装和测试”中所做的事情不太一样。在这个例子中,我们将构建二进制安装包,支持包管理。我们将使用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)
以上就是所有需要做的事情,现在解释一下各行的含义。最开始的include(InstallRequiredSystemLibraries)
,这个模块将会把项目在当前平台上需要的运行时库包含进来。然后我们设置了一些CPack变量:在哪里保存License文件以及项目版本号。请预先将License.txt文件放置到项目的顶层目录中,版本号的值之前的步骤中我们已经设置过了。最后,我们包含CPack模块,这个模块将会使用我们设置的CPack变量以及其他一些当前系统的属性来生成一个安装器。
下一步就是构建项目,然后运行cpack生成给一个二进制发布包:
cpack
如果想指定生成安装包的格式,使用-G选项。对于有多种配置的构建,使用-C来指定打包哪一种构建。例如:
cpack -G ZIP -C Debug
如果想创建一个源码发布包,输入:
cpack --config CPackSourceConfig.cmake
运行生成的安装器,运行安装好的应用程序来验证是否安装成功。
添加Dashboard支持 (Step8)
我们在Step4中定义了一些测试,现在我们需要运行这些测试并且把结果提交到dashboard。为了支持dashboards我们需要在顶层CMakeLists.txt
中添加CTest模块。
替换:
# enable testing
enable_testing()
为:
# enable dashboard scripting
include(CTest)
CTest模块会自动调用enable_testing()
,所以我们可以从CMakeLists文件中删除它。
我们还需要在顶层目录中创建一个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配置项目,但不要构建它。然后在二进制目录中,运行:
ctest [-VV] -D Experimental
对于多配置的生成器,配置类型必须指定:
ctest [-VV] -C Debug -D Experimental
CTest将会构建并且测试项目,然后提交测试结果到Kitware公共dashboard。测试结果将被提交到Kitware’s公共dashboard的这个地址:https://my.cdash.org/index.php?project=CMakeTutorial.
融合动态库和静态库 (Step9)
在这一节将介绍如何使用BUILD_SHARED_LIBS
变量来控制add_library
的行为和未指明类型的library如何构建。
我们需要在顶层CMakeLists.txt
中添加BUILD_SHARED_LIBS
,并且使用option
命令,这样用户能够选择是否启用BUILD_SHARED_LIBS
。
然后我们修改MathFunctions,使它变成一个封装了使用mysqrt
或sqrt
的库,而不是由调用者来决定使用哪个函数。这意味着USE_MYMATH
将不再用于控制MathFunctions的构建,但仍然会用于控制这个库的行为。
第一步是更改顶层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
中我们需要创建一个根据USE_MYMATH
条件构建的SqrtLibrary库。在本教程中,我们指定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
使用MathFunctions.h
头文件和detail
命名空间:
#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
:
始终包含
MathFunctions.h
始终使用
mathfunctions::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
属性为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)
添加生成器表达式 (Step10)
生成器表达式在构建过程中计算,用来指定构建配置信息。
生成表达式可以用到很多target属性上,比如:LINK_LIBRARIES
、INCLUDE_DIRECTORIES
、COMPILE_DEFINITIONS
等等,他们也可以用在给这些属性添加值的命令上,比如:target_link_libraries()
、target_include_directories()
、target_complie_definitions()
等等。
生成器表达式可以用来进行条件连接、条件宏定义、条件头文件路径等。条件可以基于构建配置、target属性、平台信息、以及很多其他可以查询的信息。
有不同类型的生成器表达式,包括:逻辑表达式、信息表达式、输出表达式。
逻辑表达式用来创建条件输出,最基本的表达式是 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
生成器表达式来控制哪些标志会添加到给定的语言和编译器:
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
表达式里面。这样做可以使得安装我们项目的消费者不会继承这些警告标志。
添加导出配置 (Step11)
在Step4我们为项目添加了安装库和头文件的能力,在Step7我们为项目添加了发布部署包的能力。
下一步,我们将添加一些必要信息使得其他CMake项目可以通过构建目录、本地安装或安装包来使用我们的项目。
首先更新install(TARGETS)
命令,不仅指定DESTINATION
,还指定EXPORT
。EXPORT
关键字生成一个CMake文件到安装目录,该CMake文件中的代码从安装目录导入目标。让我们更新MathFunctions/CMakeLists.txt
文件中的instal
命令,导出MathFunctions这个库:
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.
造成这个错误的原因是,在生成导出信息的过程中导出了一个当前机器的路径,这个路径在其他机器上可能是无效的。解决方案是,改变target_include_directories
MathFunctions,使得它明白当在构建目录内使用和通过一个安装包使用时,需要的是不同的INTERFACE
位置。也就是说,需要把target_include_directories
MathFunctions转换为这样:
target_include_directories(MathFunctions
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
更新之后,我们可以再次运行CMake,它不会再报错了。
现在,CMake已经能正确地打包需要的目标信息了,但我们仍然需要生成一个MathFunctionsConfig.cmake
使得CMakefind_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
的时候如何声明项目的外部依赖。
打包Debug和Release (MultiPackage)
缺省情况下,一个构建目录中只包含一个配置:Debug、Release、MinSizeRel、或RelWithDebInfo。
但是可以设置一个项目包含多个配置,可以在一次构建包的过程中打包多个构建目录。
首先需要建立一个目录multi_config
,里面包含了将被打包在一起的所有构建。
然后在multi_config
目录下创建debug
和release
目录。最终的目录布局像这样:
─ multi_config
├── debug
└── release
现在我们需要设置debug和release构建,大致如同下面的:
cd debug
cmake -DCMAKE_BUILD_TYPE=Debug ../../MultiPackage/
cmake --build .
cd ../release
cmake -DCMAKE_BUILD_TYPE=Release ../../MultiPackage/
cmake --build .
cd ..
现在debug和release构建都完成了,我们可以使用一个自定义的MultiCPackConfig.cmake
文件把这两个构建打包到一个发布中:
cpack --config ../../MultiPackage/MultiCPackConfig.cmake