CMake 秘籍(一)(2)https://developer.aliyun.com/article/1524601
如何操作
我们将重用之前的配方中的hello-world.cpp
和CMakeLists.txt
。唯一的区别在于 CMake 的调用方式,因为我们现在必须使用-G
命令行开关显式传递生成器。
- 首先,我们使用以下命令配置项目:
$ mkdir -p build $ cd build $ cmake -G Ninja .. -- The CXX compiler identification is GNU 8.1.0 -- Check for working CXX compiler: /usr/bin/c++ -- Check for working CXX compiler: /usr/bin/c++ -- works -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done -- Generating done -- Build files have been written to: /home/user/cmake-cookbook/chapter-01/recipe-02/cxx-example/build
- 在第二步,我们构建项目:
$ cmake --build . [2/2] Linking CXX executable hello-world
它是如何工作的
我们已经看到,配置步骤的输出与之前的配方相比没有变化。然而,编译步骤的输出和构建目录的内容将会有所不同,因为每个生成器都有其特定的文件集:
build.ninja
和rules.ninja
:包含 Ninja 的所有构建语句和构建规则。CMakeCache.txt
:无论选择哪种生成器,CMake 总是会在此文件中生成自己的缓存。CMakeFiles
:包含 CMake 在配置过程中生成的临时文件。cmake_install.cmake
:处理安装规则的 CMake 脚本,用于安装时使用。
注意 cmake --build .
是如何将 ninja
命令包装在一个统一的跨平台接口中的。
另请参阅
我们将在 第十三章,替代生成器和交叉编译中讨论替代生成器和交叉编译。
CMake 文档是了解生成器的良好起点:cmake.org/cmake/help/latest/manual/cmake-generators.7.html
。
构建和链接静态和共享库
本配方的代码可在 github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-03
获取,并提供了 C++ 和 Fortran 的示例。本配方适用于 CMake 3.5(及以上)版本,并在 GNU/Linux、macOS 和 Windows 上进行了测试。
一个项目几乎总是由多个源文件构建的单个可执行文件组成。项目被拆分到多个源文件中,通常分布在源树的不同子目录中。这种做法不仅有助于在项目中组织源代码,而且极大地促进了模块化、代码重用和关注点分离,因为可以将常见任务分组到库中。这种分离还简化了项目开发过程中的重新编译并加快了速度。在本配方中,我们将展示如何将源分组到库中,以及如何将目标链接到这些库。
准备工作
让我们回到最初的例子。然而,我们不再使用单一的源文件来编译可执行文件,而是引入一个类来封装要打印到屏幕的消息。这是我们更新的 hello-world.cpp
:
#include "Message.hpp" #include <cstdlib> #include <iostream> int main() { Message say_hello("Hello, CMake World!"); std::cout << say_hello << std::endl; Message say_goodbye("Goodbye, CMake World"); std::cout << say_goodbye << std::endl; return EXIT_SUCCESS; }
Message
类封装了一个字符串,提供了对 <<
操作符的重载,并由两个源文件组成:Message.hpp
头文件和相应的 Message.cpp
源文件。Message.hpp
接口文件包含以下内容:
#pragma once #include <iosfwd> #include <string> class Message { public: Message(const std::string &m) : message_(m) {} friend std::ostream &operator<<(std::ostream &os, Message &obj) { return obj.printObject(os); } private: std::string message_; std::ostream &printObject(std::ostream &os); };
相应的实现包含在 Message.cpp
中:
#include "Message.hpp" #include <iostream> #include <string> std::ostream &Message::printObject(std::ostream &os) { os << "This is my very nice message: " << std::endl; os << message_; return os; }
如何操作
这两个新文件也需要编译,我们需要相应地修改 CMakeLists.txt
。然而,在这个例子中,我们希望先将它们编译成一个库,而不是直接编译成可执行文件:
- 创建一个新的 目标,这次是静态库。库的名称将是目标的名称,源代码列表如下:
add_library(message STATIC Message.hpp Message.cpp )
- 创建
hello-world
可执行文件的目标未作修改:
add_executable(hello-world hello-world.cpp)
- 最后,告诉 CMake 库目标需要链接到可执行目标:
target_link_libraries(hello-world message)
- 我们可以使用与之前相同的命令进行配置和构建。这次将编译一个库,与
hello-world
可执行文件一起:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . Scanning dependencies of target message [ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o [ 50%] Linking CXX static library libmessage.a [ 50%] Built target message Scanning dependencies of target hello-world [ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o [100%] Linking CXX executable hello-world [100%] Built target hello-world $ ./hello-world This is my very nice message: Hello, CMake World! This is my very nice message: Goodbye, CMake World
工作原理
前面的示例介绍了两个新命令:
add_library(message STATIC Message.hpp Message.cpp)
:这将生成将指定源代码编译成库所需的构建工具指令。add_library
的第一个参数是目标的名称。在整个CMakeLists.txt
中可以使用相同的名称来引用该库。生成的库的实际名称将由 CMake 通过在前面添加前缀lib
和作为后缀的适当扩展名来形成。库扩展名是根据第二个参数STATIC
或SHARED
以及操作系统来确定的。target_link_libraries(hello-world message)
:将库链接到可执行文件。此命令还将确保hello-world
可执行文件正确依赖于消息库。因此,我们确保消息库总是在我们尝试将其链接到hello-world
可执行文件之前构建。
成功编译后,构建目录将包含 libmessage.a
静态库(在 GNU/Linux 上)和 hello-world
可执行文件。
CMake 接受 add_library
的第二个参数的其他有效值,我们将在本书的其余部分遇到所有这些值:
STATIC
,我们已经遇到过,将用于创建静态库,即用于链接其他目标(如可执行文件)的对象文件的归档。SHARED
将用于创建共享库,即可以在运行时动态链接和加载的库。从静态库切换到动态共享对象(DSO)就像在CMakeLists.txt
中使用add_library(message SHARED Message.hpp Message.cpp)
一样简单。OBJECT
可用于将传递给add_library
的列表中的源代码编译成目标文件,但不将它们归档到静态库中,也不将它们链接到共享对象中。如果需要一次性创建静态库和共享库,使用对象库尤其有用。我们将在本示例中演示这一点。MODULE
库再次是 DSOs。与SHARED
库不同,它们不在项目内链接到任何其他目标,但可能会在以后动态加载。这是构建运行时插件时要使用的参数。
CMake 还能够生成特殊类型的库。这些库在构建系统中不产生输出,但在组织目标之间的依赖关系和构建要求方面非常有帮助:
IMPORTED
,这种类型的库目标代表位于项目外部的库。这种类型的库的主要用途是模拟项目上游包提供的预先存在的依赖项。因此,IMPORTED
库应被视为不可变的。我们将在本书的其余部分展示使用IMPORTED
库的示例。另请参见:cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#imported-targets
INTERFACE
,这种特殊的 CMake 库类型类似于IMPORTED
库,但它是可变的,没有位置。它的主要用例是模拟项目外部目标的使用要求。我们将在第 5 个配方中展示INTERFACE
库的使用案例,即将依赖项作为 Conda 包分发项目,在第十一章,打包项目中。另请参见:cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#interface-libraries
ALIAS
,顾名思义,这种类型的库为目标定义了一个别名。因此,不可能为IMPORTED
库选择别名。另请参见:cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#alias-libraries
在本例中,我们直接使用add_library
收集源文件。在后面的章节中,我们将展示使用target_sources
CMake 命令来收集源文件,特别是在第七章,项目结构化中。也可以参考 Craig Scott 的这篇精彩博文:crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/
,它进一步说明了使用target_sources
命令的动机。
还有更多
现在让我们展示 CMake 中提供的对象库功能的使用。我们将使用相同的源文件,但修改CMakeLists.txt
:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-03 LANGUAGES CXX) add_library(message-objs OBJECT Message.hpp Message.cpp ) # this is only needed for older compilers # but doesn't hurt either to have it set_target_properties(message-objs PROPERTIES POSITION_INDEPENDENT_CODE 1 ) add_library(message-shared SHARED $<TARGET_OBJECTS:message-objs> ) add_library(message-static STATIC $<TARGET_OBJECTS:message-objs> ) add_executable(hello-world hello-world.cpp) target_link_libraries(hello-world message-static)
首先,注意add_library
命令已更改为add_library(message-objs OBJECT Message.hpp Message.cpp)
。此外,我们必须确保编译为对象文件生成位置无关代码。这是通过使用set_target_properties
命令设置message-objs
目标的相应属性来完成的。
对于目标显式设置POSITION_INDEPENDENT_CODE
属性的需求可能只在某些平台和/或使用旧编译器时才会出现。
现在,这个对象库可以用来获取静态库(称为message-static
)和共享库(称为message-shared
)。需要注意的是,用于引用对象库的生成器表达式语法:$
。生成器表达式是 CMake 在生成时(即配置时间之后)评估的构造,以产生特定于配置的构建输出。另请参阅:cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html
。我们将在第五章,配置时间和构建时间操作中深入探讨生成器表达式。最后,hello-world
可执行文件与message
库的静态版本链接。
是否可以让 CMake 生成两个同名的库?换句话说,它们是否可以都称为message
而不是message-static
和message-shared
?我们需要修改这两个目标的属性:
add_library(message-shared SHARED $<TARGET_OBJECTS:message-objs> ) set_target_properties(message-shared PROPERTIES OUTPUT_NAME "message" ) add_library(message-static STATIC $<TARGET_OBJECTS:message-objs> ) set_target_properties(message-static PROPERTIES OUTPUT_NAME "message" )
我们可以链接 DSO 吗?这取决于操作系统和编译器:
- 在 GNU/Linux 和 macOS 上,无论选择哪个编译器,它都能正常工作。
- 在 Windows 上,它无法与 Visual Studio 配合使用,但可以与 MinGW 和 MSYS2 配合使用。
为什么?生成好的 DSO 需要程序员限制符号可见性。这是通过编译器的帮助实现的,但在不同的操作系统和编译器上约定不同。CMake 有一个强大的机制来处理这个问题,我们将在第十章,编写安装程序中解释它是如何工作的。
使用条件控制编译
本节代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-04
找到,并包含一个 C++示例。本节适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
到目前为止,我们研究的项目相对简单,CMake 的执行流程是线性的:从一组源文件到一个单一的可执行文件,可能通过静态或共享库。为了确保对项目构建过程中所有步骤的执行流程有完全的控制,包括配置、编译和链接,CMake 提供了自己的语言。在本节中,我们将探讨使用条件结构if-elseif-else-endif
。
CMake 语言相当庞大,包括基本控制结构、CMake 特定命令以及用于模块化扩展语言的新函数的基础设施。完整的概述可以在线找到:cmake.org/cmake/help/latest/manual/cmake-language.7.html
。
如何操作
让我们从与上一个配方相同的源代码开始。我们希望能够在这两种行为之间切换:
- 将
Message.hpp
和Message.cpp
编译成一个库,无论是静态还是共享,然后将生成的库链接到hello-world
可执行文件中。 - 将
Message.hpp
、Message.cpp
和hello-world.cpp
编译成一个单一的可执行文件,不生成库。
让我们构建CMakeLists.txt
以实现这一点:
- 我们首先定义最小 CMake 版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-04 LANGUAGES CXX)
- 我们引入了一个新变量,
USE_LIBRARY
。这是一个逻辑变量,其值将被设置为OFF
。我们还打印其值供用户查看:
set(USE_LIBRARY OFF) message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
- 将 CMake 中定义的
BUILD_SHARED_LIBS
全局变量设置为OFF
。调用add_library
并省略第二个参数将构建一个静态库:
set(BUILD_SHARED_LIBS OFF)
- 然后,我们引入一个变量
_sources
,列出Message.hpp
和Message.cpp
:
list(APPEND _sources Message.hpp Message.cpp)
- 然后,我们根据
USE_LIBRARY
的值引入一个if-else
语句。如果逻辑开关为真,Message.hpp
和Message.cpp
将被打包成一个库:
if(USE_LIBRARY) # add_library will create a static library # since BUILD_SHARED_LIBS is OFF add_library(message ${_sources}) add_executable(hello-world hello-world.cpp) target_link_libraries(hello-world message) else() add_executable(hello-world hello-world.cpp ${_sources}) endif()
- 我们可以再次使用相同的命令集进行构建。由于
USE_LIBRARY
设置为OFF
,所有源文件将被编译成hello-world
可执行文件。这可以通过在 GNU/Linux 上运行objdump -x
命令来验证。
工作原理
我们引入了两个变量:USE_LIBRARY
和BUILD_SHARED_LIBS
。两者都设置为OFF
。正如 CMake 语言文档中所详述的,真或假值可以用多种方式表达:
- 逻辑变量在以下情况下为真:设置为
1
、ON
、YES
、TRUE
、Y
或非零数字。 - 逻辑变量在以下情况下为假:设置为
0
、OFF
、NO
、FALSE
、N
、IGNORE
、NOTFOUND
、空字符串或以-NOTFOUND
结尾。
USE_LIBRARY
变量将在第一种和第二种行为之间切换。BUILD_SHARED_LIBS
是 CMake 提供的一个全局标志。记住,add_library
命令可以在不传递STATIC
/SHARED
/OBJECT
参数的情况下调用。这是因为,内部会查找BUILD_SHARED_LIBS
全局变量;如果为假或未定义,将生成一个静态库。
这个例子展示了在 CMake 中引入条件语句以控制执行流程是可能的。然而,当前的设置不允许从外部设置开关,也就是说,不通过手动修改CMakeLists.txt
。原则上,我们希望将所有开关暴露给用户,以便在不修改构建系统代码的情况下调整配置。我们将在稍后展示如何做到这一点。
else()
和endif()
中的()
可能会在你开始阅读和编写 CMake 代码时让你感到惊讶。这些的历史原因是能够指示作用域。例如,如果这有助于读者理解,可以使用if(USE_LIBRARY) ... else(USE_LIBRARY) ... endif(USE_LIBRARY)
。这是一个品味问题。
在引入_sources
变量时,我们向代码的读者表明这是一个不应在当前作用域外使用的局部变量,方法是将其前缀加上一个下划线。
向用户展示选项
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-05
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在上一食谱中,我们以相当僵硬的方式引入了条件:通过引入具有硬编码真值的变量。有时这可能很有用,但它阻止了代码用户轻松切换这些变量。僵硬方法的另一个缺点是,CMake 代码没有向读者传达这是一个预期从外部修改的值。在项目构建系统生成中切换行为的推荐方法是使用option()
命令在CMakeLists.txt
中将逻辑开关作为选项呈现。本食谱将向您展示如何使用此命令。
如何操作
让我们回顾一下上一食谱中的静态/共享库示例。我们不再将USE_LIBRARY
硬编码为ON
或OFF
,而是更倾向于将其作为具有默认值的选项公开,该默认值可以从外部更改:
- 将上一食谱中的
set(USE_LIBRARY OFF)
命令替换为具有相同名称和默认值为OFF
的选项。
option(USE_LIBRARY "Compile sources into a library" OFF)
- 现在,我们可以通过将信息传递给 CMake 的
-D
CLI 选项来切换库的生成:
$ mkdir -p build $ cd build $ cmake -D USE_LIBRARY=ON .. -- ... -- Compile sources into a library? ON -- ... $ cmake --build . Scanning dependencies of target message [ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o [ 50%] Linking CXX static library libmessage.a [ 50%] Built target message Scanning dependencies of target hello-world [ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o [100%] Linking CXX executable hello-world [100%] Built target hello-world
-D
开关用于为 CMake 设置任何类型的变量:逻辑值、路径等。
工作原理
option
命令接受三个参数:
option(<option_variable> "help string" [initial value])
是代表选项的变量名。
"帮助字符串"
是记录选项的字符串。此文档在 CMake 的终端或图形用户界面中可见。[初始值]
是选项的默认值,可以是ON
或OFF
。
还有更多
有时需要引入依赖于其他选项值的选项。在我们的示例中,我们可能希望提供生成静态或共享库的选项。但是,如果USE_LIBRARY
逻辑未设置为ON
,则此选项将没有意义。CMake 提供了cmake_dependent_option()
命令来定义依赖于其他选项的选项:
include(CMakeDependentOption) # second option depends on the value of the first cmake_dependent_option( MAKE_STATIC_LIBRARY "Compile sources into a static library" OFF "USE_LIBRARY" ON ) # third option depends on the value of the first cmake_dependent_option( MAKE_SHARED_LIBRARY "Compile sources into a shared library" ON "USE_LIBRARY" ON )
如果USE_LIBRARY
设置为ON
,则MAKE_STATIC_LIBRARY
默认为OFF
,而MAKE_SHARED_LIBRARY
默认为ON
。因此,我们可以运行以下命令:
$ cmake -D USE_LIBRARY=OFF -D MAKE_SHARED_LIBRARY=ON ..
这仍然不会构建库,因为USE_LIBRARY
仍然设置为OFF
。
如前所述,CMake 通过包含模块来扩展其语法和功能,这些模块可以是 CMake 自带的,也可以是自定义的。在这种情况下,我们包含了一个名为CMakeDependentOption
的模块。如果没有包含语句,cmake_dependent_option()
命令将不可用。另请参阅cmake.org/cmake/help/latest/module/CMakeDependentOption.html
。
任何模块的手册页也可以使用cmake --help-module
从命令行访问。例如,cmake --help-option CMakeDependentOption
将打印刚刚讨论的模块的手册页。
指定编译器
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-06
获取,并包含一个 C++/C 示例。该食谱适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
到目前为止,我们没有过多考虑的一个方面是编译器的选择。CMake 足够复杂,可以根据平台和生成器选择最合适的编译器。CMake 还能够将编译器标志设置为一组合理的默认值。然而,我们通常希望控制编译器的选择,在本食谱中,我们将展示如何做到这一点。在后面的食谱中,我们还将考虑构建类型的选择,并展示如何控制编译器标志。
CMake 秘籍(一)(4)https://developer.aliyun.com/article/1524608