CMake 官方完整版
- 一些概念
- Target
- Signature
- 1. A Basic Starting Point(Step 1)
- Adding a Version Number and Configured Header File
- Specify the C++ Standard
- Build and Test
- 2. Adding a Library(Step 2)-添加一个库
- Adding an Option
- 3. Adding Usage Requirements for Library(Step 3)-为库添加使用需求
- Setting the C++ Standard with Interface Libraries
- 4. Adding Generator Expressions (Step 4)-添加生成器表达式
- 5. Installing and Testing(Step 5)-安装和测试
- Install Rules
- Testing Support
- CTest 运行方式
- ctest -N
- ctest -VV
- 6. Adding Support for a Testing Dashboard(Step 6)-添加测试仪表盘支持
- 7. Adding System Introspection(Step 7)-添加系统自省
- 8. Adding a Custom Command and Generated File(Step 8)-增加自定义命令并生成文件
- 9. Building an Installer(Step 9)-构建一个安装程序
- 10. Selecting Static or Shared Libraries(Step 10)-混合静态库和动态库
- 11. Adding Export Configuration (Step 11)-添加导出配置
- 12. Packaging Debug and Release (Step 12)-打包调试和发布
Reference:
- CMake Tutorial-v3.20
- CMake教程
- CMake Tutorial-v3.27
相关文章:
- CMakeLists Option使用简介
CMake 教程提供了一个循序渐进的指南,涵盖了 CMake 帮助解决的常见构建系统问题。了解不同的主题如何在示例项目中一起工作是非常有帮助的。教程文档和示例源代码可以在 以下路径 找到。每个步骤都有自己的子目录,其中包含了可能会用到的代码。教程示例是渐进式的,因此每个步骤都为上一步提供了完整的解决方案。
一些概念
Target
在 CMake 中,target
是一个重要的概念,用于表示项目中的各种构建目标,例如可执行程序、静态库、共享库等。以下是常见的 CMake target 类型:
- 可执行程序(Executable Targets):
add_executable(target_name source_files...)
用于定义一个可执行程序目标。- 示例:
add_executable(my_app main.cpp)
- 静态库(Static Library Targets):
add_library(target_name STATIC source_files...)
用于定义一个静态库目标。- 示例:
add_library(my_static_lib STATIC lib.cpp)
- 共享库(Shared Library Targets):
add_library(target_name SHARED source_files...)
用于定义一个共享库目标。- 示例:
add_library(my_shared_lib SHARED lib.cpp)
- 模块库(Module Library Targets):
add_library(target_name MODULE source_files...)
用于定义一个模块库目标,通常用于插件和动态加载的场景。- 示例:
add_library(my_module MODULE plugin.cpp)
- 接口库(Interface Library Targets):
add_library(target_name INTERFACE)
用于定义一个接口库目标,它通常不包含实际的源文件,主要用于传递编译器和链接器的设置。- 示例:
add_library(my_interface_lib INTERFACE)
- 自定义目标(Custom Targets):
- 你可以使用
add_custom_target
来定义自定义的构建目标,这些目标可以执行自定义的构建命令。 - 示例:
add_custom_target(my_custom_target COMMAND echo "Custom Target")
- 你可以使用
- 自定义命令(Custom Commands):
- 使用
add_custom_command
可以定义自定义的构建命令,这些命令可以与目标关联。 - 示例:
add_custom_command(TARGET my_target POST_BUILD COMMAND my_command)
- 使用
- 导入目标(Imported Targets):
- 用于导入外部项目或库的目标,通常由
find_package
或find_library
等命令生成。 - 示例:
find_package(Boost REQUIRED)
- 用于导入外部项目或库的目标,通常由
Signature
在 CMake 中,签名(Signature)
指的是 CMake 命令的参数及其类型和顺序。CMake 命令的签名定义了如何正确使用该命令以及应该提供哪些参数。每个 CMake 命令都有一个特定的签名,开发者必须按照该签名的要求提供参数,以便CMake正确地处理它们。
CMake 命令的签名通常包括以下信息:
-
命令名称:命令的名称,例如
add_executable
、add_library
、target_include_directories
等。 -
参数列表:参数的名称和顺序。每个参数都有特定的名称,并且按照特定的顺序提供给命令。
-
参数类型:每个参数的类型,例如
STRING
、LIST
、PATH
等。参数的类型指定了参数应该是一个字符串、列表、路径或其他类型的数据。 -
默认值:有些命令的参数有默认值,如果不提供参数,则将使用默认值。
例如,以下是 add_executable
命令的签名:
add_executable( [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 …])
在这个签名中:
<target>
是一个必须提供的参数,表示要创建的可执行程序的名称。[WIN32]
和[MACOSX_BUNDLE]
是可选参数,用于指定可执行程序的类型。[EXCLUDE_FROM_ALL]
也是可选参数,用于指定是否将目标从所有构建中排除。source1 [source2 ...]
是必须提供的参数,表示可执行程序的源文件列表。
签名的存在是为了帮助开发者正确使用 CMake 命令,以避免错误和不一致性。在 CMake 文档中,你可以找到每个命令的签名及其详细说明,以便了解如何正确使用它们。
1. A Basic Starting Point(Step 1)
最基础的项目是将源代码构建成可执行文件。对于一个简单的项目,只需要一个
3
3
3 行的 CMakeLists.txt
文件即可。在Step1
目录下创建一个 CMakeLists.txt
文件:
# 指明了对cmake的最低版本要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名
project(Tutorial)
# 添加可执行目标
add_executable(Tutorial tutorial.cxx) //每次看到cxx总感觉不太适应
注意:CMake 支持大写、小写或大小写混合命令。但在这些例子中,每个 CMakeLists.txt
文件都使用小写命令。tutorial.cxx
中的源代码放在Step1
目录下,它用于计算一个数的平方根。
Adding a Version Number and Configured Header File
我们将添加的第一个特性是:为我们的可执行文件和项目提供一个版本号。尽管我们可以在源代码中做到这一点,但是使用 CMakeLists.txt
会更加灵活。
首先,修改 CMakeLists.txt
文件,使用 project()
命令设置项目名和版本号:
cmake_minimum_required(VERSION 3.10)
# 设置项目名和版本号
project(Tutorial VERSION 1.0)
然后,configure_file
配置一个头文件并将版本号传递给源代码:
- configure_file:将文件复制到另一个位置并修改其内容。
将input
文件复制到output
文件中,并替换输入文件内容中引用的变量值,如@VAR@
或${VAR}
。
configure_file(TutorialConfig.h.in TutorialConfig.h)
因为配置的文件将被写到 binary tree 目录下(CMakeCache.txt
所在的目录,即 build 目录----${PROJECT_BINARY_DIR}
),所以我们必须将那个目录添加到 include 搜索路径列表中。
- target_include_directories:向目标添加包含目录。
target_include_directories(<target> [SYSTEM] [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
target
是要为其设置包含目录的目标名称。PRIVATE
、PUBLIC
和INTERFACE
关键字用于指定包含目录的可见性和作用范围。PRIVATE
:只有目标本身可以看到这些包含目录。PUBLIC
:目标本身和依赖于该目标的其他目标都可以看到这些包含目录。INTERFACE
:只有依赖于该目标的其他目标可以看到这些包含目录。
将下面这行添加到 CMakeLists.txt
文件的末尾:
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
使用你最喜欢的编辑器,在源代码目录(Step1
目录)中创建TutorialConfig.h.in
,其中包含以下内容:
// 该教程的配置选项和设置
#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;
}
Specify the C++ Standard
接下来,给我们的项目添加一些 C++11 特性,在 tutorial.cxx
中用 std::stod
替换 atof
(不带std的是 C 的,带std的是 C++ 的)。同时,删除 #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_CXX_STANDARD
声明添加到 add_executable
的上面:
cmake_minimum_required(VERSION 3.10)
# 设置项目名和版本号
project(Tutorial VERSION 1.0)
# 指定c++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
Build and Test
运行 cmake
或 cmake-gui
来配置(configure)项目,然后使用你所选的构建工具构建(build)它。
例如,我们可以从命令行导航到 CMake 源代码树的 Help/guide/tutorial 目录,并运行以下命令:
mkdir Step1_build
cd Step1_build
cmake ../Step1
cmake --build .
导航到构建教程的目录(可能是 make 目录或 Debug 或 Release 构建配置子目录)并运行这些命令:
Tutorial 4294967296
Tutorial 10
Tutorial
2. Adding a Library(Step 2)-添加一个库
现在,我们已经看到了如何使用CMake创建一个基本项目。在这一步中,我们将学习如何在项目中创建和使用库。我们还将看到如何使库的使用成为可选的。
要在 CMake 中添加一个库,使用 add_library()
命令并指定哪些源文件应该组成该库。
- add_library:使用指定的源文件向项目添加库。
add_library(MathFunctions mysqrt.cxx)
我们可以使用一个或多个子目录组织项目,而不是将所有源文件放在一个目录中。在本例中,我们将专门为库创建一个子目录。在这里,我们可以添加一个新的 CMakeLists.txt
文件和一个或多个源文件。在顶层的 CMakeLists.txt
文件中,我们将使用 add_subdirectory()
命令将子目录添加到构建中。
创建库之后,使用 target_include_directories()
和 target_link_libraries()
将其连接到可执行目标。
在 MathFunctions
目录下的 CMakeLists.txt
文件中,我们使用 add_library()
创建了一个名为 MathFunctions
的库目标。库的源文件作为参数传递给 add_library()
。这看起来像下面这行:
add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)
为了使用新库,我们将在顶层 CMakeLists.txt
文件中添加 add_subdirectory()
调用,以便库将被构建。
- add_subdirectory:向 build 中添加一个子目录。
source_dir
指定源CMakeLists.txt
和代码文件所在的目录。如果它是一个相对路径,它将相对于当前目录进行评估(典型的用法),但它也可以是一个绝对路径。用于在当前项目的CMakeLists.txt
文件中包含另一个子目录的CMakeLists.txt
文件。
add_subdirectory(MathFunctions)
接下来,使用 target_link_libraries()
将新的库目标链接到可执行目标。
target_link_libraries(Tutorial PUBLIC MathFunctions)
最后,我们需要指定库的头文件位置。修改 target_include_directories()
以添加 MathFunctions
子目录作为包含目录,以便可以找到 MathFunctions.h
头文件。
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)
现在让我们使用库。在 tutorial.cxx
,包含 MathFunctions.h
:
#include "MathFunctions.h"
最后,用库函数 mathfunctions::mysqrt
替换 sqrt
。
const double outputValue = mathfunctions::sqrt(inputValue);
Adding an Option
现在让我们在 MathFunctions
库中添加一个选项,允许开发人员选择自定义的平方根实现或内置的标准实现。虽然对于教程来说,确实没有必要这样做,但对于大型项目来说,这是一种常见的情况。
CMake 可以使用 option()
命令执行此操作。这为用户提供了一个可以在配置 cmake 构建时更改的变量。这个设置将被存储在缓存中,这样用户就不需要每次在构建目录上运行 CMake 时都设置这个值(意思是不需要使用 -D
设置吧)。
第一步是在 MathFunctions/CMakeLists.txt
中添加一个选项。该选项将显示在 cmake-gui
和 ccmake
中,默认值为 ON
,可由用户更改。
option(USE_MYMATH "Use tutorial provided math implementation" ON)
接下来,使用这个新选项使库与 mysqrt
函数的构建和链接成为有条件的。
创建一个 if()
语句来检查 USE_MYMATH
的值。在 if()
块中,放入 target_compile_definitions()
命令和编译定义 USE_MYMATH
。(类似CMakeLists Option使用简介,但链接内使用的是全局的 add_definitions()
)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()
当 USE_MYMATH
为 ON
时,编译定义 USE_MYMATH
将被设置。然后,我们可以使用这个编译定义来启用或禁用源代码的部分。
对源代码的相应更改相当简单。在 MathFunctions.cxx
,我们让 USE_MYMATH
控制使用哪个平方根函数:
#ifdef USE_MYMATH
return detail::mysqrt(x);
#else
return std::sqrt(x);
#endif
接下来,如果定义了 USE_MYMATH
,我们需要包含 mysqrt.h
。
#ifdef USE_MYMATH
# include "mysqrt.h"
#endif
最后,在使用 std::sqrt
时,我们需要包含 cmath
。
#include <cmath>
此时,如果 USE_MYMATH
为 OFF
,mysqrt.cxx
不会被使用,但它仍然会被编译,因为 MathFunctions
目标有 mysqrt.cxx
列在来源下面。
有几种方法可以解决这个问题。第一个选项是使用 target_sources()
来添加 mysqrt
。从 USE_MYMATH
块中取出。另一个选择是在 USE_MYMATH
块中创建一个额外的库,它负责编译 mysqrt.cxx
。在本教程中,我们将创建一个额外的库。
首先,在 USE_MYMATH
中创建一个名为 SqrtLibrary
的库,其源代码为 mysqrt.cxx
。
add_library(SqrtLibrary STATIC
mysqrt.cxx
)
接下来,当启用 USE_MYMATH
时,我们将 SqrtLibrary
链接到 MathFunctions
。
target_link_libraries(MathFunctions PUBLIC SqrtLibrary)
最后,我们可以删除 mysqrt
。从 MathFunctions
库源列表中取出 cxx
,因为它将在包含 SqrtLibrary
时被拉入。
add_library(MathFunctions MathFunctions.cxx)
通过这些更改,mysqrt
函数现在对于正在构建和使用 MathFunctions
库的人来说完全是可选的。用户可以切换 USE_MYMATH
来操作构建中使用的库。
现在使用另一种方式使构建和链接 MathFunctions
库成为一个条件。为此,我们将根目录下 CMakeLists.txt
文件的结尾更改为:
- list:列表操作。
# Use list() and APPEND to create a list of optional libraries
# called EXTRA_LIBS and a list of optional include directories called
# EXTRA_INCLUDES. Add the MathFunctions library and source directory to
# the appropriate lists.
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()
# 添加可执行目标
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
# 添加binary tree目录到include文件的搜索路径
# 以便我们能找到TutorialConfig.h
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
${EXTRA_INCLUDES}
)
注意:使用变量 EXTRA_LIBS
收集所有可选库(list 的作用),以便之后链接到可执行文件中(target_include_directories
)。类似地,变量 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
文件中(这种方法的原理是将 TutorialConfig.h.in
里面的 #cmakedefine USE_MYMATH
,变成 TutorialConfig.h
的 #define USE_MYMATH
。这是种方法但感觉还是不够方便,我更倾向于在前面那种方法,即使用 option 后接 add_definitions
或 target_compile_definitions
):
#cmakedefine USE_MYMATH
运行 cmake
或 cmake-gui
配置并构建项目,然后运行生成的可执行文件。
现在让我们更新USE_MYMATH
的值。最简单的方式是使用 cmake-gui
或 ccmake
,如果你在终端中的话。或者,如果你想通过命令行更改选项的话,你可以:
cmake ../Step2 -DUSE_MYMATH=OFF
然后构建并重新运行。
3. Adding Usage Requirements for Library(Step 3)-为库添加使用需求
目标参数的 使用需求(Usage requirements)
让我们可以对库或可执行文件的链接和 include 行进行更好的控制,同时也让我们对 CMake 中目标的传递属性进行更多的控制。利用使用需求的主要命令有:
- target_compile_definitions()
- target_compile_options()
- target_include_directories()
- target_link_libraries()(这个在3.17咋不写出来了)
- target_link_directories()
- target_link_options()
- target_precompile_headers()
- target_sources()
现在,让我们使用现代的 CMake 方式(通过使用需求)重构 Step 2。首先声明任何链接到 MathFunctions
的都需要包含当前的 source 目录,而 MathFunctions
本身不需要。所以这可以成为一个 INTERFACE
使用需求。(相关内容请参考 CMake 构建系统 https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html)
记住,INTERFACE
意味着这玩意的使用者需要,但库的制作者不需要(看名字就可以知道,这里包含头文件是用来做接口的。所以编译的当前 library 并不需要用到它,而如果想使用这个 library,就需要相应的头文件了)。添加下面这行到 MathFunctions/CMakeLists.txt
文件尾:
#State that anybody linking to MathFunctions needs to include the
# current source directory, while MathFunctions itself doesn't.
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
现在,我们已经指定了 MathFunctions
的使用需求,我们可以从根目录安全的删除 EXTRA_INCLUDES
变量的使用(之前的 list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
被删了):
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
endif()
还有这里:
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
完成这些操作之后,运行 cmake
或 cmake-gui
来配置并构建项目。
(这样写很有意思啊,经验证 target_include_directories(MathFunctions)
内使用 PUBLIC
或 INTERFACE
均可编译通过,而如果使用 PRIVATE
会报错:fatal error: MathFunctions.h: No such file or directory #include "MathFunctions.h"
。不过感觉这种写法只适合在 add_subdirectory
时使用,这里的 target_include_directories
使用 INTERFACE
传递的应该是路径,我如果向其他人提供这种方式编译出的库文件,缺少了接口头文件应该还是会报找不到头文件的错)
代码可以看到代码中还有以下片段:
target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)
注意,使用这种技术,我们的可执行目标使用我们的库所做的唯一事情就是用库目标的名称调用 target_link_libraries()
。在较大的项目中,手动指定库依赖项的经典方法很快就会变得非常复杂。比如像我这样的一长条?
Setting the C++ Standard with Interface Libraries
既然我们已经将代码转换为更现代的方法,那么让我们演示一种为多个目标设置属性的现代技术。让我们重构现有代码以使用 INTERFACE
库。在这里,我们将重构代码,使用 INTERFACE
库指定 C++ 标准。让我们更新上一步的代码,使用接口库来设置 C++ 需求。
首先,我们需要删除变量 CMAKE_CXX_STANDARD
和 CMAKE_CXX_STANDARD_REQUIRED
上的两个 set()
调用。具体要删除的行如下:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
接下来,我们需要创建一个接口库 tutorial_compiler_flags
。然后使用 target_compile_features()
来添加编译器特性 cxx_std_11
。
- add_library:使用指定的源文件向项目添加库。
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [source1] [source2 ...])
- v3.15 的
Interface Libraries
:-
add_library(<name> INTERFACE)
-
创建接口库。
INTERFACE
库目标不编译源代码,也不会在磁盘上生成一个库工件(artifacts)。但是,它可以设置属性,并且可以安装和导出。通常,INTERFACE_*
属性在接口目标上使用以下命令填充:- set_property()
- target_link_libraries(INTERFACE)
- target_link_options(INTERFACE)
- target_include_directories(INTERFACE)
- target_compile_options(INTERFACE)
- target_compile_definitions(INTERFACE)
- target_sources(INTERFACE)
然后像其他目标一样将它用作
target_link_libraries()
的参数。使用上述签名创建的接口库本身没有源文件,也不会作为目标包含在生成的构建系统中。
-
- target_compile_features:向目标添加预期的编译器特性。
target_compile_features(<target> <PRIVATE|PUBLIC|INTERFACE> <feature> [...])
- 指定编译给定目标时所需的编译器特性。如果该特性没有在
CMAKE_C_COMPILE_FEATURES
变量或CMAKE_CXX_COMPILE_FEATURES
变量中列出,则 CMake 将报告错误。如果使用该特性需要额外的编译器标志,例如-std=gnu++11
,则会自动添加该标志。
add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)
最后,在设置好接口库之后,我们需要将可执行的 Target
、MathFunctions
库和 SqrtLibrary
库链接到新的 tutorial_compiler_flags
库。分别,代码看起来像这样:
target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)
这样:
target_link_libraries(SqrtLibrary PUBLIC tutorial_compiler_flags)
还有这样:
target_link_libraries(MathFunctions PUBLIC SqrtLibrary)
尽管如此,我们所有的代码仍然需要 C++ 11 来构建。注意,使用这种方法,我们可以明确哪些目标需要特定的需求。此外,我们在接口库中创建了一个单一的事实来源。
4. Adding Generator Expressions (Step 4)-添加生成器表达式
生成器表达式
(Generator expressions) 在 build 生成系统时被评估,以产生特定于每个构建配置的信息。
生成器表达式在许多目标属性的上下文中是允许的,例如 LINK_LIBRARIES
、INCLUDE_DIRECTORIES
、COMPILE_DEFINITIONS
等。当使用命令来填充这些属性时,它们也可以被使用,例如 target_link_libraries()
、 target_include_directories()
、target_compile_definitions()
等。
生成器表达式可以用于启用条件链接,编译时使用的条件定义,条件 include 目录等等。条件可以基于构建配置、目标属性、平台信息或任何其他可查询的信息。
有几种不同类型的生成器表达式,包括 逻辑表达式
、信息表达式
和 输出表达式
。
逻辑表达式
用于创建条件输出。基本表达式是
0
0
0 和
1
1
1 表达式。$<0:...>
结果为空字符串,而 <1:...>
结果为 ...
的内容。它们也可以被嵌套。
生成器表达式的常用用途是条件性地添加编译器标志,例如语言级别或警告标志。一个好的模式是将这些信息与一个 INTERFACE
目标关联起来,以便这些信息能够传播。
更新 cmake_minimum_required()
以至少要求 CMake 版本 3.15:
cmake_minimum_required(VERSION 3.15)
接下来,我们确定系统当前正在使用哪个编译器进行构建,因为警告标志会根据我们使用的编译器而变化。这是通过 COMPILE_LANG_AND_ID
生成器表达式完成的。我们在变量 gcc_like_cxx
和 msvc_cxx
中设置结果如下:
# 将一个生成表达式赋值给变量 gcc_like_cxx。
# COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC 是一个生成表达式
# 它用于检查当前编译器是否与其中任何一个指定的编译器相关。具体来说,它检查当前编译器是否是C++编译器(CXX)
# 并且是否与 ARMClang、AppleClang、Clang、GNU、LCC中的任何一个匹配
# 如果当前编译器匹配这些条件,那么 gcc_like_cxx 变量的值将被设置为 1(表示真),否则将被设置为 0(表示假)
# Create helper variables to determine which compiler we are using:
# * Create a new variable gcc_like_cxx that is true if we are using CXX and
# any of the following compilers: ARMClang, AppleClang, Clang, GNU, LCC
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
# * Create a new variable msvc_cxx that is true if we are using CXX and MSVC
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
接下来,我们添加项目所需的编译器警告标志(这里说的警告标志是下面的 -Wall
等警告标志)。
- 通用警告标志:
-Wall
:启用所有常规警告。-Wextra
:启用额外的警告。-Werror
:将警告视为错误。-pedantic
:启用更严格的警告。-Wno-<warning>
:禁用特定警告,例如-Wno-unused-variable
可以禁用未使用变量的警告。
- 警告级别:
-W1
、-W2
、-W3
:设置警告级别,例如-W3
表示启用大多数警告。
- 特定警告:
--W<warning>
:启用特定类型的警告,例如-Wunused-variable
启用未使用变量的警告。 - 禁用警告:
-w
或-Wno-*
:禁用所有警告或特定警告。
- target_compile_options():向目标添加编译选项。
target_compile_options(<target> [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
使用变量 gcc_like_cxx
和 msvc_cxx
,我们可以使用另一个生成器表达式,仅在变量为真时应用各自的标志。我们使用 target_compile_options()
将这些标志应用于接口库。
# Add warning flag compile options to the interface library tutorial_compiler_flags.
target_compile_options(tutorial_compiler_flags INTERFACE
"$<${gcc_like_cxx}:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>"
"$<${msvc_cxx}:-W3>"
)
最后,我们只希望在构建期间使用这些警告标志。已安装项目的使用者(Consumers of our installed project) 不应该继承我们的警告标志(警告标志仅是在我们编译库时使用的)。为了指定这一点,我们使用 BUILD_INTERFACE
条件将标志包装在生成器表达式中。生成的完整代码如下所示:
# With nested generator expressions, only use the flags for the build-tree
target_compile_options(tutorial_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
5. Installing and Testing(Step 5)-安装和测试
通常,仅仅构建可执行文件是不够的,它还应该是可安装的。使用 CMake,我们可以使用 install()
命令指定安装规则。在 CMake 中支持本地安装通常就像指定安装位置和要安装的目标和文件一样简单。
现在,我们可以开始给我们的项目添加 安装规则(install rules)
和 测试支持(testing support)
。
Install Rules
安装规则相当简单:对于 MathFunctions
,我们想安装库和头文件;对于应用程序,我们想安装可执行文件和配置好的头文件。
所以在 MathFunctions/CMakeLists.txt
文件的末尾添加:
# Create a variable called installable_libs that is a list of all
# libraries we want to install (e.g. MathFunctions and tutorial_compiler_flags)
# Then install the installable libraries to the lib folder.
set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
# Install the library headers to the include folder.
install(FILES MathFunctions.h DESTINATION include)
并在最上层目录下的 CMakeLists.txt
文件末尾处添加:
# Install Tutorial in the bin directory
install(TARGETS Tutorial DESTINATION bin)
# Install TutorialConfig.h to the include directory
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include
)
这就是创建一个基本的本地安装规则所需的全部内容。
现在,运行 cmake
或 cmake-gui
配置并构建项目。然后,在命令行中使用 cmake
命令的 install
选项(CMake 3.15 中引入,旧版的 CMake 必须使用 make install
等本地的构建命令)。对于多配置的工具,不要忘了使用 --config
参数时指定配置。如果使用一个 IDE,你只需要构建 INSTALL
这个目标。这将安装合适的头文件、库和可执行文件。例如:
cmake --install .
默认情况下,需要以管理员身份运行,否则可能会权限不足。
对于多配置工具,不要忘记使用 --config
参数来指定配置。
cmake --install . --config Release
如果使用 IDE,只需构建 INSTALL 目标。您可以从命令行构建相同的安装目标,如下所示:
cmake --build . --target install --config Debug
CMake 变量 CMAKE_INSTALL_PREFIX
用于决定这些文件被安装到哪里,它是一个安装前缀,即一个目录。如果使用 cmake --install
命令,可以通过 --prefix
参数重写安装前缀。例如:
cmake --install . --prefix "/home/myuser/installdir"
导航到 install 目录,并验证安装的 Tutorial 是否运行。
在运行以上指令后,可以看到在对应路径下的文件如下:
Testing Support
CTest 提供了一种轻松管理项目测试的方法。可以通过 add_test()
命令添加测试。尽管在本教程中没有明确介绍,但 CTest 和其他测试框架(如 GoogleTest)之间有很多兼容性。
接下来让我们测试我们的应用程序。在顶层 CMakeLists.txt
文件的末尾,我们首先需要使用 enable_testing()
命令启用测试。
enable_testing()
启用测试后,我们将添加一些基本测试来验证应用程序是否正常工作。首先,我们使用 add_test()
创建一个测试,它运行教程可执行文件,并传入参数
25
25
25。对于这个测试,我们不打算检查可执行程序的计算答案。此测试将验证应用程序是否运行,没有出现分段故障或以其他方式崩溃,并且返回值为零。这是 CTest 测试的基本形式。
- add_test:将测试添加到要由
ctest(1)
运行的项目中。add_test(NAME <name> COMMAND <command> [<arg>...] [CONFIGURATIONS <config>...] [WORKING_DIRECTORY <dir>] [COMMAND_EXPAND_LISTS])
- 其中第二项是测试名,自己命名的。
# Add a test called Runs which runs the following command:$ Tutorial 25
add_test(NAME Runs COMMAND Tutorial 25)
接下来,让我们使用 PASS_REGULAR_EXPRESSION
测试属性来验证测试的输出是否包含某些字符串。在本例中,验证当提供的参数数量不正确时是否打印 usage 消息。
# Add a test called Usage which runs the following command:$ Tutorial 25
add_test(NAME Usage COMMAND Tutorial)
# Make sure the expected output is displayed.
set_tests_properties(Usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
)
我们将添加的下一个测试验证计算值确实是平方根。
add_test(NAME StandardUse COMMAND Tutorial 4)
set_tests_properties(StandardUse
PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2"
)
这一个测试还不足以让我们确信它对传入的所有值都有效。我们应该增加更多的测试来验证这一点。为了方便地添加更多测试,我们创建了一个名为 do_test
的函数,该函数运行应用程序并验证给定输入的计算平方根是否正确。对于 do_test
的每次调用,另一个带有名称、输入和基于传递的参数的预期结果的测试被添加到项目中。
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 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")
CTest 运行方式
导航到构建目录并重新构建应用程序。然后,运行 ctest 可执行文件:ctest -N
和 ctest -VV
。对于多配置生成器(例如 Visual Studio),必须使用 -C <mode>
标志指定配置类型。例如,要在 Debug 模式下运行测试,请从构建目录(而不是 Debug 子目录!)使用 ctest -C Debug -VV
。Release 模式将从相同的位置执行,但使用 -C Release
。或者,从 IDE 构建 RUN_TESTS
目标。
- CTest:
-N, --show-only[=<format>]
禁用测试的实际执行。
这个选项告诉 CTest 列出将要运行但没有实际运行的测试。与-R
和-E
选项一起使用。-VV, --extra-verbose
从测试中启用更详细的输出。
测试输出通常被抑制,只显示摘要信息。这个选项将显示更多的测试输出。
ctest -N
使用 ctest -N
输出如下:
ctest -VV
使用 ctest -VV
输出如下(删减版):
6. Adding Support for a Testing Dashboard(Step 6)-添加测试仪表盘支持
讲道理这一步作用不是很清楚,不像很有用的样子。
添加将测试结果提交到 dashboard 的支持很简单。我们已经在上一节测试支持中为我们的项目定义了许多测试。现在我们只需要运行这些测试并将它们提交给 CDash。
我们很容易将测试结果提交到一个仪表盘上。我们已经为我们的项目定义了测试编号。现在,我们只需要运行那些测试并将它们提交到一个仪表盘。为了包含对仪表盘的支持,我们在顶层目录下的 CMakeLists.txt
中包含了 CTest
模块。
将:
# enable testing
enable_testing()
替换为:
# enable dashboard scripting
include(CTest)
CTest 模块将自动调用 enable_testing()
,所以我们可以从 CMake 文件中移除它。
我们也需要在顶层目录下创建一个 CTestConfig.cmake
文件,在这个文件中指定项目名和要提交到的仪表盘。
ctest [-VV] -D Experimental
记住,对于多配置的生成器(例如Visual Studio),必须指定配置类型:
ctest [-VV] -C Debug -D Experimental
或者从 IDE 中构建 Experimental 目标。
CTest 将构建和测试该项目,然后提交测试结果到 Kitware 的公共仪表盘。
7. Adding System Introspection(Step 7)-添加系统自省
让我们考虑在我们的项目中添加一些代码,这些代码依赖于目标平台可能没有的特性。对于本例,我们将添加一些代码,这些代码取决于目标平台是否具有 log
和 exp
函数。当然,几乎每个平台都有这些功能,但在本教程中,我们假设它们并不常见。
在这个练习中,我们将使用 CheckCXXSourceCompiles
模块中的函数,所以首先我们必须将它包含在 MathFunctions/CMakeLists.txt
中。
include(CheckCXXSourceCompiles)
然后使用 check_cxx_compiles_source
测试 log
和 exp
的可用性。这个函数允许我们在真正的源代码编译之前尝试编译带有所需依赖项的简单代码。结果变量 HAVE_LOG
和 HAVE_EXP
表示这些依赖项是否可用。
# Use check_cxx_source_compiles with simple C++ code to verify availability of:
# * std::log
# * std::exp
# Store the results in HAVE_LOG and HAVE_EXP respectively.
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
接下来,我们需要将这些 CMake 变量传递给我们的源代码。这样,我们的源代码就可以知道哪些资源是可用的。如果 log
和 exp
都可用,使用 target_compile_definitions()
将 HAVE_LOG
和 HAVE_EXP
指定为 PRIVATE
编译定义。
if(HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary
PRIVATE "HAVE_LOG" "HAVE_EXP"
)
endif()
因为我们可能会使用 log
和 exp
,所以我们需要修改 mysqrt.cxx
包含 cmath
。
#include <cmath>
如果系统上有 log
和 exp
,那么在 mysqrt
函数中使用它们来计算平方根。MathFunctions/mysqrt.cxx
中的 mysqrt
函数看起来如下:
#if defined(HAVE_LOG) && defined(HAVE_EXP)
double result = std::exp(std::log(x) * 0.5);
std::cout << "Computing sqrt of " << x << " to be " << result
<< " using log and exp" << std::endl;
#else
double result = 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;
}
#endif
8. Adding a Custom Command and Generated File(Step 8)-增加自定义命令并生成文件
假设,出于教学的目的,我们决定不使用平台 log
和 exp
函数,而是希望生成一个预先计算值的表,以便在 mysqrt
函数中使用。在本节中,我们将创建表作为构建过程的一部分,然后将该表编译到我们的应用程序中。
首先,让我们删除 MathFunctions/CMakeLists.txt
中对 log
和 exp
函数的检查。然后从 mysqrt.cxx
中删除对 HAVE_LOG
和 HAVE_EXP
的检查。同时,我们可以删除 #include <cmath>
。
在 MathFunctions
子目录中,有一个名为 MakeTable.cxx
的新源文件被提供来生成表格。
检查完文件后,我们可以看到表是作为有效的 C++ 代码生成的,并且输出文件名作为参数传入。
下一步是创建 MathFunctions/MakeTable.cmake
。然后,将适当的命令添加到文件中以构建 MakeTable
可执行文件,然后将其作为构建过程的一部分运行。需要几个命令来完成这个任务。
首先,我们为 MakeTable
添加一个可执行文件。
add_executable(MakeTable MakeTable.cxx)
在创建可执行文件之后,我们使用 target_link_libraries()
将 tutorial_compiler_flags
添加到可执行文件中。
target_link_libraries(MakeTable PRIVATE tutorial_compiler_flags)
然后添加自定义命令,它规定了在运行 MakeTable
时如何产生 Table.h
。
- add_custom_command:向生成的构建系统添加自定义构建规则。
add_custom_command(OUTPUT output1 [output2 ...] COMMAND command1 [ARGS] [args1...] [COMMAND command2 [ARGS] [args2...] ...] [MAIN_DEPENDENCY depend] [DEPENDS [depends...]] [BYPRODUCTS [files...]] [IMPLICIT_DEPENDS <lang1> depend1 [<lang2> depend2] ...]
- 这个签名用于添加自定义命令以产生输出。
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
库的源代码列表中去完成这一任务。(如果不添加这一步,仅有 include(MakeTable.cmake)
并不会生成 Table.h
文件)
add_library(SqrtLibrary STATIC
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
我们还需要添加当前的 binary
(就是 build
文件夹下的 MathFunctions
)目录到 include
目录列表,以便 Table.h
可以被找到并被 mysqrt.cxx
包含。
target_include_directories(SqrtLibrary PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
作为最后一步,我们需要包含 MakeTable
。在 MathFunctions/CMakeLists.txt
的顶部。
include(MakeTable.cmake)
现在让我们使用被生成的表。首先,修改 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
。最后,它将编译 mysqrt
。它包含了 Table.h
来生成 MathFunctions
库。
运行 Tutorial
可执行文件并验证它是否正在使用表。
9. Building an Installer(Step 9)-构建一个安装程序
接下来假设我们想要将我们的项目分发给其他人,以便他们可以使用它。我们希望在各种平台上提供二进制和源代码发行版。这与我们之前在安装和测试中所做的安装略有不同,在安装中我们安装了从源代码构建的二进制文件。在本例中,我们将构建支持二进制安装和包管理特性的安装包。为此,我们将使用 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}")
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
这就是它的全部。我们从包含 InstallRequiredSystemLibraries
开始。此模块将包含当前平台上项目所需的任何运行时库。接下来,我们将一些 CPack
变量设置到存储此项目的许可证和版本信息的位置。本教程前面设置了版本信息,并且 License.txt
已包含在此步骤的顶层源代码目录中。CPACK_SOURCE_GENERATOR
变量为源包选择一种文件格式。
最后,我们将包括 CPack
模块,它将使用这些变量和当前系统的一些其他属性来设置安装程序。
下一步是以通常的方式构建项目,然后运行 cpack
可执行文件。要构建一个二进制发行版,从二进制目录运行:
cpack
要指定生成器,请使用 -G
选项。对于多配置构建,使用 -c
来指定配置。例如:
cpack -G ZIP -C Debug
有关可用生成器的列表,请参阅 cpack-generators(7) 或调用 cpack --help
。像 ZIP 这样的归档生成器创建所有已安装文件的压缩归档。
要创建完整源代码树的存档,您可以输入:
cpack --config CPackSourceConfig.cmake
或者,运行 make package
或右键单击 Package
目标并在 IDE
中 Build Project
。
运行二进制目录中的安装程序。然后运行已安装的可执行文件并验证它是否有效。
10. Selecting Static or Shared Libraries(Step 10)-混合静态库和动态库
在本节中,我们将展示如何使用 BUILD_SHARED_LIBS
变量来控制 add_library()
的默认行为,并允许控制如何构建没有显式类型的库( STATIC
、SHARED
、MODULE
或 OBJECT
)。
要做到这一点,我们需要将 BUILD_SHARED_LIBS
添加到顶层的 CMakeLists.txt
中。我们使用 option()
命令是因为它允许用户选择该值是 ON
还是 OFF
。
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
接下来,我们需要为静态库和共享库指定输出目录。
- CMAKE_ARCHIVE_OUTPUT_DIRECTORY :
- 用于指定存放静态库(archive 文件,通常以.a或.lib为扩展名)的输出目录。
- 静态库是编译后的代码库,它包含了函数和符号,可以在链接时与其他可执行程序或动态库一起使用。
- CMAKE_LIBRARY_OUTPUT_DIRECTORY:
- 用于指定存放共享库(dynamic library,通常以.so、.dll或.dylib为扩展名)的输出目录。
- 共享库是一种可以在运行时被动态加载的库,可以被多个可执行程序或其他共享库使用。
LIBRARY_OUTPUT_PATH
是老版本的写法,现不再推荐使用。使用官方建议的变量可以提高项目的一致性,因为它们符合 CMake 的最新标准和最佳实践。
- CMAKE_RUNTIME_OUTPUT_DIRECTORY:
- 用于指定存放可执行程序(runtime 可执行文件)的输出目录。
- 可执行程序是编译后的应用程序,它们是最终用户可以运行的文件。
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)
最后,更新 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
。
target_link_libraries(MathFunctions PUBLIC tutorial_compiler_flags)
# define the symbol stating we are using the declspec(dllexport) when
# building on windows
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")
11. Adding Export Configuration (Step 11)-添加导出配置
在本教程的第 4 4 4 步(安装和测试)过程中,我们为 CMake 添加了安装项目库和头文件的功能。在第 9 9 9 步(构建一个安装程序)期间,我们添加了打包这些信息的功能,以便将其分发给其他人。
下一步是添加必要的信息,以便其他 CMake 项目可以使用我们的项目,无论是从构建目录,本地安装还是打包。
第一步是更新我们的 install(TARGETS)
命令,不仅要指定 DESTINATION
,还要指定 EXPORT
。EXPORT
关键字生成一个 CMake 文件,其中包含从安装树中导入 install 命令中列出的所有目标的代码。因此,让我们继续,通过更新 MathFunctions/CMakeLists.txt
中的安装命令显式地导出 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 contains
path:
"/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions"
which is prefixed in the source directory.
CMake 想说的是,在生成导出信息期间,它将导出一个与当前机器本质上绑定的路径,并且在其他机器上无效。这个问题的解决方案是更新 MathFunctions
target_include_directories()
,以了解它在从构建目录和从安装/包中使用时需要不同的 INTERFACE
位置。这意味着将 MathFunctions
的 target_include_directories()
调用转换为如下样子:
target_include_directories(MathFunctions
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
一旦这个被更新,我们可以重新运行 CMake 并验证它不再发出警告。
此时,我们已经让 CMake 正确地打包了所需的目标信息,但我们仍然需要生成一个 MathFunctionsConfig
。这样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)
接下来,我们执行 configure_package_config_file()
。该命令将配置所提供的文件,但与标准的 configure_file()
方式有一些具体的区别。为了正确地利用这个功能,除了所需的内容外,输入文件还应该有一行文本 @PACKAGE_INIT@
。该变量将被替换为一个代码块,该代码块将集合值转换为相对路径。这些新值可以用相同的名称引用,但要加上 PACKAGE_
前缀。
install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)
include(CMakePackageConfigHelpers)
# generate the config file that 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
)
write_basic_package_version_file()
是下一个。这个命令写一个文件供 find_package()
使用,记录所需包的版本和兼容性。在这里,我们使用 Tutorial_VERSION_*
变量,并说它与 AnyNewerVersion
兼容,这表示该版本或任何更高的版本与请求的版本兼容。
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
COMPATIBILITY AnyNewerVersion
)
最后,设置两个生成的文件都要安装:
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
DESTINATION lib/cmake/MathFunctions
)
至此,我们已经为我们的项目生成了一个可重新定位的 CMake 配置,它可以在项目安装或打包后使用。如果我们想让我们的项目也从构建目录中使用,我们只需要在顶层 CMakeLists.txt
的底部添加以下内容:
export(EXPORT MathFunctionsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)
通过这个导出调用,我们现在生成一个 MathFunctionsTargets
。允许配置 MathFunctionsConfig.Cmake
在构建目录中,以供其他项目使用,而无需安装它。
12. Packaging Debug and Release (Step 12)-打包调试和发布
注意:这个例子只能用于单配置生成器,不能工作于多配置生成器(例如 Visual Studio)。
默认形况下,CMake 的构建目录只能包含一个单独的配置,可以是 Debug
、Release
、MinSizeRel
或 RelWithDebInfo
。然而可以让 CPack
打包多个构建目录,并构造一个包含了当前项目的多个配置的包。
首先,我们要确保 debug
和 release
构建为可执行目标和库目标使用不同的名字。我们为调试的可执行目标和库目标添加后缀 d
。
在顶层 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 tutorial_compiler_flags)
同时,添加版本号到 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("release/CPackConfig.cmake")
set(CPACK_INSTALL_CMAKE_PROJECTS
"debug;Tutorial;ALL;/"
"release;Tutorial;ALL;/"
)
在 Step12 目录下,运行 cpack
,通过 config
选项指定我们自定义的配置文件:
cpack --config MultiCPackConfig.cmake