学习C++:C++进阶(三)CMake基础篇---用一个小型项目了解CMake及环境构建

V1.1 于2022年7月15日第二次修改:添加了比较多的解释图,解读了各类库的CMakelist.txt文件

目录

第一部分 基础篇(Basics)

1.0 本部分主要学什么(Intro)

1.1 启动CMake (Kickstarting CMake)

1.1.1 本章引言(Intro):介绍本章学习内容及可以运行本章内容的环境需要

1.1.2 概括CMake(CMake in a nutshell)

1.1.3 安装cmake (Installing CMake)

1.1.4 构建你的第一个项目 (Building your first project)

1.1.5 一个小型的 CMakeLists.txt 文件(A minimal CMakeLists.txt file)

1.1.6 了解 CMake 构建过程 (Understanding the CMake build process)

1.1.7 源文件夹和构建文件夹 (Source folders and build folders)

1.1.8 CMake关键字的简单介绍(A brief introduction to CMake keywords)

1.1.9 不同的工具链和构建类型(Different toolchains and build types)

1.1.10  构建类型(Build types)

1.1.11 使用预设维护良好的构建配置(Maintaining good build configurations with presets)

1.1.12 本章小结及拓展 (Summary&Further reading)

1.2   在命令行、ccmake、cmakeGUI、clion、QT creator、VScode使用Cmake进行项目配置、构建与运行(Use Cmake on the command line, ccmake, cmakeGUI, clion, QT creator, and VScode to configure, build, and run projects)

1.2.0 本章引言(Intro)

1.2.1 通过命令行界面使用 CMake(Using CMake via a command-line interface)

1.2.2 配置CMake项目(Configuring a project via the CLI)

1.2.3 通过 CLI 构建配置的项目(Building a configured project via CLI)

1.2.4  使用 CMake-GUI 和 ccmake 进行高级配置(Advanced configuration using CMake-GUI and ccmake)

1.2.5  在 Visual Studio、Visual Studio Code 、CLion 和 Qt Creator 中使用 CMake(Using CMake in Visual Studio, Visual Studio Code, and Qt Creator)

1.3  创建 CMake 项目(Creating a CMake Project)

1.3.1  设置项目(Setting up a project)*

1.3.2 使用嵌套项目(Working with nested projects) 

1.3.3  创建一个“hello world”可执行文件(Creating a "hello world" executable) 

1.3.4  创建一个简单的库(Creating a simple library) 

1.3.5  命名库(Naming libraries)

 1.3.6  共享库中的符号可见性(Symbol visibility in shared libraries)

1.3.7 更改默认可见性 (Changing the default visibility)

1.3.8  接口或仅头文件库 (Interface or header-only libraries)

1.3.9 对象库——仅供内部使用(Object libraries – for internal use only)

1.3.10  将其整合在一起——使用您的库(Bringing it together – using your libraries)

1.3.11 设置编译器和链接器选项(Setting compiler and linker options)

1.3.12 调试编译器选项(Debugging compiler options) 

1.3.13  库别名(Library aliases)

1.3.14 总结及问题(Summary&Questions)

1.4 通过一个案例总结前三章的内容(Summarize the content of the first three chapters with a case)

1.4.1 项目源码及文件(Project source code and files)

1.4.2 构建并创建项目(Build and create the project)

1.4.3 用cmake构建仅有头文件的库(Building a header-only library with cmake)

1.4.4 用cmake构建对象库(Building object libraries with cmake)

1.4.4 用cmake构建共享库(Building shared libraries with cmake)

1.4.5 用cmake构建静态库(Build static library with cmake)​​​​​​​

1.4.6 外部调用这些创建好的库(call these created libraries externally)​​​​​​​


第一部分 基础篇(Basics)

1.0 本部分主要学什么(Intro)

        在第一章中,您将学习如何调用 CMake 并对其基本概念进行高级概述,以及对 CMake 语言的简要介绍。
        第二章将介绍从命令行、 GUI 或在各种 IDE 和编辑器中使用 CMake。它将说明如何更改各种配置选项并选择不同的编译器。
        在第三章中,我们将介绍如何创建一个简单的 CMake 项目来构建可执行文件和库。

本部分包含以下章节:

第 1 章:启动 CMake
第 2 章:在命令行、ccmake、cmakeGUI、clion、QT creator、VScode使用Cmake进行项目配置、构建与运行
第 3 章:创建 CMake 项目 

1.1 启动CMake (Kickstarting CMake)

1.1.1 本章引言(Intro):介绍本章学习内容及可以运行本章内容的环境需要

1.为什么要学习cmake

        如果您使用 C++ 或 C 开发软件,您可能以前听说过 CMake。在过去的 20 年中,CMake 已经发展成为构建 C++ 应用程序的行业标准。但 CMake 不仅仅是一个构建系统——它是一个构建系统生成器,这意味着它为 Makefile、Ninja、Visual Studio、Qt Creator、Android Studio 和 Xcode 等其他构建系统生成指令。它并不止于构建软件——CMake 还包括支持安装、打包和测试软件的功能。

        作为事实上的行业标准,CMake 是任何 C++ 程序员必须了解的技术。在1.1节中,您将获得 CMake 是什么的高级概述,并了解构建您的第一个程序所需的基础知识。我们将了解 CMake 的构建过程,并概述如何使用 CMake 语言来配置构建过程。

2.本章学习内容

• 概括 CMake
• 安装 CMake
• CMake 构建过程
• 编写 CMake 文件
• 不同的工具链和构建配置 

3.软件版本需要

        要运行本章中的示例,您将需要一个装载 C++17 的最新 C++ 编译器。

        cmake版本要高于3.2.0。

1.1.2 概括CMake(CMake in a nutshell)

1.cmake组件:三个命令行工具&两个交互工具

        CMake 是开源的,可在许多平台上使用。它也是独立于编译器的,在构建和分发跨平台软件时,它是一个非常强大的工具。所有这些特性使它成为以现代方式构建软件的宝贵工具。

CMake 由三个命令行工具组成:

•cmake:CMake本身,用于生成构建指令。
•ctest:CMake 的测试实用程序,用于检测和运行测试。
•cpack:CMake 的打包工具,用于将软件打包成方便的安装程序,例如 deb、RPM 和自解压安装程序

还有两个交互工具:

• cmake-gui:帮助配置项目的 GUI 前端
• ccmake:用于配置 CMake 的交互式终端 UI

2.CMake GUI简单介绍(后文有详细介绍)

cmake-gui 可用于方便地配置 CMake 构建并选择要使用的编译器 

3.ccmake的简单介绍

        如果在控制台上工作,但仍希望有一个交互式配置CMake,那么 ccmake 是正确的工具。虽然不如 cmake-gui 方便,但它提供相同的功能。

4.cmake优势

        CMake 相对于常规构建系统的优势是多方面的。

        首先,有跨平台方面。使用 CMake,可以更轻松地为各种编译器和平台创建构建指令,而无需深入了解相应构建系统的细节。
        其次,CMake 能够发现系统库和依赖项,这大大减轻了查找正确库以构建软件的痛苦。

        另一个好处是 CMake 与 Conan 和 vcpkg 等包管理器很好地集成。
        不仅仅是为多个平台构建软件的能力,而且它对测试、安装和打包软件的原生支持使得 CMake 成为构建软件的候选者,而不仅仅是一个构建系统。能够在一个点上定义从构建和过度测试到打包的所有内容,这极大地有助于长期维护项目。
        CMake 本身对系统的依赖很少,无需用户交互即可在命令行上运行,这使得它非常适合 CI/CD 管道中的构建系统自动化。
        现在我们已经简要介绍了 CMake 可以做什么,让我们学习如何安装 CMake。 

1.1.3 安装cmake (Installing CMake)

1.下载路径

        CMake 可从下面链接免费下载。

CMake官方下载地址http://xn--https-rfa//cmake.org/download/        它可以作为预编译的二进制文件或源代码使用。对于大多数用例来说,预编译的二进制文件就足够了,但由于 CMake 本身的依赖项很少,因此也可以构建一个版本。
        任何主要的 Linux 发行版都在其软件包存储库上提供 CMake。尽管 CMake 的预打包版本通常不是最新版本,但如果系统定期更新,这些安装通常足以使用。

        本教程要求CMake版本大于3.21,我把我的cmake3.22放到了资源里面,供大家免费下载。尽管这些示例不够复杂,不足以要求新标准的功能,但已相应地设置了示例。

2.从源代码构建 CMake

        CMake 是用 C++ 编写的,并使用 Make 来构建自己。从头开始构建 CMake 是可能的,但对于大多数用例,使用二进制下载就可以了。 

        从 https://cmake.org/download/ 下载源码包后,解压到一个文件夹并运行以下命令

./configure make

        如果还想构建 cmake-gui,请使用 --qt-gui 选项对其进行配置。这需要安装 Qt。配置需要一段时间,但一旦成功,可以使用以下命令安装 CMake:

make install

        测试是否安装成功,可以执行以下命令:

cmake --version

        这将打印出 CMake 的版本,如下所示:

liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/下载$ cmake --version
cmake version 3.22.5

CMake suite maintained and supported by Kitware (kitware.com/cmake).

1.1.4 构建你的第一个项目 (Building your first project)

1.生成可执行文件

        现在,是时候动手测试安装是否有效。我们提供了一个简单的 hello world 项目示例,您可以立即下载并构建它。打开控制台,输入以下内容,您就可以开始了:

git clone https://github.com/PacktPublishing/CMake-BestPractices.git 
cd CMake-Best-Practices/chapter_1
mkdir build
cd build 
cmake ..
cmake --build .
liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/桌面/CMake-Best-Practices-main
/chapter01/build$ cmake ..
-- The CXX compiler identification is GNU 9.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/liuhongwei/桌面/CMake-Best-Practices-main/chapter01/build
liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/桌面/CMake-Best-Practices-main
/chapter01/build$ make
[ 50%] Building CXX object simple_executable/CMakeFiles/chapter1.dir/src/main.cpp.o
[100%] Linking CXX executable chapter1
[100%] Built target chapter1

        这将会使一个名为 Chapter_1 的可执行文件在控制台上打印出 Welcome to CMake Best Practices。让我们看看详细发生了什么。

2.短短两行代码程序发生了什么

        首先,示例资源检测出用Git构建项目,然后创建文件树如下所示。示例 CMake 项目的文件结构在构建之前将如下所示: 

.
├── CMakeLists.txt
└── build
└── src
    └── main.cpp

        除了包含源代码的文件夹外,还有一个名为 CMakeLists.txt 的文件。

        此文件包含 CMake 关于如何为项目创建构建说明以及如何构建它的说明。每个 CMake 项目在项目的根目录下都有一个 CMakeLists.txt 文件,但在不同的子文件夹中可能有许多具有该名称的文件。

        克隆存储库后,使用 cmake 启动构建过程。 CMake 的构建过程是一个两阶段的过程。   

        第一步,通常称为配置(configuration),读取 CMakeLists.txt 文件并为系统的原生构建工具链生成指令

        第二步,执行这些构建指令构建可执行文件或库。 

        在配置(configuration)步骤中,检查构建需求,解析依赖关系,并生成构建指令。

*个人理解:第一步是将cmake语言转化为机器语言,第二步是通过机器语言执行指令。

        配置项目还会创建一个名为 CMakeCache.txt 的文件,其中包含创建构建指令所需的所有信息。

        下一次调用 cmake --build 。通过内部调用 CMake 执行构建;如果您使用的是 Windows,它会通过调用 Visual Studio 编译器来实现。这是编译二进制文件的实际步骤。如果一切顺利,构建文件夹中应该有一个名为 Chapter1 的可执行文件。

        为简洁起见,我们 cd 到前面示例中的构建目录并使用相对路径来查找源文件夹。这通常很方便,但是如果你想从其他地方调用 CMake,你可以使用 --S 选项来选择源文件和 --B 选项来选择构建文件夹: (后文将详细介绍)

cmake -S /path/to/source -B /path/to/build
cmake -build /path/to/build

        在持续集成环境中使用 CMake 时,显式传递构建和源目录通常会派上用场,因为显式有助于可维护性。如果您想为不同的配置创建不同的构建目录,例如在构建跨平台软件时,这也很有帮助。

1.1.5 一个小型的 CMakeLists.txt 文件(A minimal CMakeLists.txt file)

1.查看CMakeList.txt文档

对于一个非常简单的 hello world 示例,CMakeLists.txt 文件仅包含几行指令:

cmake_minimum_required(VERSION 3.21)
project(
  "chapter1"
  VERSION 1.0
  DESCRIPTION "A simple project to demonstrate basic CMake usage" 
  LANGUAGES CXX)
add_executable(Chapter1)
target_sources(Chapter1 PRIVATE src/main.cpp)

2.让我们更详细地了解这些说明

• 第一行定义了构建此项目所需的最低 CMake 版本

cmake_minimum_required(VERSION 3.21)

        每个 CMakeLists.txt 文件都以此指令开头。这用于在项目使用 CMake 的功能时警告用户,这些功能仅在某个版本以上可用。通常,我们建议将版本设置为支持项目中使用的功能的最低版本。
• project指令是要构建的项目的名称、版本和描述,然后是项目中使用的编程语言。在这里,我们使用 CXX 将其标记为 C++ 项目。
add_executable 指令告诉CMake 我们要构建一个可执行文件(与我们将在本书后面介绍的库或自定义工件相反)。
• target_sources 语句告诉CMake 在哪里查找名为Chapter1 的可执行文件的源,并且源的可见性仅限于可执行文件。我们将在本文后面部分详细介绍单个命令。
        恭喜——您现在可以使用 CMake 创建软件程序。但是要了解这些命令背后发生了什么,让我们详细了解一下 CMake 构建过程。 

1.1.6 了解 CMake 构建过程 (Understanding the CMake build process)

        CMake 的构建过程分两步进行,如下图所示。

        首先,如果在没有任何特殊标志的情况下调用它,CMake 会在配置过程中扫描系统以查找任何可用的工具链,然后决定其输出应该是什么。

        第二步,也就是调用 cmake --build 的时候,才是真正的编译构建过程:

        执行完cmake后标准输出是 Unix Makefiles,除非检测到的唯一编译器是 Microsoft Visual Studio,在这种情况下,将创建 Visual Studio 解决方案 (.sln)。 

        要更改生成器,您可以将 -G 选项传递给 CMake,如下所示:

cmake .. -G Ninja

        这将生成与 Ninja (https://ninja-build.org/) 一起使用的文件,这是一种替代构建生成器。许多生成器可用于 CMake。可以在 CMake 的网站上找到原生支持的列表:https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html。
        有两种主要类型的生成器 ---- 一种有许多 Makefile 风格和 Ninja 生成器,通常从命令行使用,另一种为 Visual Studio 或 Xcode 等 IDE 创建构建文件。

1.1.7 源文件夹和构建文件夹 (Source folders and build folders)

        在 CMake 中,存在两个逻辑文件夹。

        一个是源文件夹,其中包含一组分层项目,而另一个是构建文件夹,其中包含构建指令、缓存以及所有生成的二进制文件和工件。
        源文件夹的根目录是顶部 CMakeLists.txt 文件所在的位置。

        build 文件夹可以放在源文件夹中,但有些人更喜欢将它放在另一个位置。两者都很好;请注意,对于本文中的示例,我们决定将构建文件夹(biuld文件夹)保留在源文件夹中。 build 文件夹通常称为 build,但它可以采用任何名称,包括不同平台的前缀和后缀。在源代码树中使用构建文件夹时,最好将其添加到 .gitignore 中,这样它就不会被意外签入。

        配置(configuring) CMake 项目时,会在 build 文件夹中重新创建源文件夹的项目和文件夹结构,以便所有构建工件位于同一位置。


        在每个映射文件夹中,都有一个名为 CMakeFiles 的子文件夹,其中包含 CMake 配置步骤生成的所有信息。
以下代码显示了 CMake 项目的示例结构:

liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/桌面/CMake-Best-Practices-main
/chapter01$ tree
.
├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   │   ├── 3.22.5
│   │   │   ├── CMakeCXXCompiler.cmake
│   │   │   ├── CMakeDetermineCompilerABI_CXX.bin
│   │   │   ├── CMakeSystem.cmake
│   │   │   └── CompilerIdCXX
│   │   │       ├── a.out
│   │   │       ├── CMakeCXXCompilerId.cpp
│   │   │       └── tmp
│   │   ├── cmake.check_cache
│   │   ├── CMakeDirectoryInformation.cmake
│   │   ├── CMakeOutput.log
│   │   ├── CMakeTmp
│   │   ├── Makefile2
│   │   ├── Makefile.cmake
│   │   ├── progress.marks
│   │   └── TargetDirectories.txt
│   ├── cmake_install.cmake
│   ├── Makefile
│   ├── policy_example                ----------------------映射文件夹1
│   │   ├── CMakeFiles                ----------------------
│   │   │   ├── CMakeDirectoryInformation.cmake
│   │   │   └── progress.marks
│   │   ├── cmake_install.cmake
│   │   └── Makefile
│   └── simple_executable             ----------------------映射文件夹2
│       ├── chapter1
│       ├── CMakeFiles                ----------------------
│       │   ├── chapter1.dir
│       │   │   ├── build.make
│       │   │   ├── cmake_clean.cmake
│       │   │   ├── compiler_depend.make
│       │   │   ├── compiler_depend.ts
│       │   │   ├── DependInfo.cmake
│       │   │   ├── depend.make
│       │   │   ├── flags.make
│       │   │   ├── link.txt
│       │   │   ├── progress.make
│       │   │   └── src
│       │   │       ├── main.cpp.o
│       │   │       └── main.cpp.o.d
│       │   ├── CMakeDirectoryInformation.cmake
│       │   └── progress.marks
│       ├── cmake_install.cmake
│       └── Makefile
├── CMakeLists.txt
├── policy_example
│   ├── CMakeLists.txt
│   └── src
│       └── main.cpp
└── simple_executable
    ├── CMakeLists.txt
    └── src
        └── main.cpp

16 directories, 40 files

        执行 CMake 配置时,会将 CMake 项目的文件结构映射到 build 文件夹中。每个包含 CMakeLists.txt 文件的文件夹都将被映射,并创建一个名为 CMakeFiles 的子文件夹,其中包含 CMake 用于构建的信息:

        到目前为止,我们已经使用现有项目来了解 CMake 构建过程。我们了解了配置和构建步骤以及生成器,并且我们需要 CMakeLists.txt 文件将必要的信息传递给 CMake。所以,让我们更进一步,看看 CMakeLists.txt 文件长什么样,以及 CMake 语言是如何工作的。

1.1.8 CMake关键字的简单介绍(A brief introduction to CMake keywords)

1.各个cmake关键词简介(后续章节会逐一讨论、先有个印象)

        在编写 CMake 文件时,您需要了解一些核心概念和语言特性。我们不会在这里涵盖语言的每个细节,因为 CMake 的文档在这方面做得很好——尤其是在全面性方面。在以下部分中,我们将概述核心概念和语言特性。后续章节将深入探讨不同方面的细节。

        完整文档可以在 https://cmake.org/cmake/help/latest/manual/cmake-language.7.html

2.CMake语言概述 

        CMake 使用称为 CMakeLists.txt 文件的配置文件来确定构建规范。这些文件是用脚本语言编写的,通常也称为 CMake。
        该语言本身很简单,支持变量、字符串函数、宏、函数定义和导入其他 CMake 文件。
        除了列表(list)之外,不支持结构或类等数据结构。但正是这种相对简单性使得 CMake 项目如果操作得当,本质上是可维护的。
        语法基于关键字(keywords)和空格分隔的参数(whitespace-separated arguments)。例如,以下命令告诉 CMake 哪些文件要添加到库中: 

target_sources(MyLibrary 
                PUBLIC include/api.h
                PRIVATE src/internals.cpp src/foo.cpp)

        PUBLIC 和 PRIVATE 关键字表示文件在链接到此库时的可见性,并用作文件列表之间的分隔符。
        此外,CMake 语言支持所谓的“生成器表达式”,它们在构建系统生成期间进行评估。这些通常用于为每个构建配置指定特殊信息。它们将在第 3 章中详细介绍,创建一个 CMake 项目。

3.project关键字简介 

        CMake 将各种构建工件(例如库(libraries)、可执行文件(executables)、测试(tests)和文档(documentation))组织到项目中。尽管项目可以相互封装,但始终只有一个根项目

        通常,每个 CMakeLists.txt 文件应该只有一个项目,这意味着每个项目必须在源目录中有一个单独的文件夹。 

        项目描述如下:

 project(
"chapter1"
VERSION 1.0
DESCRIPTION "A simple C++ project to demonstrate basic CMake usage" 
LANGUAGES CXX
)

        当前正在解析的项目存储在 PROJECT_NAME 变量中,根项目存储在 CMAKE_PROJECT_NAME 中,这对于确定一个项目是独立的还是封装在另一个项目中很有用。

        从3.21版本开始,还有一个 PROJECT_IS_TOP_LEVEL 变量可以直接判断当前项目是否为顶级项目。此外,使用  <PROJECT-NAME>_IS_TOP_LEVEL ,您可以检测特定项目是否为顶级项目。
        以下是有关项目的一些附加变量。所有这些都可以在根项目的值前面加上 CMAKE_。如果它们未在 project() 指令中定义,则字符串为空:

• PROJECT_DESCRIPTION:项目的描述字符串
• PROJECT_HOMEPAGE_URL:项目的 URL 字符串
• PROJECT_VERSION:提供给项目的完整版本
• PROJECT_VERSION_MAJOR:版本字符串的第一个数字
• PROJECT_VERSION_MINOR:版本字符串的第二个数字
• PROJECT_VERSION_PATCH:版本字符串的第三个数字
• PROJECT_VERSION_TWEAK:版本字符串的第四个数字

        每个项目都有一个源代码和二进制目录,它们可以相互封装。假设以下示例中的每个 CMakeFiles.txt 文件都定义了一个项目:

.
├── CMakeLists.txt #defines project("CMakeBestPractices"...)
├── chapter_1
│   ├── CMakeLists.txt # defines project("Chapter 1"...)

        在解析根文件夹(CMake-Best-Practices)中的 CMakeLists.txt 文件时,PROJECT_NAME 和 CMAKE_PROJECT_NAME 都将是 CMake-Best-Practices。(因为正在解析的项目是总目录且里面的子项目也是总目录的根项目)

         当您解析 chapter_1/CMakeLists.txt 时(如上图),PROJECT_NAME 变量将更改为“Chapter_1”,但 CMAKE_PROJECT_NAME 将保留为 CMake-Best-Practices,如根文件夹中的文件中设置的那样。

        尽管项目可以嵌套,但最好以可以独立工作的方式编写它们。虽然它们可能依赖于文件层次结构中较低的其他项目,但应该不需要一个项目作为另一个项目的子项目存在。可以在同一个 CMakeLists.txt 文件中对 project() 进行多次调用,但我们不鼓励这种做法,因为它会使项目变得混乱且难以维护。

        一般来说,最好为每个项目创建一个 CMakeLists.txt 文件,并用子文件夹组织结构。(各个项目单独编译)
        本文的 示例 以分层方式(每一个项目都有单独的CMakeLists.txt而不是整个项目仅有一个CMakeLists.txt)组织,其中每一章都是一个单独的项目,可能包含更多项目,用于不同的部分和示例。
        虽然每个示例都可以独立构建,但您也可以从存储库的根目录构建此项目。

4. variable关键字简介

        变量是 CMake 语言的核心部分。可以使用 set 命令设置变量并使用 unset 删除。变量名区分大小写。以下示例显示如何设置名为 MYVAR 的变量并为其分配值 1234: 

set(MYVAR "1234") 

        要删除 MYVAR 变量,我们可以使用 unset

unset(MYVAR)

        一般的代码约定是将变量全部大写。在内部,变量始终表示为字符串。
        可以使用 $ 符号和大括号访问(message)变量的值:

message(STATUS "The content of MYVAR are ${MYVAR}")

        变量引用甚至可以嵌套并由内向外求值:

${outer_${inner_variable}_variable} 

        变量可以通过以下方式限定范围:
• 函数范围:在函数内部设置的变量仅在函数内部可见。
• 目录范围:源树中的每个子目录都绑定变量并包括父目录中的任何变量绑定。
• 持久缓存:缓存变量可以是系统定义的,也可以是用户定义的。这些值在多次运行中保持不变。
将 PARENT_SCOPE 选项传递给 set() 会使变量在父范围内可见。
CMake 带有各种各样的预定义变量。完整列表可在

https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html 获得。

 5.list关键字简介

        即使 CMake 在内部将变量存储为字符串,也可以通过用分号拆分值来处理 CMake 中的列表。

        以下是两种创建列表的方式:

        1.可以通过将多个不带引号的变量传递给 set()

        2.直接作为分号分隔的字符串来创建列表:

set(MYLIST abc def ghi)
set(MYLIST "abc;def;ghi")

        可以使用 list 命令通过修改其内容、重新排序或查找内容来操作列表。

        以下代码将查询 MYLIST 以获取 abc 值的索引,然后检索该值并将其存储在名为 ABC 的变量中: 

list(FIND MYLIST abc ABC_INDEX)
list(GET MYLIST ${ABC_INDEX} ABC)

        要将值附加到列表中,我们可以使用 APPEND 关键字。这里,xyz 值被附加到 MYLIST:

list(APPEND MYLIST "xyz")

6.缓存的变量和选项简介

         CMake 缓存了一些变量,以便它们在后续构建中运行得更快。变量存储在 CMakeCache.txt 文件中。通常,您不必手动编辑它们,但它们非常适合调试不符合预期的构建。
        所有用于配置构建的变量都被缓存。

        要使用 foo 值缓存名为 ch1_MYVAR 的自定义变量,可以使用 set 命令,如下所示:

 set(ch1_MYVAR foo CACHE STRING "Variable foo that configures bar")

        请注意,缓存的变量必须具有类型和文档字符串,以提供它们的快速摘要。

        大多数自动生成的缓存变量都标记为高级,这意味着它们在 cmake-gui 和 ccmake 中默认对用户隐藏。
        为了使它们可见,必须明确地切换它们。如果 CMakeLists.txt 文件生成了其他缓存变量,也可以通过调用 mark_as_advanced(MYVAR) 命令来隐藏它们:(第二章有详细介绍)

        用户应更改的任何选项或变量都应标记为高级。
        对于简单的布尔缓存变量,CMake 还提供了 option 关键字。除非另有说明,否则选项的默认值为 OFF。它们还可以通过 CMakeDependentOption 模块相互依赖: 

option(CHAPTER1_PRINT_LANGUAGE_EXAMPLES "Print examples for 
 each language" OFF)
include(CMakeDependentOption)
cmake_dependent_option(CHAPTER1_PRINT_HELLO_WORLD "print a 
 greeting from chapter1 " ON CHAPTER1_PRINT_LANGUAGE_EXAMPLES 
 ON)

        选项通常是指定简单项目配置的便捷方式。它们是 bool 类型的缓存变量。如果已经存在与选项同名的变量,则对选项的调用不会执行任何操作。

7.Propertie关键字简介 

        CMake 中的属性是附加到 CMake 的特定对象或范围的值,例如文件、目标、目录或测试用例。

        可以使用 set_property 函数设置或更改属性。

        要读取属性的值,可以使用 get_property 函数,它遵循类似的模式。

        默认情况下,set_property 会覆盖已存储在属性中的值。通过将 APPEND 或 APPEND_STRING 传递给 set_property,可以将值添加到当前值。 

        完整签名如下:

set_property(<Scope> <EntityName>
              [APPEND] [APPEND_STRING]
              PROPERTY <propertyName> [<values>])

        范围说明符可能具有以下值:
• GLOBAL:影响整个构建过程的全局属性。
• DIRECTORY <dir>:绑定到当前目录或<dir> 中指定的目录的属性。这些也可以使用 set_directory_properties 命令直接设置。
• TARGET <targets>:特定目标的属性。它们也可以使用 set_target_properties 函数进行设置。
• SOURCE <files>:将属性应用于源文件列表。它们也可以使用 set_source_files_properties 直接设置。此外,还有 SOURCE DIRECTORY 和 SOURCE TARGET_DIRECTORY 扩展选项:
•DIRECTORY <dirs>:设置目录范围内源文件的属性。该目录必须已经被 CMake 解析为当前目录或使用 add_subdirectory 添加。
•TARGET_DIRECTORY <targets>:这会将属性设置为创建指定目标的目录。同样,目标必须已经存在于设置属性的位置。
• INSTALL <files>:设置已安装文件的属性。这些可以用来控制 cpack 的行为。
• TEST <tests>:设置测试的属性。它们也可以使用 set_test_properties 直接设置。
• CACHE <entry>:设置缓存变量的属性。最常见的包括将变量设置为高级或向它们添加文档字符串。
        可在 https://cmake.org/cmake/help/latest/manual/cmake properties.7.html 中找到按其不同实体排序的受支持属性的完整列表。
        修改属性时最好使用直接函数,例如 set_target_properties 和 set_test_properties,而不是更通用的 set_property 命令。使用显式命令可以避免在属性名称之间出现错误和混淆,并且通常更具可读性。还有 define_property 函数,它创建一个属性而不设置值。我们建议您不要使用它,因为属性应始终具有合理的默认值。

8.Loops and conditions简介 

与任何编程语言一样,CMake 支持条件块和循环块。
条件块位于 if()、elseif()、else() 和 endif() 语句之间。
使用各种关键字来表达条件。

一元关键字在值之前加上前缀,如下所示:

 if(DEFINED MY_VAR)

条件中使用的一元关键字如下:
• COMMAND:如果提供的值是命令,则为真
• EXISTS:检查文件或路径是否存在
• DEFINED:如果值是定义的变量,则为真
此外,还有一元文件系统条件:
• EXISTS:如果传递的文件或目录退出,则为真
• IS_DIRECTORY:检查提供的路径是否为目录
• IS_SYMLINK:如果提供的路径是符号链接,则为真
• IS_ABSOULTE:检查提供的路径是否为绝对路径
二进制测试比较两个值并放置在要比较的值之间,如下所示:

if(MYVAR STREQUAL "FOO")

 二元运算符如下:
• LESS、GREATER、EQUAL、LESS_EQUAL 和 GREATER_EQUAL:它们比较数值。
• STRLESS、STREQUAL、STRGREATER、STRLESS_EQUAL 和 STRGREATER_
EQUAL:这些按字典顺序比较字符串。
• VERSION_LESS、VERSION_EQUAL、VERSION_GREATER、VERSION_LESS_
EQUAL 和 VERSION_GREATER_EQUAL:这些比较版本字符串。
• MATCHES:这与正则表达式进行比较。
• IS_NEWER_THAN:检查通过的两个文件中的哪一个
最近修改。
• IS_NEWER_THAN:不幸的是,这不是很精确,因为如果两个文件具有相同的时间戳,它也会返回 true。还有更多的混乱,因为如果缺少任何一个文件,结果也是正确的。
最后,还有布尔 OR、AND 和 NOT 运算符。
循环由 while() 和 endwhile() 或 foreach() 和 endforeach() 实现:        

可以使用 break() 终止循环; continue() 中止当前迭代并立即开始下一个迭代。
while 循环采用与 if 语句相同的条件。只要 MYVAR 小于 5,以下示例就会循环。请注意,为了增加变量,我们使用了 math() 函数:

 set(MYVAR 0)
while(MYVAR LESS "5") 
  message(STATUS "Chapter1: MYVAR is '${MYVAR}'")
  math(EXPR MYVAR "${MYVAR}+1") 
endwhile()

除了 while 循环,CMake 还知道用于迭代列表或范围的循环:

foreach(ITEM IN LISTS MYLIST)
# do something with ${ITEM}
endforeach()

可以使用 RANGE 关键字创建特定范围内的 for 循环:

foreach(ITEM RANGE 0 10)
# do something with ${ITEM}
endforeach()

虽然 foreach() 的 RANGE 版本只能使用停止变量,但最好始终指定开始值和结束值。

9.Functions 

        函数由 function()/endfunction() 定义。函数为变量打开了一个新的作用域,因此在内部定义的所有变量都不能从外部访问,除非将 PARENT_SCOPE 选项传递给 set()。
函数不区分大小写,通过调用函数调用,后跟括号: 

function(foo ARG1)
# do something
endfunction()
# invoke foo with parameter bar
foo("bar")

        函数是使部分 CMake 可重用的好方法,并且在您处理大型项目时通常会派上用场。

10.Macros 

         CMake 宏是使用 macro()/endmacro() 命令定义的。它们有点像函数,不同之处在于在函数中,参数是真正的变量,而在宏中,它们是字符串替换。这意味着必须使用大括号访问宏的所有参数。
        另一个区别是,通过调用函数,控制权转移到了函数。宏的执行就像宏的主体已粘贴到调用状态的位置一样。这意味着宏不会创建有关变量和控制流的范围。因此,强烈建议避免在宏中调用 return(),因为这会阻止作用域在调用宏的位置执行。

11. 生成器表达式(Generator expressions)

        CMake 的构建系统被组织为一组逻辑目标,这些目标对应于可执行文件、库或自定义命令或工件,例如文档或类似文件。
        在 CMake 中创建目标的主要方法有三种:add_executablea​​dd_libraryadd_custom_target

        前两个用于创建可执行文件和静态或共享库,而第三个可以包含几乎任何要执行的自定义命令。
        可以使目标相互依赖,以便必须先构建一个目标。
        在为构建配置或编译器选项设置属性时,使用目标而不是全局变量是一种很好的做法。一些目标属性具有可见性修饰符,例如 PRIVATE、PUBLIC 或 INTERFACE,以表示哪些要求是可传递的——也就是说,哪些属性必须由依赖目标“继承”。 

        生成器表达式是在构建的配置阶段评估的小语句。大多数函数都允许使用生成器表达式,但也有少数例外。它们采用 $<OPERATOR:VALUE> 的形式,其中 OPERATOR 被应用或与 VALUE 进行比较。您可以将生成器表达式视为小的内联 if 语句。
        在以下示例中,如果编译器是 GCC、Clang 或 Apple Clang,则使用生成器表达式为 my_target 启用 –Wallcompiler 标志。
        请注意,GCC 被标识为 COMPILER_ID "GNU": 

target_compile_options(my_target PRIVATE 
  "$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall>")

        此示例告诉 CMake 将 CXX_COMPILER_ID 变量评估为逗号分隔的 GNU、Clang、AppleClang 列表,如果匹配,则将 -Walloption 附加到目标 - 即 my_target。生成器表达式对于编写独立于平台和编译器的 CMake 文件非常方便。
除了查询值之外,生成器表达式还可用于转换字符串和列表:

        

$<LOWER_CASE:CMake>

        这将输出 cmake。
您可以在 https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html 了解有关生成器表达式的更多信息。
        由于 CMake 支持多种构建系统、编译器和链接器,因此通常用于为不同平台构建软件。在下一节中,我们将学习如何告诉 CMake 使用哪个工具链以及如何配置不同的构建类型,例如调试或发布。

11. CMake policies

        对于顶级 CMakeLists.txt 文件,必须在调用项目之前调用 cmake_minimum_required,因为它还设置了用于构建项目的 CMake 内部策略。
        策略用于保持跨多个 CMake 版本的向后兼容性。它们可以配置为使用 OLD 行为,这意味着 cmake 的行为向后兼容,或者配置为 NEW,这意味着新策略生效。由于每个新版本都会引入新的规则和功能,因此将使用策略来警告您向后兼容性问题。可以使用 cmake_policy 调用禁用或启用策略。
        在以下示例中,CMP0121 策略已设置为向后兼容(backward-compatible)的值。 CMP0121 是在 CMake 版本 3.21 中引入的,用于检查 list() 命令的索引变量是否采用有效格式——即它们是否为整数(数组索引)

cmake_minimum_required(VERSION 3.21)
cmake_policy(SET CMP0121 OLD)
list(APPEND MYLIST "abc;def;ghi")
list(GET MYLIST "any" OUT_VAR)

        通过设置 cmake_policy(SET CMP0121 OLD),程序向后兼容性并且不会产生警告,尽管使用“any”索引访问 MYLIST并且它不是整数。

        若将参数设置为 NEW 会在 CMake 的配置步骤中引发错误 - [build] list index: any is not a valid index。

        到目前为止,我们已经介绍了用于配置构建系统的 CMake 语言背后的基本概念。 CMake 用于为不同类型的构建和语言生成构建指令。在下一节中,我们将学习如何指定要使用的编译器以及如何配置构建。

**避免设置单一策略,除非您包含遗留项目:
通常,应该通过设置 cmake_minimum_required 命令而不是通过更改单个策略来控制策略。更改单个策略的最常见用例是将旧项目包含为子文件夹的时候。

1.1.9 不同的工具链和构建类型(Different toolchains and build types)

        CMake 的强大之处在于您可以为各种编译器工具链使用相同的构建规范(即 CMakeLists.txt),而无需重写任何内容。工具链通常由一系列程序组成,这些程序可以编译和链接二进制文件,以及创建档案等。
        CMake 支持可以配置工具链的多种语言。在本书中,我们将重点关注 C++。通过将以下变量的 CXX 部分替换为相应的语言标签来为不同的编程语言配置工具链:

• C

• CXX – C++

• CUDA

• OBJC – Objective C

• OBJCXX – Objective C++

• Fortran

• HIP – HIP C++ runtime API for NVIDIA and AMD GPUs

• ISPC – C-based SPMD programming language

• ASM – Assembler

        如果项目未指定其语言,则假定正在使用 C 和 CXX。
        CMake 将通过检查系统自动检测要使用的工具链,但如果需要,可以通过环境变量进行配置,或者在交叉编译的情况下,通过提供工具链文件来配置。此工具链存储在缓存中,因此如果工具链发生更改,则必须删除并重建缓存。如果安装了多个编译器,您可以通过在调用 CMake 之前将环境变量设置为 C 的 CC 或 C++ 编译器的 CXX 来指定非默认编译器。在这里,我们使用 CXX 环境变量来覆盖 CMake 中使用的默认编译器:

CXX=g++-7 cmake /path/to/the/source

        或者,您可以通过使用 -D 传递相应的 cmake 变量来覆盖要使用的 C++ 编译器,如下所示:

cmake -D CMAKE_CXX_COMPILER=g++-7 /path/to/source

        这两种方法都确保 CMake 使用 GCC 版本 7 来构建,而不是系统中可用的任何默认编译器。避免在 CMakeLists.txt 文件中设置编译器工具链,因为这与声明 CMake 文件应该与平台和编译器无关的范例相冲突。
        默认情况下,链接器由所选编译器自动选择,但可以通过使用 CMAKE_CXX_LINKER 变量将路径传递给链接器可执行文件来选择不同的链接器。

1.1.10  构建类型(Build types)

        在构建 C++ 应用程序时,具有各种构建类型是很常见的,例如包含所有调试符号的调试构建和优化的发布构建。
CMake 原生提供了四种构建类型:
• 调试(Debug):这是未优化的,包含所有调试符号。在这里,所有的断言都被启用。这与为 GCC 和 Clang 设置 -O0 -g 相同。
• 发布(Release):针对速度进行了优化,无需调试符号和断言禁用。通常,这是交付的构建类型。这与 -O3 -DNDEBUG 相同。
• RelWithDebInfo:这提供优化的代码并包括调试符号但禁用断言,这与-O2 -g -DNDEBUG 相同。
• MinSizeRel:这与Release 相同,但针对较小的二进制大小而不是速度进行了优化,即-Os -DNDEBUG。请注意,并非所有平台上的所有生成器都支持此配置。
        请注意,构建类型必须在配置状态期间传递,并且仅与 CMake 或 Ninja 等单目标生成器相关。对于 MSVC 等多目标生成器,不使用它们,因为构建系统本身可以构建所有构建类型。可以创建自定义构建类型,但由于它们不适用于每个生成器,因此通常不鼓励。
        由于 CMake 支持如此广泛的工具链、生成器和语言,一个常见的问题是如何找到和维护这些选项的工作组合。
        在这里,预设可以提供帮助。

1.1.11 使用预设维护良好的构建配置(Maintaining good build configurations with presets)

        使用 CMake 构建软件时的一个常见问题是如何共享良好或有效的配置来构建项目。通常,人们和团队对构建工件应该去哪里、在哪个平台上使用哪个生成器、或者只是希望 CI 环境应该使用与本地相同的设置进行构建有一个首选的方式。由于 CMake 3.19 于 2020 年 12 月问世,这些信息可以存储在 CMakePresets.json 文件中,这些文件放置在项目的根目录中。此外,每个用户都可以使用 CMakeUserPresets.json 文件叠加他们的配置。基本预设通常置于版本控制之下,但用户预设不会检入版本系统。两个文件都遵循相同的 JSON 格式,顶层大纲如下:

{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [...],
"buildPresets": [...],
"testPresets": [...]
}

1. 第一行,“version”:3 表示 JSON 文件的 schema 版本。 CMake 3.21 支持最高版本 3,但预计新版本将带来新版本的架构。
2. 接下来,cmakeMinimumRequired{...} 指定使用哪个版本的 CMake。尽管这是可选的,但最好将其放在此处并将版本与 CMakeLists.txt 文件中指定的版本匹配。
3. 可以使用 configurePresets、buildPresets 和 testPresets 添加不同构建阶段的各种预设。顾名思义,configurePresets 适用于 CMake 构建过程的配置阶段,而另外两个用于构建和测试阶段。构建和测试预设可以继承一个或多个配置预设。如果未指定继承,则它们适用于前面的所有步骤。

        要查看项目中配置了哪些预设,请运行 cmake --list-presets 以查看可用预设的列表。要使用预设进行构建,请运行 cmake --build --preset name。
        要查看 JSON 模式的完整规范,请访问                 https://cmake.org/cmake/help/v3.21/manual/cmake-presets.7.html。
        预设是一种以非常明确的方式分享有关如何构建项目的知识的好方法。在撰写本文时,越来越多的 IDE 和编辑器正在原生添加对 CMake 预设的支持,尤其是在处理与工具链的交叉编译方面。在这里,我们只为您提供 CMake 预设的最简要概述,我们将在第 12 章“跨平台编译和自定义工具链”中更深入地介绍.

1.1.12 本章小结及拓展 (Summary&Further reading)

1.本章小结

        在本章中,您获得了 CMake 的简要概述。首先,您学习了如何安装和运行一个简单的构建。然后,您了解了 CMake 的两阶段构建过程,然后再接触编写 CMake 文件的最重要的语言特性。
        到目前为止,您应该能够构建本教程的 GitHub 存储库中提供的示例:https://github.com/PacktPublishing/CMake-Best-Practices。

        您了解了 CMake 语言的核心功能,例如变量、目标和策略。我们简要介绍了函数和宏,以及用于流控制的条件语句和循环。随着您继续阅读本文,您将使用目前所学的知识来发现更多的良好实践和技术,以从简单的单一目标项目转移到通过良好的 CMake 设置保持可维护的复杂软件项目。
        在下一章中,我们将学习如何执行 CMake 中一些最常见的任务,以及 CMake 如何与各种 IDE 协同工作。

2.拓展

要了解有关本章所涵盖主题的更多信息,请查看以下资源:
• 官方 CMake 文档:https://cmake.org/cmake/help/latest/
• 官方 CMake 教程:https://cmake.org/cmake/help/latest/guide/tutorial/index.html 

3.问题回顾

 回答以下问题以测试您对本章的了解:
1.如何开始CMake的配置步骤? cmake -S /sourcefolder -B /buildfolder
2.如何开始 CMake 的构建步骤? cmake --build /buildfolder
3.CMake 中的哪个可执行文件可用于运行测试? bin下的二进制文件
4.CMake 中的哪个可执行文件用于打包? cmakeconfig?
5.CMake 中的目标是什么?没明白.....
6.属性和变量有什​​么区别?
7.CMake 预设有什么用途?协同

1.2   在命令行、ccmake、cmakeGUI、clion、QT creator、VScode使用Cmake进行项目配置、构建与运行(Use Cmake on the command line, ccmake, cmakeGUI, clion, QT creator, and VScode to configure, build, and run projects)

1.2.0 本章引言(Intro)

1.本章介绍

        在上一章中,我们熟悉了 CMake,了解了 CMake 的基本概念。现在,我们将学习如何与 CMake 交互。我们将通过学习配置、构建和安装现有项目来做到这一点。这将使您能够与 CMake 项目进行交互。
        本章将研究 CMake 作为接口提供的功能,并检查一些流行的 IDE 和编辑器集成。本章将涵盖以下内容:
• 通过命令行界面使用 CMake
• 使用 cmake-gui 和 ccmake 接口
• IDE 和编辑器集成(Visual Studio、Visual Studio Code CLion 和 Qt Creator)
由于我们有很多内容要介绍,所以我们不要浪费任何时间并开始了解技术要求

2.技术要求

在进一步详细介绍之前,需要满足一些要求才能遵循示例:
• 本教程 Git 存储库:这是包含本教程所有示例性内容的主存储库。
https://github.com/PacktPublishing/CMake-Best-Practices

1.2.1 通过命令行界面使用 CMake(Using CMake via a command-line interface)

1.验证cmake正确安装

        使用 CMake 的最常见方式是通过命令行界面 (CLI)。 CLI 几乎存在于每个环境中,因此,学习在 CLI 中使用 CMake 是必不可少的。在本节中,我们将学习如何使用 CLI 执行最基本的 CMake 操作。
        假设已安装 CMake 并且 cmake 可执行文件包含在系统的 PATH 变量(或等效变量)中,则可以通过在操作系统的终端中发出 cmake 命令来完成与 CMake CLI 的交互。您可以通过在终端中发出不带任何参数的 cmake 来验证这一点,如下图所示:


         如果您的终端缺少命令,那么您应该安装 CMake(在第 1 章 Kickstarting CMake 中进行了解释)或通过将其添加到系统的 PATH 变量中使其可被发现。请参阅您的操作系统指南,了解如何向系统的 PATH 变量添加路径。

        安装 CMake 并将其添加到 PATH 变量(如果需要)后,您应该测试 CMake 是否可用。您可以在命令行中执行的最基本的命令是 cmake --version,它允许您检查 CMake 的版本。

         CMake 会以 cmake version <maj.min.rev> 的形式输出一个版本字符串。
您应该会看到一个输出,其中包含您在系统上安装的 CMake 的版本号。

        安装 CMake 后,您还应该安装构建系统和编译器。对于类似 Debian 的操作系统(例如,Debian 和 Ubuntu),这可以通过以下命令完成!

 sudo apt install build-essential

        发出 命令轻松完成。这个包本质上包含 gcc、g++ 和 make。


        CLI 的使用将在 Ubuntu 20.04 环境中进行说明。除了较小的边缘情况外,在其他环境中的用法也相同。随着我们继续,这些边缘情况将被提及。

1.2.2 配置CMake项目(Configuring a project via the CLI)

本节将学习有关使用 CMake CLI 应了解的三项基本知识:
• 配置 CMake 项目
• 构建 CMake 项目
• 安装 CMake 项目
学习基础知识后,您将能够构建和安装您选择的任何 CMake 项目。让我们开始配置。 

1.配置CMake项目的语法基础 

Ⅰ.基本语法

        要通过命令行配置 CMake 项目,您可以使用如下代码构造。(这一步是配置config,回想第一章,如果要得到可执行文件还需要构建build!)

cmake -G "Unix Makefiles" -S <project_root> -B <output_directory>

        -S 参数用于指定要配置的 CMake 项目,而 -B 指定配置输出目录。最后,-G 参数允许我们指定将用于生成构建系统的生成器。配置过程的结果将被写入 <output_directory>。

Ⅱ.举例:如何利用上述命令在当前目录下(chapter1)构建项目
        作为说明,让我们在项目根构建目录中配置我们的示例项目:

        我们从桌面开始,配置build目录,举两个例子:

        首先先看以下我们的文件结构:

        现在进入 CMake-Best-Practices-main 目录下的chapter1并发出 cmake -G "Unix Makefiles" -S .  -B ./build,如下图所示:

        这个命令就像是对 CMake 说,使用“Unix Makefiles”(-G“Unix Makefiles”)生成器为当前目录(-S .)中的 CMake 项目生成构建系统(-B ./build)目录。


        CMake 将配置位于 build 文件夹中当前文件夹中的项目。由于我们省略了构建类型,CMake 使用了 Debug 构建类型(项目的默认 CMAKE_BUILD_TYPE 值)。

        我们执行完这行命令,发现可以在chapter01目录下找到build目录,构建成功!


        在后续部分中,我们将了解配置步骤中使用的基本设置。

Ⅲ.举例:如何利用上述命令在桌面构建项目

 

2.更改构建类型(Changing the build type)

        默认情况下,CMake 不假定任何构建类型。为了设置构建类型,必须向配置命令提供一个名为 CMAKE_BUILD_TYPE 的附加变量。要提供其他变量,变量必须以 -D 为前缀。
要获取 Release 版本而不是 Debug,请在配置命令中添加 CMAKE_BUILD_TYPE 变量,如前所述:     

cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING=Release -S . -B ./

来构建。
        CMAKE_BUILD_TYPE 变量仅对单一配置生成器有意义,例如 Unix Makefiles 和 Ninja。

        在 Visual Studio 等多配置生成器中,构建类型是构建时参数而不是配置时参数,因此无法使用 CMAKE_BUILD_TYPE 参数进行配置。

3.更改生成器类型(Changing the generator type)

        根据构建环境,CMake 默认尝试选择合适的生成器。要显式指定生成器,必须为 -G 参数提供有效的生成器名称。例如,如果你想使用 Ninja 作为构建系统而不是 make,你可以将其更改如下:

cmake -G "Ninja" -DCMAKE_BUILD_TYPE:STRING=Debug -S . -B ./build

        输出应该类似于下图所示的命令输出:

         这将导致 CMake 生成 Ninja 构建文件而不是 makefile。
        要查看适用于您的环境的所有可用生成器类型,请发出 cmake --help 命令。可用的生成器将列在帮助文本生成器部分的末尾,如下所示:

         旁边有一个星号的生成器是您当前所在环境的默认值。

4.更改编译器(Changing the compiler)

        在 CMake 中,要使用的编译器是通过 CMAKE_<LANG>_COMPILER 变量在每种语言的基础上指定的。为了更改语言的编译器,必须将 CMAKE_<LANG>_COMPILER 提供给 Configure 命令。对于 C/C++ 项目,通常被覆盖的变量是 CMAKE_C_COMPILER(C 编译器)和 CMAKE_CXX_COMPILER(C++ 编译器)。

        编译器标志同样由 CMAKE_<LANG>_FLAGS 变量控制。此变量可用于保存与配置无关的编译器标志。
        例如,让我们尝试在一个不是默认编译器的环境中使用 g++-10 作为 C++ 编译器:

cmake -G "Unix Makefiles" -DCMAKE_CXX_COMPILER=/usr/bin/g++-10 -S . -B ./build

        在这里,我们可以看到使用 g++-10 代替了系统默认的编译器 g++-9:

         如果没有编译器规范,CMake 更喜欢在这个环境中使用 g++-9:

5.将标志传递给编译器(Passing flags to the compiler) 

        为了说明如何指定编译器标志,假设您要启用所有警告并将它们变成错误(warnings变成errors)。这些行为分别由 gcc 工具链中的 -Wall 和 -Werrorcompiler 标志控制;因此,我们需要将这些标志传递给 C++ 编译器。以下代码指定了如何执行此操作:

        这个有什么用呢:将代码中所有warning变成error,提高代码规范

cmake -G "Unix Makefiles" -DCMAKE_CXX_FLAGS:STRING="-Wall -Werror" -DMAKE_BUILD_TYPE:STRING=DEBUG -S . -B build

我们可以看到命令中指定的标志(-Wall 和 -Werror)在以下示例中被传递到编译器:

         可以通过使用大写的构建类型字符串作为后缀来为每个构建类型自定义构建标志。四种不同的构建类型有四个变量,如下所列。它们对于根据编译器标志指定构建类型很有用。此类变量中指定的标志仅在配置构建类型匹配时有效:
• CMAKE_<LANG>_FLAGS_DEBUG
• CMAKE_<LANG>_FLAGS_RELEASE
• CMAKE_<LANG>_FLAGS_RELWITHDEBINFO
• CMAKE_<LANG>_FLAGS_MINSIZEREL
        除了前面的示例,如果您只想在发布版本中将警告视为错误,则特定于构建类型的编译器标志允许您这样做。

         这是一个示例,说明了特定于构建类型的编译器标志的用法:

cmake -G "Unix Makefiles" -DCMAKE_CXX_FLAGS:STRING="-Wall -Werror" -DCMAKE_CXX_FLAGS_RELEASE:STRING="-O3" -DCMAKE_BUILD_TYPE:STRING=Debug -S . -B ./build

        请注意,前面的命令中存在一个额外的 CMAKE_CXX_FLAGS_RELEASE 参数。只有当构建类型为 Release 时,此变量中的内容才会传递给编译器。由于构建类型指定为 Debug,我们可以看到传递给编译器的标志中不存在 -O3 标志,如下图所示:

        请注意 CMake 正在发出有关指定但未使用的变CMAKE_CXX_FLAGS_RELEASE 的警告。这确认 CMAKE_CXX_FLAGS_RELEASE 变量未在 Debug 构建类型中使用。当构建类型指定为 Release 时,我们可以看到存在 -O3 标志:

cmake -G "Unix Makefiles" -DCMAKE_CXX_FLAGS:STRING="-Wall -Werror" -DCMAKE_CXX_FLAGS_RELEASE:STRING="-O3"-DCMAKE_BUILD_TYPE:STRING= "Release" -S . -B ./build

        在这一行中,您对 CMake 说,使用“Unix Makefiles”生成器将位于当前目录的 CMake 项目配置为构建/文件夹。对于所有构建类型,无条件地将 -Wall 标志传递给编译器。如果构建类型是 Release,也传递 -O3 标志。

        以下是构建类型设置为 Release 时的命令输出:

         在上图中,我们可以确认 -O3 标志也被传递给编译器。请注意,即使 RelWithDebInfo 和 MinSizeRel 也是发布版本,它们与发布版本类型是分开的,因此 CMAKE_<LANG>_FLAGS_RELEASE 变量中指定的标志将不适用于它们。

6.列出缓存的变量(Listing cached variables) 

        可以通过发出 cmake -L ./build/ 命令列出所有缓存的变量。默认情况下,这不显示与每个变量关联的高级变量和帮助字符串。要同时显示它们,请改用

cmake -LAH ./build/command

1.2.3 通过 CLI 构建配置的项目(Building a configured project via CLI)

        要构建配置的项目,请发出 cmake --build ./build 命令。
        此命令告诉 DCMake 构建已在构建文件夹中配置的 CMake 项目。
您也可以等效地发出 cd build && make。使用 cmake --build 的好处是它使您免于调用特定于构建系统的命令。在构建 CI 管道或构建脚本时特别有用。通过这种方式,您可以在不更改构建命令的情况下更改构建系统生成器。
        可以在以下示例中看到 cmake --build ./build 命令的示例输出:

1. 并行建设(Building in parallel)

        还可以在发出构建命令时自定义构建时间详细信息。最突出的构建时间配置是将用于构建项目的作业数量。要指定作业计数,请将 --parallel <job_count> 附加到您的 cmake --build 命令。
        要并行构建,请发出 cmake --build ./build --parallel 2,其中数字 2 指定作业计数。构建系统的推荐作业数量最多为每个硬件线程一个作业。在多核系统中,还建议使用至少比可用硬件线程数少一个,以免在构建过程中影响系统的响应性。


        通常可以为每个硬件线程使用多个作业并获得更快的构建时间,因为构建过程主要受 I/O 限制。
        最好不要在预期在不同环境中调用的命令(例如 CI/CD 脚本和构建脚本)中为环境相关变量使用固定值。这是一个示例构建命令,它利用 nproc 动态确定并行作业的数量:

cmake --build ./build/ --parallel $(($(nproc)-1))

        让我们观察不同的作业计数如何影响构建时间。我们将使用时间工具来测量每个命令调用的时间。环境详情如下:

• OS: Ubuntu 20.04.3 LTS

• CPU: AMD® Ryzen 7 5800h with radeon graphics × 16 

• RAM: 32 GB

对于一项工作(--parallel 1),构建时间结果如下:

 具有两个作业(--parallel 2)的构建时间结果如下:

 三个作业(--parallel 3)的构建时间结果如下:

 这是具有四个作业(--parallel 4)的构建时间结果:

         即使在一个非常简单的项目中调用,我们也可以清楚地看到额外的作业如何帮助获得更快的构建时间。从一个作业到两个作业将构建时间减少了 0.3 秒,而从两个作业到三个作业给了我们额外的 0.2 秒。但是,从三个作业变为四个作业仅相差 0.01 秒,这意味着我们已经达到了该项目的构建并行度的极限。从这一点开始,抛出更多作业不会在构建时间上实现显着差异。

2. 仅构建特定目标(Building specific target(s) only) 

        默认情况下,CMake 将构建所有配置的可用目标。由于构建所有目标并不总是可取的,因此 CMake 允许通过 --target 子选项构建目标子集。该子选项可以指定多次,如下所示:

cmake --build ./build/ --target "ch2_framework_component1" --target "ch2_framework_component2"

      

        此命令会将构建范围限制为仅 ch2_framework_component1 和 ch2_framework_component2 目标。如果这些目标还依赖于其他目标,它们也将被构建。

        我们再构建剩下的两个!

3.在构建之前删除以前的构建工件(Removing previous build artifacts before the build)

         如果您想运行一个干净的构建,您可能需要先从上一次运行中删除工件。为此,可以使用 --clean-first 子选项。此子选项将调用一个特殊目标,该目标清除由构建过程生成的所有工件(例如,调用 make clean)。
        这是一个示例,说明如何为名为 build 的构建文件夹执行此操作:

cmake --build ./build/ --clean-first

         执行过后我们发现他的build文件夹还在。

4.调试你的构建过程(Debugging your build process) 

        正如我们之前在将标志传递给编译器部分所做的那样,您可能想要检查在构建过程中使用哪些参数调用了哪些命令。 --verbosesub-command 指示 CMake 以详细模式调用所有构建命令,因为该命令支持详细模式。这使我们能够轻松地调查令人讨厌的编译和链接错误。
要以详细模式构建名为 build 的文件夹,请调用 --build,如以下示例所示:

cmake --build ./build/ --verbose

5.将命令行参数传递给构建工具(Passing command-line arguments to the build tool)

         如果您需要将参数传递给底层构建工具,您可以在命令末尾附加 -- 并编写将给出的参数:

cmake --build ./build/ -- --trace  

        在前面的例子中,--trace 将被直接转发到构建工具,在我们的例子中是 make。这将导致 make 打印每个构建的配方的跟踪信息。

6.通过 CLI 安装项目(Installing a project via the CLI) 

        如果需要,CMake 本机允许在环境中安装工件。为了做到这一点,CMake 代码必须已经使用 CMake install() 指令来指定在调用 cmake --install (或构建系统等效项)时要安装的内容。 chaptert_2 的内容已经以这种方式配置,用于说明命令。
        我们将在后面的第 4 章“打包、部署和安装 CMake 项目”中学习如何使 CMake 目标可安装。
        cmake --install 命令需要一个已经配置和构建的项目。如果您还没有完成,请配置并构建 CMake 项目。之后,发出 cmake --install <project_binary_dir> 命令来安装 CMake 项目。因为在我们的示例中,build 被用作项目二进制目录,所以 <project_binary_dir> 将被替换为 build。
        下图显示了安装命令的示例:

sudo cmake --install build

        默认安装目录因环境而异。对于类 Unix 环境,它默认为 /usr/local,而在 Windows 环境中,它默认为 C:/Program Files。

        在尝试安装项目之前,该项目必须已经构建。为了能够成功安装项目,您必须具有适当的权限/权限才能写入安装目标目录。

7. 更改默认安装路径(Changing the default installation path)

        要更改默认安装目录,您可以指定额外的 --prefix 参数,如下所示,以更改安装目录:

cmake --install build --prefix /tmp/example

        下图显示了使用 /tmp/example 前缀调用 cmake --install 后 /tmp/example 文件夹的内容:

  

         从这里可以看出,安装根目录成功更改为桌面。

8.安装时剥离二进制文件(Stripping binaries while installing) 

        在软件世界中,构建工件通常与一些额外的信息捆绑在一起,例如,调试所需的符号表。此信息对于执行最终产品可能不是必需的,并且可能会大大增加二进制文件的大小。如果您希望减少最终产品的存储空间,剥离二进制文件可能是一个不错的选择。
        剥离的另一个好处是,由于从二进制文件中剥离了基本的符号信息,因此对二进制文件进行逆向工程变得更加困难。

        CMake 的 --install 命令允许在安装操作时剥离二进制文件。它可以通过在 --install 命令中指定一个额外的 --strip 选项来启用,如下所示:

cmake --install build --strip

        在以下示例中,您可以观察到未剥离和剥离的二进制文件之间的大小差异。请注意,剥离静态库有其自身的限制,CMake 默认不执行它。您可以在此图中看到未剥离的二进制文件的大小:

         使用剥离的 (cmake –install build --strip) 二进制文件,大小差异如下图所示:

9.仅安装特定组件(基于组件的安装)(Installing specific components only (component-based install)) 

        如果项目在 install() 命令中使用 CMake 的 COMPONENT 功能,您可以通过指定组件名称来安装特定组件。 COMPONENT 功能允许将安装分成多个子部分。为了说明此功能,chapter_2 示例被构造为两个组件,分别名为库和可执行文件。
        为了安装特定组件,需要一个额外的 --component 参数以及 cmake --install 命令:

cmake --install build --component executables

        这是一个示例调用:

10.安装特定配置(仅适用于多配置生成器)(Installing a specific configuration (for multiple-configuration generators only))

        一些生成器支持同一构建配置的多个配置(例如,Visual Studio)。对于那种生成器, --install 选项提供了一个额外的 --config 参数来指定要安装的二进制文件的配置。
这是一个例子:

cmake --install build --config Debug

        可能已经注意到,示例中使用的命令参数非常长且明确。这是故意的。显式指定参数允许我们在每次运行中获得一致的结果,无论我们在哪个环境中运行我们的命令。例如,如果没有 -G 参数,CMake 将默认为环境的首选构建系统生成器,这可能不是我们的意图。我们的座右铭是,明确几乎总是比含蓄好。前者使我们的意图更清晰,自然也可以在 CI 系统/脚本中启用更多面向未来和可维护的 CMake 代码。

        我们已经介绍了 CMake 命令行使用的基础知识。让我们继续了解另一种可用的界面形式——CMake的图形界面。

1.2.4  使用 CMake-GUI 和 ccmake 进行高级配置(Advanced configuration using CMake-GUI and ccmake)

        尽管外观不同,但大多数界面倾向于做同样的事情;因此,我们在上一节中已经介绍的大部分内容在这里也是有效的。
        请记住,我们将改变我们的交互形式,而不是我们实际交互的工具。

        在继续之前,请检查 ccmake 命令是否在您的终端中可用。如果没有,请验证您的 PATH 变量设置是否正确并检查您的安装。

1.学习如何使用 ccmake (CMake curses GUI) (Learning how to use ccmake (CMake curses GUI))

        ccmake 是一个基于终端的 CMake 图形用户界面 (GUI),它允许用户编辑缓存的 CMake 变量。与其称它为 GUI,术语终端用户界面 (TUI) 可能更适合,因为没有传统的 shell UI 元素,例如窗口和按钮。这些元素使用名为 ncurses 的基于文本的界面框架在终端中呈现。
        由于 ccmake 是默认 CMake 安装的一部分,因此除了 CMake 之外不需要额外安装。使用 ccmake 与在 CLI 中使用 CMake 完全相同,只是它缺少调用构建和安装步骤的能力。主要区别在于 ccmake 将显示一个基于终端的图形界面,用于交互式地编辑缓存的 CMake 变量。当您尝试设置时,这是一个方便的工具。 ccmake 的状态栏将显示每个设置及其可能值的描述。
        要开始使用 ccmake,请在项目配置步骤中使用 ccmake 而不是 cmake。在我们的示例中,我们将完全复制我们之前在通过 CLI 配置项目部分中所做的 CLI 示例:

ccmake -G "Unix Makefiles" -S . -B ./build

        以下显示了上述命令的示例输出:

        运行命令后,将出现基于终端的 UI。初始页面是可以编辑 CMake 变量的主页面。 EMPTY CACHE 表示之前没有进行任何配置,并且 CMake 缓存文件 (CMakeCache.txt) 当前为空。为了开始编辑变量,必须首先配置项目。要进行配置,请按键盘上的 C 键,如 Keys: 部分所示。
        按 C 键后,将执行 CMake 配置步骤,并显示日志输出屏幕和配置输出:    

     

        要关闭日志输出屏幕并返回主屏幕,请按 E。返回后,您会注意到 EMPTY CACHE 已替换为 CMakeCache.txt 文件中的变量名称。

        要选择变量,请使用键盘上的向上和向下箭头键。当前选择的变量将以白色突出显示,如上图所示

        在前面的屏幕截图中,选择了 CMAKE_BUILD_TYPE 变量。在右侧,显示了 CMake 变量的当前值。对于 CMAKE_BUILD_TYPE,它现在是空的。变量值旁边的星号表示该变量的值刚刚随着之前的配置而改变。您可以通过按 Enter 键对其进行编辑,也可以通过按键盘上的 D 键将其删除。下图显示了更改变量后的 ccmake 主屏幕的样子:

        先按[enter]更改,然后输入release,再按[enter]确认:

         让我们将 CMAKE_BUILD_TYPE 设置为 Release 并再次配置:

         我们可以观察到构建类型现在设置为 Release。返回上一屏幕并按 g生成(generate)按钮保存更改。可以通过按 q(退出而不生成)按钮放弃更改。


        要编辑其他变量,例如 CMAKE_CXX_COMPILER 和 CMAKE_CXX_FLAGS,应打开高级模式。默认情况下,这些变量通过调用 mark_as_advanced() CMake 函数被标记为高级标志;因此,它们默认隐藏在图形界面上。在主屏幕上,按 t 切换到高级模式。

 

         激活高级模式后,可以看到一组全新的选项。您可以像普通变量一样观察和更改它们的值。您可能已经注意到以前隐藏的名为 CHAPTER2_BUILD_DRIVER_APPLICATION 的变量现在出现了。

        这是一个用户定义的 CMake 变量。该变量定义如下:

# Option to exclude driver application from build.
set(CHAPTER2_BUILD_DRIVER_APPLICATION TRUE CACHE BOOL "Whether 
to include driver application in build. Default: True")
# Hide this option from GUI's by default.
mark_as_advanced(CHAPTER2_BUILD_DRIVER_APPLICATION)

        CHAPTER2_BUILD_DRIVER_APPLICATION 变量定义为布尔类型的缓存变量,默认值为 true。它被标记为高级,这就是它在非高级模式中不存在的原因。

 2.通过 cmake-gui 使用 CMake(Using CMake via cmake-gui)

        如果不喜欢命令行,或者你更喜欢 GUI 而不是 CLI,那么 CMake 也有一个跨平台的 GUI。与 ccmake 相比,cmake-gui 提供了更多功能,例如环境编辑器和正则表达式资源管理器。
        CMake GUI 是默认 CMake 安装的一部分;除了 CMake,不需要额外的安装。其主要目的是允许用户配置 CMake 项目。要启动 cmake-gui,请在终端中发出 cmake-gui 命令。对于 Windows,它也可以从开始菜单中找到。如果这些方法都不起作用,请进入您的 CMake 安装路径,它应该存在于 bin\ 目录中。

        如果您在 Windows 环境中启动 cmake-gui 并打算使用 Visual Studio 提供的工具链,请从 IDE 的相应本机工具命令提示符启动 cmake-gui。如果您有多个版本的 IDE,请确保您使用的是正确的本机工具命令提示符。否则,CMake 可能无法发现所需的工具,例如编译器,或者可能会发现不正确的工具。有关详细信息,请参阅

https://docs.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2019。

        这是 CMake GUI 的主窗口:

 CMake GUI 的主屏幕主要包含以下内容:
• 源代码路径字段
• 输出路径字段
• 配置和生成按钮
• 缓存变量列表

        这是我们将要与之交互的四个基本事物。要开始配置项目,请单击 Browse Source... 按钮选择项目的根目录。
        之后,通过单击 Browse Build... 按钮为项目选择一个输出目录。此路径将是所选生成器生成的输出文件的路径。

        根据您的环境填写这些详细信息后,单击 Finish 继续。
        CMake GUI 将开始使用给定的详细信息配置您的项目,并在日志部分报告输出。成功配置后,您还应该在缓存变量列表部分中看到缓存变量(红色的):

        如果一切似乎都井然有序,请按 Generate 按钮以生成您选择的构建系统所需的构建文件。对于 Visual Studio 生成器,生成的文件是 .sln 和 .cxxproj 以及其他文件。生成项目后,“打开项目”按钮将变为启用状态。它将导致使用适当的编辑器或 IDE 打开生成的项目。如果构建系统未与任何 IDE 关联(例如,makefile),则将显示生成的文件。之后,您可以使用 IDE 构建项目。

        请注意,生成的项目只是生成器的工件,对生成的项目文件(.sln、.cxxproj)的更改将不会保存,并且会在下一代丢失。当您更改 CMakeLists.txt 文件或编辑 CMakeCache.txt 文件(直接或间接)时,不要忘记重新生成项目文件。对于版本控制方面,您应该将生成的项目文件视为构建工件,而不应将它们添加到版本控制中。您始终可以通过 CMake 使用适当的生成器生成项目,从而从头开始获取它们。

        有时,项目可能需要调整一些缓存变量,或者您可能决定使用不同的构建类型,例如。要更改任何缓存变量,请单击所需缓存变量的值;它应该变得可编辑。根据变量类型,可能会显示复选框而不是字符串。如果所需的变量在列表中不可见,则可能是高级变量,只有在选中窗口上的Advanced复选框时才能看到该变量。您还可以使用搜索框更轻松地定位变量。在这里,您可以在高级模式下看到 cmake-gui:

         调整任何缓存值后,单击 Configure ,然后单击 Generate 以应用更改。

        另一个有用的特性是分组特性,它允许将缓存变量分组到它们的公共前缀中(如果有的话)。组名由变量名的第一部分决定,直到第一个下划线。

        我们已经介绍了 cmake-gui 最​​基本的功能。在转向其他杂项之前,如果您需要重新加载缓存值或删除缓存并从头开始,您可以在“文件”菜单中找到“Reload Cache”和“Delete Cache”菜单项。

调整环境变量(Tweaking environment variables )

        CMake GUI 带有一个方便的环境变量编辑器,允许对环境变量进行 CRUD 操作。要访问它,只需单击主屏幕上的 Environment… 按钮。点击后会弹出 Environment Editor 窗口,如下图所示:

         当前环境中存在的环境变量。要编辑环境变量,请双击表中所需环境变量的值字段。该窗口还允许使用添加条目和删除条目按钮添加和删除信息。

使用 CMake 评估正则表达式(Evaluating regular expressions with CMake) 

        你有没有想过 CMake 将如何评估正则表达式以及它会给出什么结果?如果是这样,您之前可能已经通过使用 message() 打印出正则表达式匹配结果变量来手动调试它。如果我说有更好的方法呢?让我向您介绍 CMake GUI 的正则表达式资源管理器工具。

        这个隐藏的 gem 允许您使用 CMake 的正则表达式引擎调试正则表达式。它位于Tools菜单中,名称为Regular Expressions Explorer….。使用它非常简单明了:

1. 在正则表达式字段中输入表达式。该工具将检查表达式是否有效。如果是这样,屏幕上的有效文本将为绿色。如果 CMake 的正则表达式引擎不喜欢您给出的表达式,它将变为红色。
2. 在输入文本字段中输入测试字符串。正则表达式将与此文本匹配。
3. 如果有任何匹配,窗口上的匹配字会由红色变为绿色。匹配的字符串将打印在 Complete Match 字段中。
4. 匹配时,捕获组将分别分配给匹配 1、匹配 2 和匹配 N(如果有)。

1.2.5  在 Visual Studio、Visual Studio Code 、CLion 和 Qt Creator 中使用 CMake(Using CMake in Visual Studio, Visual Studio Code, and Qt Creator)

        作为软件开发中的常用工具,CMake 与各种 IDE 和源代码编辑器集成。在使用 IDE 或编辑器时使用此类集成对用户来说可能更方便。在本节中,我们将介绍 CMake 如何与一些流行的 IDE 和编辑器集成。
        如果您希望获得有关如何使用 IDE 或编辑器的指南,那么本节将不涉及该内容。本节的主要重点是调查和了解 CMake 与此类工具的集成。本部分假设您已经拥有使用要与之交互的 IDE/编辑器的经验。
        让我们从 Visual Studio 开始。

1. Visual Studio

Ⅰ.从头开始一个 CMake 项目 (Starting a CMake project from scratch)

        Visual Studio 项目创建功能基于项目模板。对于 VS2017 及更高版本,项目模板也包含 CMake 项目模板。我们将学习如何使用这个模板来创建新的 CMake 项目。

        要使用 Visual Studio 创建新的 CMake 项目,请单击欢迎页面上的Create a new project按钮。或者,您可以通过在 IDE 主窗口上单击文件 File | New | Project 访问它进行,或使用 Ctrl + Shift + N(新建项目)键盘快捷键。 VS22 欢迎屏幕如下所示:

         在 Create a new project 屏幕上,双击项目模板列表中的 CMake Project。您可以使用位于列表顶部的搜索栏过滤项目模板:

         单击 next 后,将出现项目配置屏幕。在此页面上,您可以为新的 CMake 项目命名并选择放置新项目的位置。在我们的示例中,我们将使用默认项目名称 CMakeProject1

         填写详细信息后,单击 Create 以创建新的 CMake 项目。生成的项目将包含一个顶级 CMakeLists.txt 文件、一个 C++ 源文件和一个 C++ 头文件,以所选项目名称命名。新建项目的布局如下图所示:

Ⅱ. 打开现有的 CMake 项目(Opening an existing CMake project)

        要打开现有的 CMake 项目,请转到 File | Open | CMake 并选择要打开的项目的顶级 CMakeLists.txt 文件。
        打开菜单如下所示:

         接下来,让我们看看如何配置和构建 CMake 项目。

Ⅲ. 配置和构建 CMake 项目(Configuring and building a CMake project)

        要在 Visual Studio 中构建 CMake 项目,请先转到 Project | Configure。这将调用 CMake 配置步骤(configure)并生成所需的构建系统文件。配置完成后,点击Build | Build All 来构建项目。您还可以使用 F7 键盘快捷键触发 Build All
        请注意,每当您保存作为项目一部分的 CMakeLists.txt 文件时,Visual Studio 都会自动调用 configure。

Ⅳ.在 CMake 目标上执行常见操作(Executing common actions on a CMake target) 

        Visual Studio 将启动目标概念用于需要目标的操作,例如生成、调试和启动。要将 CMake 目标设置为启动目标,请使用工具栏上的Select Startup Target下拉框。 Visual Studio 将在配置时使用 Cmake 目标自动填充此下拉框。

         设置启动目标后,您可以调用诸如 Debug、Build 或 Launch 之类的操作,就像在 Visual Studio 中一样:
1. 要调试,首先,单击Debug | Startup Target,然后单击Debug | Start Debugging或使用 F5 键盘快捷键。
2. 要在不调试的情况下启动,请单击 Start without debug 或使用 Ctrl + F5 键盘快捷键。
3. 要构建,请单击Build,或单击 Build | Build <target>,或使用 Ctrl + B 键盘快捷键。
4. 按键位置如下图所示:

2. Visual Studio Code

Ⅰ. 命令面板(Command Palette)

        Visual Studio Code (VSCode) 是微软开发的开源代码编辑器。它不是 IDE,但可以通过扩展变得强大并具有类似 IDE 的功能。扩展市场有各种各样的附加内容,从主题到语言服务器。你可以找到几乎任何东西的扩展,这使得 VSCode 既强大又受到广大观众的喜爱。毫不奇怪,VSCode 也有一个官方的 CMake 扩展。此扩展最初由 Colby Pike(也称为 vector-of-bool)开发,但现在由 Microsoft 正式维护。
        在本节中,我们将学习如何安装扩展并使用它执行基本的 CMake 任务。
        在继续之前,您的环境中必须已经安装了 VSCode。如果没有,请访问了解有关下载和安装它的详细信息。

https://code.visualstudio.com/learn/get-started/basicshttp://xn--vscode-gj7k537u
        此外,我们将经常访问命令面板。强烈建议经常使用它来熟悉它。对于那些询问 Command Palette 到底是什么?,这里有一个截图:

         是的,就是那个东西。老实说,直到现在我才知道它有名字。访问命令面板的快捷键是 F1 和 Ctrl + Shift + P。命令面板是 VSCode 的基础;它加快了 VSCode 的工作流程。

Ⅱ.安装扩展(Installing the extension)

        安装扩展非常简单明了。要使用 CLI 安装它,请调用以下命令(如果您使用的是 Insiders 版本,请将 code 替换为 code-insiders):

code --install-extension ms-vscode.cmake-tools

        或者,您也可以在 VSCode GUI 中执行相同操作。打开 VSCode 并通过单击左侧导航窗格中的 Extensions 导航到 Extensions 页面。或者,您可以使用 Ctrl + Shift + X 快捷键。在扩展搜索框中键入 CMake Tools,然后选择 Microsoft 的 CMake Tools。注意不要将它与 CMake 扩展混淆。按安装按钮进行安装。

         安装完成后,扩展就可以使用了。

Ⅲ. 快速启动项目(Quick Start project)

        VSCode CMake Tools 扩展提供了一个快速启动选项,它使用示例 C++ 代码引导 CMake 项目。要使用它,首先使用 文件|打开目标文件夹。

        然后按 F1 并键入 cmake quick start。选择 CMake:Quick start,然后按键盘上的 Enter。

        首先,扩展程序会询问使用哪个套件。选择适合您的新项目的一个。套件将在处理套件部分中进一步讨论。


        选择套件后,您将被要求输入项目名称。这将是您的顶级 CMake 项目的名称。输入您选择的名称。


        最后,将显示示例应用程序代码的选择。在此选择中,您将被要求创建一个可执行的应用程序项目或一个库项目。选择一个。您已经拥有了一个有效的 CMake 项目。选择后,将生成 CMakeLists.txt 和 main.cpp 文件。这些文件的内容在可执行文件和库选项之间略有不同。

Ⅳ. 打开现有项目(Opening an existing project)

        在 VSCode 中打开 CMake 项目并没有什么特别之处。打开包含项目顶级 CMakeLists.txt 文件的文件夹。 CMake Tools 扩展会自动将此文件夹识别为 CMake 项目,并且所有与 CMake 相关的命令都将在 VSCode 命令面板上可用。

Ⅴ. 配置、构建和清理项目(Configuring, building, and cleaning a project)

         要配置 CMake 项目,请从命令面板中选择 CMake: Configure 菜单项。

         要构建项目,请通过从 Command Palette 中选择 CMake: Set Build Target 菜单项来选择构建目标。

        这将让您选择在调用构建时将构建的内容。最后,选择 CMake: Build 以构建选定的构建目标。要构建特定目标而不将其设置为构建目标,请使用 CMake: Build Target 菜单项。

         要清理构建工件,请使用 CMake: Clean Command Palette 项。这将运行 CMake 的干净目标并删除任何构建工件。

Ⅵ.调试目标(Debugging a target)

        要调试目标,请通过从命令面板中选择 CMake: Set Debug Target 菜单项来选择调试目标。您将看到列出的可调试目标。

         选择目标并从命令面板中选择 CMake: Debug (Ctrl + F5)。选定的目标将在调试器下启动。
        如果要在没有调试器的情况下运行选定的目标,请选择 CMake: Run without Debugging (Shift + F5)。

         在下一节中,我们将了解如何为调试目标提供参数。

Ⅶ.将参数传递给调试的目标(Passing arguments to the debugged target)

        您尝试调试的目标可能需要命令行参数。要将命令行参数传递给调试目标,请打开 VSCode 的 settings.json 文件并附加以下行。

"cmake.debugConfig": {
        "args": [
            "<argument1>",
            "<argument2>"
        ]
    }

        在 args JSON 数组中,您可以放置​​目标所需的任意数量的参数。这些参数将无条件地传递给所有未来的调试目标。如果您想对参数进行细粒度控制,最好定义一个 launch.json 文件。

Ⅷ.处理套件 (Dealing with kits)

        CMake Tools 扩展中的工具包代表可用于构建项目的工具组合;因此,术语工具包几乎是工具链的同义词。套件使在多编译器环境中工作变得更加容易,允许用户选择使用哪个确切的编译器。工具包可以由扩展程序自动发现、从工具链文件中读取或由用户手动定义。
要查看项目的可用套件,请选择 CMake:Select a Kit菜单项(F1 或 Ctrl+Shift+P)。

        选择的工具包将用于配置 CMake 项目,这意味着工具包中定义的工具将用于编译项目。套件选择将自动触发 CMake 配置。

        默认情况下,扩展程序会自动扫描套件。因此,发现的工具链在套件选择菜单中列为选项。如果此处未显示您的工具链,则表示 CMake 工具未能发现它。在这种情况下,请先尝试重新扫描套件。如果它仍然丢失,您始终可以通过手动将它们添加到用户本地 cmake-tools-kits.json (1) 文件来定义其他工具包。

        通常不需要添加新工具包,因为扩展在发现工具链方面做得很好。在异常失败的情况下,这里有一个工具包模板,您可以自定义并附加到用户本地的 cmake-tools-kits.json 文件以定义新工具包。要打开用户本地工具包文件,请从命令面板中选择 CMake: Edit User-Local CMake Kits 菜单项:

  {
    "name":"<name of the kit>",
    "compilers" {
      "CXX":"<absolute-path-to-c++-compiler>",
      "C": "<absolute-path-to-c-compiler>"
    }
  }

        在旧版本的 CMake 工具扩展中,cmake-tools-kits.json 文件可能被命名为 cmake-kits.json。

        请记住,如果您的套件名称与 CMake 工具中自动生成的名称冲突,CMake 工具将在扫描时覆盖您的条目,因此,请始终为您的套件定义提供唯一名称。有关套件的更多信息,请参阅 https://github.com/microsoft/vscode-cmake-tools/blob/develop/docs/kits.md。

3.Qt Creator

        Qt Creator 是另一个支持 CMake 项目的 IDE。 CMake 支持是不错的,并且支持开箱即用,无需任何额外的插件。在本节中,我们将快速浏览 Qt Creator 的 CMake 支持。
        与往常一样,请确保首先在您的环境中正确安装和配置了 IDE。示例中使用了 Qt Creator 版本 4.3.1。

Ⅰ.添加 CMake 安装(Adding your CMake installation)

        为了在 Qt Creator 中使用 CMake,必须在 Qt Creator 中定义 CMake 的路径。要查看和定义 CMake 路径,请导航到 Tools | Options | Kits | CMake

         尽管没有任何手动操作,Qt Creator 还是能够发现系统中的 CMake 安装。 Auto-detected 部分下的第一个条目是与 Qt Creator 一起提供的 CMake 可执行文件。第二个是系统的CMake安装。要选择在 Qt Creator 中运行的 CMake 可执行文件,请选择所需的条目并单击 Make Default 按钮。
        要添加新的 CMake 可执行文件,请单击Add。这将在 Manual 部分添加一个新条目,并打开一个窗口来填写新条目的详细信息。

         此处详细描述了窗口字段:
• 名称:用于区分新的 CMake 可执行条目的唯一名称。
• 路径:CMake 可执行路径 (cmake/cmake.exe)。
• 版本:CMake 的版本(由Qt Creator 推导出)。
• 帮助文件:可执行文件的可选Qt Creator 帮助文件。这将允许 CMake按 F1 时出现帮助。
• Autorun CMake:选中此项可在任何 CMakeLists.txt 上自动运行 CMake文件更改。
填写详细信息后,单击应用将新的 CMake 可执行文件添加到 Qt Creator。如果您打算使用 Qt Creator,请不要忘记将其设置为默认值

Ⅱ.创建 CMake 项目(Creating a CMake project)

        在 Qt Creator 中创建 CMake 项目的步骤与创建常规项目的步骤完全相同。 Qt Creator 不会将 CMake 视为外部构建系统生成器。相反,它允许用户在 qmake、cmake 和 qbs 三个构建系统生成器之间进行选择。任何类型的 Qt 项目都可以由任何这些构建系统生成器从头开始。
        要在 Qt Creator 中创建 CMake 项目,请单击 File | New File or Project... (Ctrl + N),然后从 New File or Project 窗口中选择项目类型。我们将使用 Qt Widgets Application 作为示例。

4.CLion

        在安装完成QT creator并且加入其环境后,clion会自动检测QT环境并为我们配置CMakeList.txt。

        关于如何下载配置QT creator,可以参照我的一篇博客:

LINUX下如何安装配置QThttps://mp.csdn.net/mp_blog/creation/editor/125399509        我们只需要点击构建进行配置,运行就可以出现窗体了。

1.3  创建 CMake 项目(Creating a CMake Project)

        到目前为止,应该熟悉如何使用 CMake 及其基本概念,例如两阶段构建。到目前为止,我们只了解了 CMake 如何与已经存在的代码一起使用,但更有趣的部分是使用 CMake 构建应用程序时。

        在本章中,您将学习如何构建可执行文件和库以及如何一起使用它们。我们将深入了解创建不同类型的库,并介绍一些关于如何构建 CMake 项目的良好实践。由于库通常带有各种编译器设置,我们将学习如何设置它们并在必要时将它们传递给依赖库。由于项目中的依赖关系可能会变得相当复杂,我们还将学习如何可视化不同目标之间的依赖关系。
在本章中,我们将介绍以下主题:
• 设置项目
• 创建一个“hello world”可执行文件
• 创建一个简单的库
• 把它放在一起

技术要求:

        与前几章一样,所有示例都使用 CMake 3.21 进行了测试,并在以下任一编译器上运行:

• GCC 9 或更新版本。
• Clang 12 或更新版本。
• MSVC 19 或更高版本。

1.3.1  设置项目(Setting up a project)*

1.cmake项目文件结构

        尽管 CMake 几乎可以处理项目的任何文件结构,但在如何组织文件方面有一些好的做法。本文中的示例使用以下常见模式:

├── CMakeLists.txt
├── build                            //放置构建文件和二进制文件的文件夹
├── include/project_name             //此文件夹包含可从项目外部公开访问的所有头文件
└── src                              //此文件夹包含所有私有的源文件和头文件

        请看以下我们构建好的文件目录:

        构建前的目录仅仅少了build文件夹。 

2.阐述文件结构

        最小项目结构中存在三个文件夹和一个文件。它们如下:
• build:放置构建文件和二进制文件的文件夹。
• include/project_name:此文件夹包含可从项目外部公开访问的所有头文件(记住这几个形容词:外部、公开访问、头文件)。添加包含项目名称的子文件夹很有帮助,因为包含是通过 <project_name/somefile.h> 完成的,从而更容易确定头文件来自哪个库。
• src:此文件夹包含所有私有的源文件和头文件。
• CMakeLists.txt:这是根 CMake 文件。

        build 文件夹几乎可以放在任何地方。将它放在项目根目录中非常方便。但是,我们强烈建议不要选择任何非空文件夹作为构建文件夹。特别是将构建文件放入 include 或 src 被认为是一种不好的做法。其他文件夹(例如 test 或 doc)可以方便地组织测试项目和文档页面。

1.3.2 使用嵌套项目(Working with nested projects) 

        当您将项目相互嵌套时,每个项目都应该映射上面的文件结构,并且应该编写每个 CMakeLists.txt,以便可以独立构建子项目。这意味着子项目的每个 CMakeLists.txt 文件都应指定 cmake_minimum_required 以及可选的项目定义。我们将在第 9 章“创建可重现的构建环境”中深入介绍大型项目和超级构建。
        嵌套项目看起来像这样:

├── CMakeLists.txt                    //主体cmake文件
├── build                             //构建文件
├── include/project_name              //项目外部公开访问的所有头文件
├── src                               //此文件夹包含所有私有的源文件和头文件
└── subproject                        //内部的嵌套项目
    ├── CMakeLists.txt                //内部的嵌套项目的主体cmake文件
    ├── include
    │   └── subproject                //再嵌套项目
    └── src

        在这里,文件夹结构在子项目文件夹中重复。坚持这样的文件夹结构并使子项目可自行构建,可以更轻松地移动项目。它还允许您只构建项目的一部分,这对于构建时间可能会相当长的大型项目非常有用。
        现在我们已经涵盖了文件结构,让我们从创建一个没有任何特殊依赖项的简单可执行文件开始。在本章的后面,我们将创建各种类型的库并将它们组合在一起。

1.3.3  创建一个“hello world”可执行文件(Creating a "hello world" executable) 

1.建立C++文件

        首先,我们将从一个简单的 hello world C++ 程序创建一个简单的可执行文件。以下 C++ 程序将打印出 Welcome to CMake Best Practices:

#include <iostream>
int main(int, char **) {
  std::cout << "Welcome to CMake Best Practices\n";
  return 0;
}

2.编写CMakeists.txt文件

         要构建它,我们需要编译它并为可执行文件命名。让我们看看构建这个可执行文件的 CMakeLists.txt 文件是什么样的:

cmake_minimum_required(VERSION 3.21)
project(
    hello_world_standalone
    VERSION 1.0
    DESCRIPTION"A simple C++ project"
    HOMEPAGE_URL https://github.com/PacktPublishing/CMake-Best-
 Practices
    LANGUAGES CXX
)

//创建目标以构建可执行文件
add_executable(hello_world)
//将源文件添加到“hello_world”目标
target_sources(hello_world PRIVATE src/main.cpp)

3.解释cmakelists.txt内容

        通过第一行 cmake_minimum_required(VERSION 3.21),我们告诉 CMake 我们期望看到哪个版本的 CMake 以及 CMake 将启用哪些功能。
        对于此示例,版本 3.1 将是绝对最低版本,因为在此之前,target_sources 命令不可用。最好将 cmake_minimum_required 命令放在每个 CMakeLists.txt 文件的顶部。
        接下来,使用 project() 命令设置项目。第一个参数是项目的名称——在我们的例子中是“hello_world_standalone”。
        接下来,将项目的版本设置为 1.0 版本。以下是简要说明和主页的 URL。

        最后,LANGUAGES CXX 属性指定我们正在构建一个 C++ 项目。除了项目名称之外,所有参数都是可选的。
        调用 add_executable(hello_world) 命令会创建一个名为 hello_world 的目标。这也将是此目标创建的可执行文件的名称。

         现在已经创建了目标,使用 target_sources 将 C++ 源文件添加到目标中(为什么药用PRIVATE关键字呢,我们往前翻1.3.1节,src目录下的都设置为私有权限)

         Chapter3 是目标名称,在 add_executable 中指定。 PRIVATE 定义源仅用于构建此目标,而不用于任何依赖目标。在范围说明符之后,有一个与当前 CMakeLists.txt 文件的路径相关的源文件列表。如果需要,可以使用 CMAKE_CURRENT_SOURCE_DIR 变量访问当前处理的 CMakeLists.txt 文件的位置。

        当然,也可以用如上绝对路径的方法。(但要是到别人电脑上怎么办呢?推荐相对路径或者如下方法)

        我们先看看CMAKE_CURRENT_SOURCE_DIR变量里存放的是什么吧!

target_sources(ch3_hello_world_standalone PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)
​target_sources(ch3_hello_world_standalone PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)

        源可以直接添加到 add_executable 函数或单独使用 target_sources 函数。使用 target_sources 添加它们允许您通过使用 PRIVATE、PUBLIC 或 INTERFACE 明确定义可以在何处使用源。但是,指定 PRIVATE 以外的任何内容仅对库目标有意义。
        您经常看到的一种常见模式是以项目名称命名项目的主要可执行文件,如下所示:

project(hello_world
...
)
add_executable(${PROJECT_NAME})

        虽然这乍一看似乎很方便,但应该避免。项目名称和目标具有不同的语义含义,因此应将它们视为单独的事物,因此应避免使用 PROJECT_NAME 作为目标的名称。
        可执行文件很重要并且很容易创建,但除非您正在构建一个巨大的单体应用程序,否则您应该使用库来模块化和分发代码。在下一节中,我们将学习如何构建库以及如何处理不同的链接方法。

3.构建这个目标

 这是我们项目的目录树,我们来构建它。

 我们看到build文件夹里有了helloworld可执行文件!成功!我们执行它!

1.3.4  创建一个简单的库(Creating a simple library) 

1.一个简单的库项目的CMakeists.txt的内容

        创建库的工作方式与创建可执行文件类似,但需要考虑一些额外的事情,因为库目标通常由其他目标使用,无论是在同一个项目中还是由其他项目。由于库通常具有内部部分和公开可见的 API,因此我们在向项目添加文件时必须考虑到这一点。
        一个简单的库项目将如下所示:

cmake_minimum_required(VERSION 3.21)
project(
  ch3.hello_lib
  VERSION 1.0
  DESCRIPTION
    "A simple C++ project to demonstrate creating executables and libraries in CMake"
  LANGUAGES CXX)
add_library(hello)          //创建可执行程序的时候是add_executable()
target_sources(             //为库添加源文件,即.cpp文件
  hello
  PRIVATE src/hello.cpp src/internal.cpp)
target_compile_features(hello PUBLIC cxx_std_17)
target_include_directories( //设置指定目标包含的头文件路径
  hello
  PRIVATE src/hello
  PUBLIC include)

2.详解CMakeLists.txt文件

Ⅰ.cmake_minimum_required

        同样,该文件以设置 cmake_minimum_required 和项目信息开始,您现在应该熟悉这些信息。

Ⅱ.add_library

        使用 add_library 创建库的目标——在这种情况下,未确定库的类型。

        我们可以通过 STATICSHARED 来明确地确定库的链接类型。通过省略这一点,我们允许库的任何下游构建者(我理解为使用你的库的人)选择如何构建和链接它。

        一般来说,静态库是最容易处理的。有关构建共享库的更多信息,请参阅共享库中的符号可见性小节。
        如果省略库的类型,则 BUILD_SHARED_LIBS 变量判定此库是默认构建为共享库或者静态库。不应在项目的 CMake 文件中无条件设置此变量;它应该始终由用户传递。

Ⅲ.target_sources

        接下来,使用 target_sources 添加库的源。

        第一个参数是目标名称,后跟由 PRIVATE、PUBLIC 或 INTERFACE 关键字分隔的源。在实践中,几乎总是使用 PRIVATE 说明符添加源文件。

         PRIVATE 和 PUBLIC 关键字指定应在何处使用源文件进行编译。

        指定 PRIVATE 意味着源将仅用于目标 hello 本身。如果使用 PUBLIC,则源将添加到 hello 以及链接到 hello 的任何目标。正如我们之前提到的,这通常是不需要的。 INTERFACE 关键字意味着源不添加到 hello 中,但应该添加到任何链接到 hello 的内容中。

        通常,为目标指定为 PRIVATE 的任何内容都可以视为构建要求。

Ⅳ.target_include_directories

        最后,使用 target_include_directories 设置指定目标包含的头文件路径。可以使用 #include <file.hpp>(带尖括号)而不是 #include "" 访问此命令指定的文件夹中的所有文件,尽管带引号的版本可能仍然有效。
        PRIVATE 包括不会包含在目标属性中的路径:即,INTERFACE_INCLUDE_DIRECTORIES。当目标依赖于库以确定哪些包含目录对依赖者可见时,CMake 将读取此属性。
        由于库的 C++ 代码使用与现代 C++ 版本相关的功能,例如 C++ 11/14/17/20 或 C++ 23,因此我们必须设置 cxx_std_17 属性。由于它用于编译库本身并与库进行交互,因此将其设置为 PUBLIC。仅当头文件包含需要特定标准的代码时,才需要将其设置为 PUBLIC 或 INTERFACE。如果只有内部代码依赖于某个标准,最好将其设置为 PRIVATE。通常,尝试将公共 C++ 标准设置为有效的最低标准。也可以只启用现代 C++ 标准之一的某些功能。
可以在下面的链接找到可用编译功能的完整列表。 https://cmake.org/cmake/help/latest/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html

1.3.5  命名库(Naming libraries)

1.重命名库名称

        当您使用 add_library(<name>) 创建库时,库的名称在项目内必须是全局唯一的,因为名称冲突是错误的。默认情况下,库的实际文件名是根据平台上的约定构建的,例如 Linux 上的 lib<name>.so 和 Windows 上的 <name>.lib 或 <name>.dll。通过设置目标的 OUTPUT_NAME 属性,可以从默认行为更改文件的名称。这可以在以下示例中看到,其中输出文件的名称已从 ch3_hello 更改为 hello。

set_target_properties(
   ch3_hello
   PROPERTIES OUTPUT_NAME hello
)

        避免使用带有 lib 前缀或后缀的库名称,因为 CMake 可能会根据平台将适当的字符串附加或添加到文件名中。

2.共享库命名约定

        共享库常用的命名约定是将版本添加到文件名中以指定构建版本和 API 版本。通过为库目标指定 VERSION 和 SOVERSION 属性,CMake 将在构建和安装库时创建必要的文件名和符号链接:

//为目标设置属性。 VERSION 将库版本设置为项目版本 * SOVERSION 将库的兼容版本设置为版本的主编号
set_target_properties(
    hello
    PROPERTIES VERSION ${PROJECT_VERSION} 
    SOVERSION ${PROJECT_VERSION_MAJOR} 
//Value Computed by CMake
CMAKE_PROJECT_VERSION:STATIC=1.0.0

//Value Computed by CMake
CMAKE_PROJECT_VERSION_MAJOR:STATIC=1

        在 Linux 上,此示例将生成 libch3_hello_shared.so 的文件名,其中包含来自 libch3_hello_shared.so 和 libch3_hello_shared.so.1 的符号链接指向实际的库文件。以下屏幕截图显示了生成的文件和指向它的符号链接:

         在项目中经常看到的另一个约定是为各种构建配置的文件名添加不同的后缀。 CMake 通过将 CMAKE_<CONFIG>_POSTFIX 全局变量设置为任何约定或将 <CONFIG>_POSTFIX 属性添加到目标来处理此问题。如果设置了这个变量,后缀将自动添加到不可执行的目标。与大多数全局变量一样,它们应该通过命令行或作为预设而不是硬编码在 CMakeLists.txt 文件中传递给 CMake。
        调试库的后缀也可以显式设置为单个目标,如下例所示:

set_target_properties(
hello
PROPERTIES DEBUG_POSTFIX d)

        当您在调试配置中构建时,这将导致库文件和符号链接被命名为 libhellod.so。由于链接库是通过目标而不是 CMake 中的文件名完成的,因此选择正确的文件名会自动发生,因此我们不必手动跟踪。但是,链接共享库时要注意的一件事是符号可见性。我们将在下一节中讨论这一点。

 1.3.6  共享库中的符号可见性(Symbol visibility in shared libraries)

        要链接到共享库,链接器必须知道可以从库外部使用哪些符号。这些符号可以是类、函数、类型(int,float,double...)等等,使它们可见的过程称为导出。(类似符号链接)
        编译器在指定符号可见性时有不同的方式和默认行为,这使得以独立于平台的方式指定它有点麻烦。它从编译器的默认可见性开始; gcc 和 clang 假定所有符号都是可见的,而 Visual Studio 编译器默认隐藏所有符号,除非它们被显式导出。

        通过设置 CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS,可以更改 MSVC 的默认行为,但这是解决问题的蛮力方法,只能在应导出库的所有符号时使用。
        虽然将所有符号设置为公开可见是确保链接容易的一种简单方法,但它有一些缺点:
• 通过导出所有内容,无法阻止相关目标使用内部代码。
• 由于外部代码可以使用每个符号,链接器不能丢弃死代码,因此生成的库往往会变得臃肿。如果库包含模板,则尤其如此,这往往会大大增加符号的数量。
• 由于每个符号都是导出的,关于什么应该被视为隐藏或内部的唯一线索必须来自文档。
• 暴露库的内部符号可能会暴露应该隐藏的东西。        

        将所有符号设置为可见
        将所有符号设置为在共享库中可见时要小心,尤其是当您担心安全问题或二进制文件的大小很重要时。

1.3.7 更改默认可见性 (Changing the default visibility)

        要更改符号的默认可见性,请将 <LANG>_VISIBILITY_PRESET 属性设置为 HIDDEN。        

        可以全局设置此属性,也可以为单个库目标设置此属性。 <LANG> 代替编写库的语言,如 CXX 代表C++ 或C 代表C。

        如果所有符号都是隐藏符号要导出,则必须在代码中专门标记。最常见的方法是指定一个预处理器定义来确定符号是否可见:

class HELLO_EXPORT Hello { 
…
};

        HELLO_EXPORT 定义将包含有关在编译库时是否要导出符号或在链接库时是否应导入符号的信息。

        GCC 和 Clang 使用 __attribute__(...) 关键字来确定这种行为,而在 Windows 上,使用 _declspec(...)。编写以跨平台方式处理此问题的头文件并非易事,尤其是如果您还必须考虑库可能构建为静态库和对象库时。

        幸运的是,CMake 提供了 generate_export_header 宏,它由 GenerateExportHeader 模块导入,使这更容易。
        在以下示例中,hello 库的符号默认设置为隐藏。然后,使用 GenerateExportHeader 模块导入的 generate_export_header 宏再次单独启用它们。
        此外,此示例将 VISIBILITY_INLINES_HIDDEN 属性设置为 TRUE,以通过隐藏内联类成员函数来进一步减少导出符号表。为内联设置可见性并不是绝对必要的,但通常在设置默认可见性时完成:       

//添加库目标
add_library(hello SHARED)

//如果使用有限的可见性,将 CXX_VISIBILILTY_PRESET 设置为“隐藏”
set_property(TARGET hello PROPERTY CXX_VISIBILITY_PRESET  "hidden")

//默认隐藏内联函数,减少库的大小
set_property(TARGET hello PROPERTY VISIBILITY_INLINES_HIDDEN  TRUE)

include(GenerateExportHeader)
//此命令在 CMAKE_CURRENT_BINARY_DIR 中生成一个头文件,该文件根据编译器设置设置可见性属性
generate_export_header(hello EXPORT_FILE_NAME export/hello/export_hello.hpp)

//将 CMAKE_CURRENT_BINARY_DIR 添加到包含路径,以便可以找到生成的标头
target_include_directories(hello PUBLIC "${CMAKE_CURRENT_BINARY_DIR} /export")

        对 generate_export_header 的调用会CMAKE_CURRENT_BINARY_DIR/export/hello 目录中创建一个名为 export_hello.hpp 的文件,该文件可以包含在库的文件中。最好将这些生成的文件放在构建目录的子文件夹中,以便仅将部分目录添加到包含路径中。

        生成文件的包含结构应与库其余部分的包含结构相匹配。因此,在本例中,如果所有公共头文件都通过调用 #include <hello/a_public_header.h> 包含在内,则导出头文件也应放在名为 hello 的文件夹中。生成的文件还必须添加到安装说明中,如第 4 章“打包、部署和安装 CMake 项目”中所述。此外,要创建导出文件,必须将用于导出符号的必要编译器标志设置为目标。

        由于生成的头文件必须包含在声明要导出的类、函数和类型的文件中,因此将 CMAKE_CURRENT_BINARY_DIR/export/ 添加到 target_include_directories。请注意,这必须是 PUBLIC 以便依赖库也可以找到该文件。关于 generate_export_header 宏还有更多选项,但我们在本节中看到的内容涵盖了大多数用例。可以从位于 https://cmake.org/cmake/help/latest/module/GenerateExportHeader.html 的官方 CMake 文档中检索有关设置符号可见性的其他信息。

1.3.8  接口或仅头文件库 (Interface or header-only libraries)

        仅头文件库有点特殊,因为它们没有被编译;相反,它们导出它们的标头,以便它们直接包含在其他库中。

        在大多数方面,仅标头库与普通库一样工作,但它们的头文件使用 INTERFACE 关键字而不是 PUBLIC 关键字公开。
        由于不需要编译仅标头库,因此它们不会将源添加到目标(target_source)。以下代码创建了一个最小的仅标头库:

hellohead.hpp

#pragma once

#include <iostream>
#include <string>

namespace hello_header_only
{
    void print_hello(const std::string& name) {
        std::cout << "Hello " << name << " from a header only library\n";
    }
}
project(
  ch3_hello_header_only
  VERSION 1.0
  DESCRIPTION "Chapter 3 header-only example"
  LANGUAGES CXX)
add_library(hello_header_only INTERFACE)

//公开包含目录
target_include_directories(hello_header_only INTERFACE include/)
//公开编译此库所需的最低 C++ 版本
target_compile_features( hello_header_only INTERFACE cxx_std_17)

        另外值得注意的是,在 CMake 版本 3.19 之前,INTERFACE 库不能添加任何 target_sources。现在,仅标头库不能列出任何源。

1.3.9 对象库——仅供内部使用(Object libraries – for internal use only)

        有时,您可能希望拆分代码以便可以重用其中的部分代码,而无需创建完整的库。

        一种常见的做法是,当您想在可执行文件和单元测试中使用某些代码时,无需重新编译所有内容两次。
        为此,CMake 提供了对象库,其中源被编译,但不存档或链接。通过调用 add_library(MyLibrary OBJECT) 创建对象库。

# Chapter3-  Example illustrating how to create an object library
#
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.17)

project(
    ch3_hello_object
    VERSION 1.0.0
    DESCRIPTION
        "A simple C++ project to demonstrate creating executables and libraries in CMake"
    LANGUAGES CXX
)

# 添加库目标
add_library(ch3_hello_object OBJECT)

# 将源添加到库目标
target_sources(ch3_hello_object PRIVATE src/hello_object.cpp)

# 定义编译该库并使其对依赖者可见所需的 C++ 标准
target_compile_features(ch3_hello_object PUBLIC cxx_std_17)

# 设置包含目录
target_include_directories(
    ch3_hello_object
    PRIVATE src/hello
    PUBLIC include
)

        从 CMake 3.12 开始,这些对象可以通过将它们添加到 target_link_libraries 函数来像普通库一样使用。

        在 3.12 版本之前,需要添加生成器表达式的对象库;即 $<TARGET_OBJECTS:MyLibrary>。这在构建系统生成期间扩展为对象列表。这仍然可以完成,但不再推荐,因为它很快变得无法维护,尤其是在项目中有多个对象库的情况下。

        使用对象库,涵盖了所有不同类型的库。库本身的编写和维护很有趣,但除非将它们集成到更大的项目中,否则它们不会做任何事情。那么,让我们看看到目前为止我们定义的所有库如何在可执行文件中使用。

1.3.10  将其整合在一起——使用您的库(Bringing it together – using your libraries)

        到目前为止,我们已经创建了三个不同的库——一个静态或动态链接的二进制库,一个接口或仅头文件库,以及一个预编译但未链接的对象库。
        让我们学习如何在共享项目的可执行文件中使用它们。将它们安装为系统库或将它们用作外部依赖项将在第 5 章“集成第三方库和依赖项管理”中介绍。因此,我们可以将 add_library 调用放在同一个 CMakeLists.txt 文件中,也可以使用 add_subdirectory 集成它们。两者都是有效的选项,取决于项目的设置方式,如本章的设置项目和使用嵌套项目部分所述。
        在以下示例中,我们假设已使用 hello_lib、hello_header_only 和 hello_object 目录中的 CMakeLists.txt 文件定义了三个库。可以使用 add_subdirectory 命令包含这些库。在这里,创建了一个名为 chapter3 的新目标,它是我们的可执行文件。然后,这些库通过 target_link_libraries 添加到可执行文件中。

add_subdirectory(hello_lib)
add_subdirectory(hello_header_only)
add_subdirectory(hello_object)
add_executable(chapter3)
target_sources(chapter3 PRIVATE src/main.cpp)
target_link_libraries(chapter3 PRIVATE hello_header_only hello hello_object)

        target_link_libraries 的目标也可以是另一个库。同样,库是使用访问说明符链接的,访问说明符是以下之一:
• PRIVATE:该库用于链接,但它不是公共接口的一部分。只有在构建目标时才需要链接库。
• INTERFACE:库未链接,但它是公共接口的一部分。当您在其他地方使用目标时,需要链接库。这通常仅在您从其他仅标头库链接仅标头库时使用。
• PUBLIC:库被链接,它是公共接口的一部分。因此,该库既是构建依赖项,也是使用依赖项。

注意——不良做法
***强烈反对以下做法,因为它们往往会创建不可维护的项目,从而难以在不同的构建环境之间移植。但是,为了完整起见,我们将它们包括在内。
        除了在 PUBLIC、PRIVATE 或 INTERFACE 之后传递另一个目标,您还可以传递库的完整路径或库的文件名,例如 /usr/share/lib/mylib.so 或只是 mylib.so。这些做法是可能的,但不鼓励这样做,因为它使 CMake 项目的可移植性降低。
此外,可以通过传递诸如 -nolibc 之类的东西在此处传递链接器标志,但同样不鼓励这样做。如果所有目标都需要特殊的链接器标志,那么使用命令行传递它们是首选方式。如果单个库需要特殊标志,则使用 target_link_options 是首选方法,最好与通过命令行设置的选项结合使用。

        在下一节中,我们将研究设置编译器和链接器选项。

1.3.11 设置编译器和链接器选项(Setting compiler and linker options)

        C++ 编译器有很多关于要设置的最常见标志的选项,从外部设置预处理器定义也是一种常见的做法。在 CMake 中,这些是使用 target_compile_options 命令传递的。使用 target_link_options 命令更改链接器行为。

        不幸的是,编译器和链接器可能有不同的设置标志的方式。例如,在 GCC 和 Clang 中,选项使用破折号 (-) 传递,而 Microsoft 编译器将斜杠 (/) 作为其选项的前缀。但是通过使用我们在第 1 章 Kickstarting CMake 中介绍的生成器表达式,这可以在 CMake 中方便地处理,如下例所示:

target_compile_options(
  hello
  PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/SomeOption> 
          $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:someOption>
)

        让我们详细看一下生成器表达式。

        $<$<CXX_COMPILER_ID:MSVC>:/SomeOption> 是一个由内向外计算的嵌套生成器表达式。生成器表达式在构建系统生成期间进行评估。首先,如果 C++ 编译器等于 MSVC,则 $<CXX_COMPILER_ID:MSVC> 计算结果为 true。如果是这种情况,则外部表达式将返回 /SomeOption,然后将其传递给编译器。如果内部表达式的计算结果为 false,则不会传递任何内容。
        $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-fopenmp> 的工作方式类似,但不是仅检查单个值,而是传递包含 GNU,Clang,AppleClang 的列表。如果 CXX_COMPILER_ID 匹配其中任何一个,则内部表达式的计算结果为 true,并且 someOption 被传递给编译器。将编译器或链接器选项作为 PRIVATE 传递会将它们标记为此目标的构建要求,而无需连接库。如果将 PRIVATE 替换为 PUBLIC,则编译选项也成为使用要求,并且依赖于原始目标的所有目标都将使用相同的编译器选项。向依赖目标公开编译器选项是需要谨慎完成的事情。如果编译器选项只需要使用目标而不是构建它,则可以使用关键字 INTERFACE。
当您构建仅包含标头的库时,通常会出现这种情况。编译器选项的一个特例是编译定义,它被传递给底层程序。这些通过 target_compile_definitions 函数传递。

1.3.12 调试编译器选项(Debugging compiler options) 

        要查看所有编译选项,您可以查看生成的构建文件,例如 Makefile 或 Visual Studio 项目。更方便的方法是让 CMake 将所有编译命令导出为 JSON 编译数据库。
        通过启用 CMAKE_EXPORT_COMPILE_COMMANDS 变量,将在构建文件夹中创建一个名为 compile_commands.json 的文件,其中包含用于编译的完整命令。
        启用此选项并运行 CMake 将产生类似于以下的结果:

{
  "directory": "/workspaces/CMake-Best-Practices/build",
  "command": "/usr/bin/g++ -I/workspaces/CMake-Best-Practices/
  chapter_3/hello_header_only/include -I/workspaces/CMake-Best-
 Practices/chapter_3/hello_lib/include -I/workspaces/CMake-
 Best-Practices/chapter_3/hello_object_lib/include -g 
 -fopenmp -o 
  chapter_3/CMakeFiles/chapter3.dir/src/main.cpp.o -c /
 workspaces/CMake-Best-Practices/chapter_3/src/main.cpp",
  "file": "/workspaces/CMake-Best-Practices/chapter_3/src/
 main.cpp"
},

        请注意在前面的示例中添加了手动指定的 -fopenMP 标志。 compile_commands.json 可以用作与构建系统无关的方式来加载命令。一些 IDE,例如 VS Code 和 CLion,可以自己解释 JSON 文件并生成项目信息。如果某些事情没有按预期工作,它通常也可以方便地调试编译器选项。编译命令数据库的完整规范可以在 https://clang.llvm.org/docs/JSONCompilationDatabase.html 找到。

1.3.13  库别名(Library aliases)

        库别名是一种在不创建新构建目标(有时称为命名空间)的情况下引用库的方法。一种常见的模式是以 MyProject::Library 的形式为从项目安装的每个库创建一个库别名。
        它们可用于对多个目标进行语义分组。它们还有助于避免命名冲突,尤其是当项目包含常见目标时,例如名为 utils、helpers 和类似名称的库。将同一项目的所有目标收集在同一命名空间下是一种很好的做法。当您从其他项目链接库时,包括命名空间可避免您意外包含错误的库。本章中的所有库目标都将使用命名空间进行别名,以便对它们进行分组,以便它们可以被它们的命名空间引用:

add_library(Chapter3::hello ALIAS hello)
...
target_link_libraries(SomeLibrary PRIVATE Chapter3::hello)

        除了帮助确定目标的来源外,CMake 还使用命名空间来识别导入的目标并创建更好的诊断消息,正如我们将在第 4 章“打包、部署和安装 CMake 项目”中查看安装和打包时看到的那样,以及第 5 章,集成第三方库和依赖管理,我们将在其中介绍依赖管理。

        命名空间是组织构建工件的好方法,但有时它们还不够。有时,我们想更全面地了解什么在使用什么以及哪个工件取决于哪个库。 CMake 可以帮助创建提供此类见解的依赖关系图,我们将在下一章中看到。

始终使用命名空间
        命名空间是组织构建工件的好方法,但有时它们还不够。有时,我们想更全面地了解什么在使用什么以及哪个工件取决于哪个库。 CMake 可以帮助创建提供此类见解的依赖关系图,我们将在下一章中看到。

1.3.14 总结及问题(Summary&Questions)

1.总结

        现在您已经完成了本章,您可以使用 CMake 创建应用程序和库,并开始构建比“hello world”更复杂的项目。您已经学习了如何将不同的目标链接在一起,以及如何将编译器和链接器选项传递给目标。我们还介绍了仅供内部使用的对象库,并讨论了共享库的符号可见性。最后,您学习了如何自动记录这些依赖关系,以便对大型项目有一个概览。
        在下一章中,您将学习如何在不同平台上打包和安装您的应用程序和库。

2.Questions

回答以下问题以测试您对本章的了解:
1.创建可执行目标的CMake命令是什么?
2.创建库目标的CMake命令是什么?
3. 如何指定库是静态链接还是动态链接?
4. 对象库有什么特别之处,它们在哪里派上用场?
5. 如何指定共享库的默认符号可见性?
6. 如何为目标指定编译器选项以及如何查看编译命令?

1.4 通过一个案例总结前三章的内容(Summarize the content of the first three chapters with a case)

1.4.1 项目源码及文件(Project source code and files)

GitHub - PacktPublishing/CMake-Best-Practices: CMake Best Practices, by Packt PublishingCMake Best Practices, by Packt Publishing. Contribute to PacktPublishing/CMake-Best-Practices development by creating an account on GitHub.https://github.com/PacktPublishing/CMake-Best-Practices这里面的chapter3文件夹

1.4.2 构建并创建项目(Build and create the project)

1.配置:通过命令 

2. 构建:通过命令

1.4.3 用cmake构建仅有头文件的库(Building a header-only library with cmake)

1.hello_header_only结构解析(仅有头文件的库)

        作为一个只有头文件的库,仅通过hpp文件创建自己的库。

         chapter03/hello_header_only/include/hello_header_only/*.hpp

#pragma once

#include <iostream>
#include <string>

namespace hello_header_only
{
    void print_hello(const std::string& name) {
        std::cout << "Hello " << name << " from a header only library\n";
    }
}

        我们来解释它的CMakLists.txt文件。

# Chapter 3 - Example illustrating how to create a header only library
#
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.21)

project(
    ch3_hello_header_only
    VERSION 1.0
    DESCRIPTION "Chapter 3 header only example"
    LANGUAGES CXX
)

# 创建库目标
add_library(ch3_hello_header_only INTERFACE)

# 公开包含目录
target_include_directories(ch3_hello_header_only INTERFACE include/)

# 公开编译此库所需的最低 C++ 版本
target_compile_features(ch3_hello_header_only INTERFACE cxx_std_17)

        这个介绍了如何创建一个仅仅有头文件.hpp的库的CMakLists.txt的写法。

        cmake_minimum_required说明表明了他要求cmake版本最低为3.21。

        project字段说明了这个项目的名称为ch3_hello_header_only,版本为1.0,项目描述字符串为Chapter 3 header only example,用C++构建。

        add_library字段说明了生成一个名字为ch3_hello_header_only的库,我们看看buid文件里但没有指定是静态还是动态库。

        target_include_directories设置库的包含目录,即这个库的源文件在include下,我们看下文件结构,也是如此。

         这样这个库就设计好了。

1.4.4 用cmake构建对象库(Building object libraries with cmake)

1. hello_object_lib结构解析(对象库解析)

        这个稍有一点复杂,我们查看其目录树:

2.库文件代码解读

#pragma once

#include <string>

namespace hello_object {
/// 显式导出到 dll 中的示例类
class HelloObject {
public:
  HelloObject(const std::string &name) : name_{name} {}

  void greet() const;

private:
  const std::string name_;
};
} // namespace hello_object

        这里有一个类,一个类的默认构造函数,另一个是greet方法,我们在源文件看其实现:

#include <hello_object/hello_object.hpp>

#include <iostream>

namespace hello_object {
void HelloObject::greet() const {
  std::cout << "Hello " << name_ << " From an object library\n";
}
} // namespace hello_object

        greet函数输出一行字。

3.CMakLists.txt文件解读

# Chapter3-  Example illustrating how to create an object library
#
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.17)

project(
    ch3_hello_object
    VERSION 1.0.0
    DESCRIPTION
        "A simple C++ project to demonstrate creating executables and libraries in CMake"
    LANGUAGES CXX
)

# 添加库目标
add_library(ch3_hello_object OBJECT)

# 将源添加到库目标
target_sources(ch3_hello_object PRIVATE src/hello_object.cpp)

# 定义编译该库并使其对依赖者可见所需的 C++ 标准
target_compile_features(ch3_hello_object PUBLIC cxx_std_17)

# 设置包含目录
target_include_directories(
    ch3_hello_object
    PRIVATE src/hello
    PUBLIC include
)

        add_library关键字:创立一个名叫ch3_hello_object的库,是一个对象库。

        target_sources关键字:添加库的源文件;第一个参数是库的名称,第二个指明私有,第三个是库源文件的路径,这时,库文件和源文件链接起来了。

        target_include_directories:设置库的包含目录,库下面有俩文件夹,源文件设置为私有,库文件设置为公共权限。

1.4.4 用cmake构建共享库(Building shared libraries with cmake)​​​​​​​

1.目录树解析

2.分析源码

        我们看看代码这个库究竟在干什么呢?

        首先看hello.hpp

#pragma once 

#include <string>
#include "hello/export_hello.hpp"


namespace hello{
    /// 显式导出到 dll 中的示例类
    class CH3_HELLO_SHARED_EXPORT Hello {
        public:
        Hello(const std::string& name) : name_{name} {}

        void greet() const; 
        private:
        const std::string name_;
    };
}

        hello.cpp

#include <hello/hello.hpp>

#include "internal.hpp"

namespace hello{
    void Hello::greet() const {
        details::print_impl(name_);
    }
}

        internal.hpp

#pragma once 

#include <string>

namespace hello::details{

    void print_impl(const std::string& name);
    
}

        internal.cpp

#include "internal.hpp"

#include <iostream>

namespace hello::details{ 
    void print_impl(const std::string& name)
    {
        std::cout << "Hello " << name << " from a shared library\n";
    }
}

        这里internal的实现包含一个hello::details命名空间下的一个print_impl符号,作用是打印默认构造传进来的string类型的参数。

        hello中通过引入头文件导出了internal中的print_impl符号,但怎么引出来的呢?我们这时看它的CMakLists.txt文件:

3.分析cmakelists.txt文件

# Chapter3- example illustrating how to create a shared library
# Additionally the example shows how to set a postfix for debug libraries and 
# how to handle symbol visibilities for shared libraries
#
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.17)

project(
    ch3_hello_shared
    VERSION 1.0.0
    DESCRIPTION
        "A simple C++ project to demonstrate creating executables and libraries in CMake"
    LANGUAGES CXX
)

# 在调试模式下构建库时,为生成的 .so 或 .dll 文件设置后缀“d”
set(CMAKE_DEBUG_POSTFIX
    d
)

# 添加库目标
add_library(ch3_hello_shared SHARED)

# 为目标设置属性。 VERSION 将库版本设置为项目版本 * SOVERSION 将库的兼容版本设置为版本的主编号
set_target_properties(
    ch3_hello_shared
    PROPERTIES VERSION ${PROJECT_VERSION}
               SOVERSION ${PROJECT_VERSION_MAJOR}
)

# 将源添加到库目标
target_sources(
    ch3_hello_shared
    PRIVATE src/hello.cpp src/internal.cpp
)

# 定义编译该库并使其对依赖者可见所需的 C++ 标准
target_compile_features(
    ch3_hello_shared
    PUBLIC cxx_std_17
)

# 设置包含目录
target_include_directories(
    ch3_hello_shared
    PRIVATE src/ch3_hello_shared
    PUBLIC include
)

# 如果使用有限的可见性,将 CXX_VISIBILILTY_PRESET 设置为“隐藏”
include(GenerateExportHeader)
set_property(
    TARGET ch3_hello_shared
    PROPERTY CXX_VISIBILITY_PRESET "hidden"
)

# 默认隐藏内联函数,减少库的大小
set_property(
    TARGET ch3_hello_shared
    PROPERTY VISIBILITY_INLINES_HIDDEN TRUE
)

# 此命令在 CMAKE_CURRENT_BINARY_DIR 中生成一个头文件,该文件根据编译器设置设置可见性属性
generate_export_header(
    ch3_hello_shared
    EXPORT_FILE_NAME
    export/hello/export_hello.hpp
)

# 将 CMAKE_CURRENT_BINARY_DIR 添加到包含路径,以便可以找到生成的标头
target_include_directories(
    ch3_hello_shared
    PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/export"
)

# 导出符号的另一种但不推荐的方式是使用如下命令
# set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) Which exports all dll symbols by
# default

        这个比较长,我们一条条来解释。

1.cmake_minimum_required(VERSION 3.17)   指定cmake最低版本为3.17

2.project字段    指定项目名称为ch3_hello_shared、库的版本为1.0.0、用C++构建。

3.set(CMAKE_DEBUG_POSTFIX d)   如果编译模式为debug,将库的名字后面加上d,我们改称后缀名为s试一试,果然后缀有一个s。

cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING=Release -S . -B ./build
cmake --build ./build

4.add_library(ch3_hello_shared SHARED)   创建共享库ch3_hello_shared

5.set_target_properties    有几种“重载”用法:

用法1是更改库的默认名称例如 Linux 上的 lib<name>.so 和 Windows 上的 <name>.lib 或 <name>.dll,可以通过修改默认行为更改库的名字,比如

set_target_properties(
   ch3_hello_shared
   PROPERTIES OUTPUT_NAME hello
)

将输出文件的名字由ch3_hello.dll变成了hello.dll。

第二种重载用法是共享库的命名约定,将版本添加到文件名中指定构建版本和api版本,为目标设置属性。 VERSION 将库版本设置为项目版本 * SOVERSION 将库的兼容版本设置为版本的主编号。
//Value Computed by CMake
CMAKE_PROJECT_VERSION:STATIC=1.0.0

//Value Computed by CMake
CMAKE_PROJECT_VERSION_MAJOR:STATIC=1

因此共享库的名字就是libch3_hello_shared.1.0.0.shared

6.target_sources  库文件中的源文件

7.target_include_directories 设置库文件的包含目录

8.set_property  如果要设置默认隐藏性,将CXX_VISIBILITY_PRESET参数设置为hidden

9.generate_export_header :生成导出头文件

generate_export_header(
    ch3_hello_shared
    EXPORT_FILE_NAME
    export/hello/export_hello.hpp

        HELLO_EXPORT 定义将包含有关在编译库时是否要导出符号或在链接库时是否应导入符号的信息。

        hello 库的符号默认设置为隐藏。然后,使用 GenerateExportHeader 模块导入的 generate_export_header 宏再次单独启用它们。

        此命令在 CMAKE_CURRENT_BINARY_DIR 中生成一个头文件,该文件根据编译器设置设置可见性属性。

10.target_include_directories 包含这个导出头文件,所有符号均可见。

target_include_directories(
    ch3_hello_shared
    PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/export"
)

这个导出文件是在build/export目录。9、10用的均是相对于CMAKE_CURRENT_BINARY_DIR的目录。

1.4.5 用cmake构建静态库(Build static library with cmake)​​​​​​​

1.目录树解析

2.代码解析 

        看它的代码,了解它做了什么:

 hello.hpp:在命名空间hello下声明一个Hello类,有一个Hello构造函数、一个greet函数、一个私有属性string name_。

#pragma once 

#include <string>

namespace hello{
    /// Example class that is explicitly exported into a dll
    class Hello {
        public:
        Hello(const std::string& name) : name_{name} {}

        void greet() const; 
        private:
        const std::string name_;
    };
}

hello.cpp:实现了hello.hpp的greet函数。并且调用一个外部符号。

#include <hello/hello.hpp>

#include "internal.hpp"

namespace hello{
    void Hello::greet() const {
        details::print_impl(name_);
    }
}

internal.hpp:定义了hello::details定义域下的print_impl符号

#pragma once 

#include <string>

namespace hello::details{

    void print_impl(const std::string& name);
    
}

internal.cpp:实现了print_impl

#include "internal.hpp"

#include <iostream>

namespace hello::details{ 
    void print_impl(const std::string& name)
    {
        std::cout << "Hello " << name << " from a shared library\n";
    }
}

3.cmakelists.txt解析 

看它的cmakelists.txt文件: 

# Chapter3-  Illustriating how to create a static library
#
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.17)

project(
    ch3_hello_static
    VERSION 1.0.0
    DESCRIPTION
        "A simple C++ project to demonstrate creating executables and libraries in CMake"
    LANGUAGES CXX
)

# 添加库目标
add_library(ch3_hello_static STATIC)

# 将目标的输出设置为`hello`,这样它将在windows上保存为hello.dll,在linux上保存为libhello.a
set_target_properties(
    ch3_hello_static
    PROPERTIES OUTPUT_NAME hello
)


# 为目标设置属性。 VERSION 将库版本设置为项目版本 * SOVERSION 将库的兼容版本设置为版本的主编号
set_target_properties(
    ch3_hello_static
    PROPERTIES VERSION ${PROJECT_VERSION}
               SOVERSION ${PROJECT_VERSION_MAJOR}
)

# 将源添加到库目标
target_sources(
    ch3_hello_static
    PRIVATE src/hello.cpp src/internal.cpp
)

# 定义编译该库并使其对依赖者可见所需的 C++ 标准
target_compile_features(
    ch3_hello_static
    PUBLIC cxx_std_17
)

# 设置包含目录
target_include_directories(
    ch3_hello_static
    PRIVATE src/ch3_hello_static
    PUBLIC include
)

        由于共享库的困难版本我们已经详细介绍,这里不再阐述。

        我们看一下编译后的结果:

库名字改变了并成功形成了静态库。

1.4.6 外部调用这些创建好的库(call these created libraries externally)​​​​​​​

1.我们先看目录树

liuhongwei@liuhongwei-Lenovo-Legion-R9000P2021H:~/桌面/CMake-Best-Practices-main
/chapter03$ tree
.
├── CMakeLists.txt
├── hello_header_only
│   ├── CMakeLists.txt
│   └── include
│       └── hello_header_only
│           └── hello_header_only.hpp
├── hello_object_lib
│   ├── CMakeLists.txt
│   ├── include
│   │   └── hello_object
│   │       └── hello_object.hpp
│   └── src
│       └── hello_object.cpp
├── hello_shared_lib
│   ├── CMakeLists.txt
│   ├── include
│   │   └── hello
│   │       └── hello.hpp
│   └── src
│       ├── hello.cpp
│       ├── internal.cpp
│       └── internal.hpp
├── hello_static_lib
│   ├── CMakeLists.txt
│   ├── include
│   │   └── hello
│   │       └── hello.hpp
│   └── src
│       ├── hello.cpp
│       ├── internal.cpp
│       └── internal.hpp
├── hello_world_standalone
│   ├── CMakeLists.txt
│   └── src
│       └── main.cpp
└── src
    └── main.cpp

18 directories, 19 files

2.main.cpp

#include <hello/hello.hpp>
#include <hello_header_only/hello_header_only.hpp>
#include <hello_object/hello_object.hpp>

int main(int, char **) {
  hello_header_only::print_hello("John Doe");
  hello::Hello hello("Jane Doe");
  hello.greet();
  return 0;
}

头文件是怎么导入的?

<hello/hello.hpp>是导入静态库(静态库的target_include_directories里声明了对外界的接口),因为静态库的include下有hello目录,hello目录里面包含hello.hpp。

其他两条比较好理解不再阐述。

3.cmakelists.txt解析

# Top-level CMakeLists file for the Chapter 3 example content.
#
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.21)

project(
    chapter_3
    VERSION 1.0
    DESCRIPTION
        "A collection of sample C++ applications and libraries to demonstrate creating libraries and executables"
    LANGUAGES CXX
)

# 添加带有示例的子目录
add_subdirectory(hello_world_standalone)
add_subdirectory(hello_shared_lib)
add_subdirectory(hello_static_lib)
add_subdirectory(hello_header_only)
add_subdirectory(hello_object_lib)

# 添加示例可执行文件
add_executable(chapter3)

# 将源代码添加到示例可执行文件
target_sources(chapter3 PRIVATE src/main.cpp)

# 将库“hello”和“hello_header_only”链接到示例可执行文件,这些库在子目录中进行了描述
target_link_libraries(chapter3 PRIVATE ch3_hello_header_only ch3_hello_shared ch3_hello_object)

1.cmake_minimum_required 定义cmake最低版本

2. add_subdirectory 添加库的目录

3.target_link_libraries 将项目与库关联

  • 10
    点赞
  • 21
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Courage2022

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值