65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Conan 和 CMake 管理 C++ 项目

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2024年8月6日

CPOL

15分钟阅读

viewsIcon

12875

downloadIcon

293

本文介绍了如何通过结合 Conan 和 CMake 来自动化和简化 C++ 项目的构建。

C++ 开发者们都是非常独立的群体。我们处理内存管理,自己进行垃圾回收,从不牺牲性能来换取安全性。在依赖管理方面,情况也是如此。如果一个项目需要一套复杂的相互依赖的库,大多数 C++ 开发者会选择编写 makefile,而不是依赖一些奇怪的依赖管理工具。

当我第一次听说 Conan 时,我觉得它没必要。但随着我的依赖树变成了一片依赖的森林,我变得越来越感兴趣。Conan 真的能自动下载和安装库吗?通过指定版本?按照特定顺序?还能处理依赖项之间的依赖关系吗?

Conan 可以做到这一切,本文的目标就是展示如何将 CMake 和 Conan 结合起来。我将从对 CMake 的基本介绍开始,然后展示如何使用 Conan 来管理 CMake 构建的包。

1. CMake 概述

在处理嵌入式项目或仅限 Linux 的可执行文件时,我更喜欢 GNU makefile 的简洁。对于更复杂的情况,我使用 CMake。使用 CMake,可以轻松地为多个操作系统构建多个目标配置。它学习起来并不容易,但其功能足以弥补不便之处。

CMake 可在此处免费下载。对于大多数项目,使用 CMake 涉及三个步骤:

  1. 在 CMakeLists.txt 中定义构建指令。
  2. 执行 cmake 命令来读取 CMakeLists.txt 并生成构建系统。
  3. 执行 cmake --build 命令来从构建系统中构建目标。

本节将逐步介绍这些步骤,并展示如何使用它们来构建一个名为 hello_cmake 的简单项目。

1.1 编写 CMakeLists.txt

CMakeLists.txt 中的指令是通过称为命令的语句提供的。关于命令有三点需要了解:

  • 每个命令都有一个名称,后面跟着零个或多个括号内的参数。
  • 命令的参数之间用空格分隔,而不是逗号。
  • 控制流依赖于开始/结束命令,而不是花括号:if()/endif()while()/endwhile()foreach()/endforeach() 等等。

我不能代表所有开发者发言,但我的 CMakeLists.txt 文件中的命令通常执行四个主要步骤:

  1. 设置项目范围的属性。
  2. 定义变量。
  3. 创建目标。
  4. 设置目标属性。

本讨论将介绍每个步骤所需的命令,然后我将解释 CMake 如何使用这些命令来构建目标。

第一部分:项目范围的属性

在我所有的项目中,CMakeLists.txt 都以相同的两个命令开始:

  • cmake_minimum_required(...) — 设置可用于构建项目的最低 CMake 版本。
  • project(...) — 定义项目属性,包括其名称和编程语言(可选)。

如果您查看本文提供的 hello_cmake 项目,您会发现 CMakeLists.txt 文件以以下两行开头:

# Set the minimum version of CMake
cmake_minimum_required(VERSION 3.12)

# Set the project's name and properties
project(hello_cmake_project LANGUAGES CXX)

project 命令有三个参数,但只需要第一个。这会将项目的名称标识为 hello_cmake_project

参数 CXX 是可选的。它前面加上 LANGUAGES 关键字,以标识它是项目的编程语言。这种关键字和参数的组合可能会令人困惑。但总的来说,关键字通常大写,而参数通常不大写。

第二部分:变量

set 命令定义一个可在整个构建文件中使用的变量。此命令最简单的形式接受两个参数:变量的名称和变量的值。

为了演示,以下命令创建了两个变量,用于标识目录名称:一个名为 SOURCE_DIR,另一个名为 INCLUDE_DIRSOURCE_DIR 的值为 srcINCLUDE_DIR 的值为 include

# Create a variable named SOURCE_DIR
set(SOURCE_DIR src)

# Create a variable named INCLUDE_DIR
set(INCLUDE_DIR include)

一旦定义了变量,它的值就可以通过 ${NAME} 访问,其中 NAME 是变量的名称。根据前面的代码,src 目录可以作为 ${SOURCE_DIR} 访问,include 目录可以作为 ${INCLUDE_DIR} 访问。

第三部分:目标

目标是通过构建创建的文件。CMake 支持两种类型的目标:可执行文件和库。定义可执行目标的命令是 add_executable,定义库目标的命令是 add_library

在 hello_cmake 项目中,构建文件定义了一个名为 hello_cmake 的可执行目标,并将其与 SOURCE_DIR 文件夹中的 main.cpp 源文件相关联。这通过以下命令实现:

# Define a target executable
add_executable(hello_cmake ${SOURCE_DIR}/main.cpp)

由于此命令,hello_cmake 将是构建生成的**可执行文件**的名称。它也将是在设置目标特定属性的命令中目标的标识符。

第四部分:目标属性

在定义目标之后,构建文件可以通过多种方式设置其属性。一种方法是调用 set_target_properties 并使用标识要设置属性的关键字。另一种方法是调用设置单个属性的命令。以下是其中五个命令:

  • target_compile_options(target, ...) — 设置编译选项。
  • target_include_directories(target, ...) — 设置包含头文件的目录。
  • target_link_directories(target, ...) — 设置包含库的目录。
  • target_link_libraries(target, ...) — 标识目标所需的库。
  • target_link_options(target, ...) — 设置链接所需库的选项。

hello_cmake 项目没有任何库,因此需要设置的唯一属性是编译选项和包含文件夹(由 INCLUDE_DIR 变量给出)。这通过以下代码完成:

# Set compile options for the target
target_compile_options(hello_cmake PUBLIC -Wall -std=c++17)

# Set the include directories for the target
target_include_directories(hello_cmake PUBLIC ${INCLUDE_DIR})

在这两个命令中,第二个选项可以设置为三个值之一,用于标识命令的范围:

  • PUBLIC — 属性为给定目标及其使用给定目标的任何目标设置。
  • PRIVATE — 属性仅为给定目标设置。
  • INTERFACE — 属性为使用给定目标的目标设置,而不是为给定目标本身设置。

我倾向于在所有构建文件中使用 PUBLIC,并且我相信有一天我会为此感到后悔。

1.2 生成构建系统

安装 CMake 后,您就可以在命令行上执行 cmake 工具。它接受几个选项和标志,但其最重要的作用是读取 CMakeLists.txt 并生成一组文件和文件夹。CMake 的文档将这些文件和文件夹称为构建系统

为 hello_cmake 项目生成构建系统需要五个步骤:

  1. 将 example_code.zip 下载到您的计算机并解压该存档。
  2. 打开终端或命令提示符,然后更改到 hello_cmake 目录。
  3. 使用命令创建 build 目录:mkdir build
  4. 使用命令更改到 build 目录:cd build
  5. 使用命令生成构建系统:cmake ..

当执行最后一步时,CMake 将在 build 文件夹内创建构建系统。在 Linux/macOS 系统上,该文件夹将包含一个文件夹和三个文件:

  • Makefile — 一个 GNU makefile,用于构建 hello_cmake 可执行文件。
  • cmake_install.cmake — 定义安装过程(与构建相对)。
  • CMakeCache.txt — 设置在构建过程中使用的名称/值对。
  • CMakefiles — 包含额外的 CMake 文件。

生成的**文件**取决于您的操作系统和 CMake 版本,大多数开发者不需要了解它们。

1.3 构建目标

构建的最后一步是最简单的。如果构建系统已成功创建,并且您位于包含生成文件的目录中,则可以使用以下命令执行构建:

cmake --build .

这会告诉 CMake 在当前目录中查找合适的 makefile 并执行其指令。如果您在 hello_cmake/build 文件夹中运行此命令,CMake 将编译并链接 hello_cmake 可执行文件。

2. 理解包

前面的讨论解释了如何构建一个简单的 C++ 项目,但现实世界的项目包含库。为了处理这些依赖项,需要更新 CMakeLists.txt 文件。

如果库文件在给定位置可用,很容易告诉 CMake 如何将其链接到目标。例如,假设目标 foo 需要 bar 库,该库位于 baz 文件夹中。以下命令告诉 CMake 如何将库链接到目标:

# Identify the libraries required by the target
target_link_libraries(foo, bar)

# Identify the location of the target's libraries
target_link_directories(foo, baz)

与其跟踪库的位置,不如使用。CMake 包包含头文件和库,以及提供元数据的 CMake 特有文件。使用包有三个主要优点:

  1. 模块化 - 包的库和头文件始终分开存放。
  2. 可重用性 - 没有硬编码的依赖项位置,因此 CMakeLists.txt 可以与不同的操作系统一起使用。
  3. 版本管理 - 易于跟踪包版本并根据需要进行更新。

当您访问包中的依赖项时,CMakeLists.txt 文件需要进行三处更改:

  • find_package 命令告诉 CMake 搜索一个包。
  • target_link_libraries 命令应指定库应通过其包访问。
  • 对于包中包含的库和头文件,不需要 target_include_directoriestarget_link_directories

当 CMake 执行 find_package 时,它会搜索一系列目录来查找给定的包。例如,以下命令告诉 CMake 查找名为 mypack 的包,如果找不到则报错:

find_package(mypack REQUIRED)

如果一个库应该通过包访问,target_link_libraries 的第二个参数应该同时标识包和库。例如,以下命令告诉 CMake 在构建目标 foo 时在 mypack 包中查找 bar 库:

target_link_libraries(foo, mypack::bar)

为了获得更实际的示例,请查看 read_json 项目中的 CMakeLists.txt 文件。此项目中的代码依赖于 Niels Lohmann 在其Github 页面上提供的 JSON 库。目标可执行文件的名称是 read_json,库的名称是 nlohmann_json,因此 CMakeLists.txt 文件包含以下命令:

# Tells CMake to find the package named nlohmann_json
find_package(nlohmann_json REQUIRED)

# Tells CMake to access the nlohmann_json library in the nlohmann_json package
target_link_libraries(read_json nlohmann_json::nlohmann_json)

第一个命令告诉 CMake 搜索 nlohmann_json 包,如果找不到则报错。第二个命令告诉 CMake 在 nlohmann_json 包中查找 nlohmann_json 库,并将其链接到 read_json 目标的构建中。

这就引出了一个重要问题。read_json 项目文件夹不包含任何库,那么 CMake 如何找到这个包?答案涉及 Conan。

3. 使用 Conan 管理包

我遇到过一些 C++ 包管理工具,包括 Spack 和 vcpkg。但我发现 Conan 是其中最简单的。与 CMake 一样,它是免费提供的。主站点是 conan.io,安装说明位于 https://docs.conan.org.cn/2/installation.html

同样,Conan 也通过一个在命令行上运行的实用程序进行访问。该实用程序的名称是 conan,它可以在许多命令中使用。表 1 列出了其中的六个。

表 1:Conan 命令
命令 描述
conan version 提供版本信息。
conan profile 配置系统配置文件。
conan remote 访问 Conan 存储库。
conan download 下载包而不安装。
conan install 从配方安装包。
conan build 安装依赖项并运行 build 方法。

第一个命令 conan version 最简单。它会打印 Conan 版本、Conan 实用程序的路径以及有关 Python 安装的信息。

3.1 配置文件配置

为了管理依赖项,Conan 需要有关操作系统和已安装构建工具的信息。此信息称为配置文件,您可以使用以下命令为您的系统创建配置文件:

conan profile detect

运行此命令后,Conan 会创建一个包含开发系统参数的文件。在我的 Linux 系统上,该文件的名称是 default,它存储在 ~/.conan2/profiles 文件夹中。其内容如下:

[settings]
arch=x86_64
build_type=Release
compiler=gcc
compiler.cppstd=gnu17
compiler.libcxx=libstdc++11
compiler.version=13
os=Linux

在我的 Windows 系统上,运行 conan profile detect 会在 C:\Users\username\.conan2\profiles 文件夹中创建一个名为 default 的文件。其内容如下:

[settings]
arch=x86_64
build_type=Release
compiler=msvc
compiler.cppstd=14
compiler.runtime=dynamic
compiler.version=194
os=Windows

由于存在这些信息,Conan 就知道需要安装和管理哪种类型的包。

3.2 下载包

使用 Conan 的主要优势在于它可以下载、安装和跟踪包,从而使开发者不必这样做。Conan 从远程存储库下载包,以下命令列出了可用的存储库:

conan remote list

当我执行此命令时,它会产生以下结果:

conancenter: https://center.conan.io [Verify SSL: True, Enabled: True]

这表明 Conan 可以从 ConanCenter 下载包,这是 Conan 的默认存储库。它存储了各种适用于不同操作系统的包,您可以在 https://conan.org.cn/center 上浏览此存储库。

您可以通过运行 conan download 来测试下载过程,这需要包的名称,后跟 -r 和存储库的名称。例如,以下代码从 ConanCenter 下载 gtest 包:

conan download gtest -r conancenter

如果 Conan 能够找到该包,它会将该包的文件存储在您主目录下的 .conan2 文件夹中。包文件夹的名称以包名开头,后跟一个唯一的数字标识符。

3.3 安装包

与其使用 conan download,不如将所需包列在一个名为配方的文件中,然后运行 conan install。此命令会读取配方,将包下载到 ~/.conan2/p 文件夹中,并生成与构建系统接口所需的文件。

Conan 识别两种类型的配方:

  • conanfile.txt — 简单的文本文件,在各个部分提供信息。
  • conanfile.py — Python 文件,可用于复杂的包管理操作。

本讨论重点介绍 conanfile.txt,它比 conanfile.py 更简单但灵活度较低。此文件在**一个或多个部分**下列出信息,每个部分名称都用方括号括起来。在 read_json 项目中,conanfile.txt 的内容包含在三个部分中:

[requires]
nlohmann_json/3.11.3

[generators]
CMakeDeps
CMakeToolchain

[layout]
cmake_layout

第一部分 requires 标识需要安装的每个包的名称和版本。在本例中,项目需要 nlohmann_json 包的版本 3.11.3,因此 requires 部分包含 nlohmann_json/3.11.3

第二部分 generators 包含告诉 Conan 在安装过程中应创建哪些文件的关键字。在此示例中,有两个关键字:

  1. CMakeDeps — 为每个依赖项创建一个名为 xyz-config.cmake 的文件,其中 xyz 是包的名称。
  2. CMakeToolchain — 创建一个名为 conan_toolchain.cmake 的文件,CMake 可以在构建过程中访问该文件。

最后一部分 layout 告诉 Conan 在生成文件时应使用的目录结构。如果设置为 cmake_layout,Conan 将创建一个 build 目录并生成方便 CMake 构建的文件。

3.4 Conan/CMake 项目示例

example_code.zip 中的第二个示例项目是 read_json。它使用 nlohmann_json 包提供的功能来解析 JSON 文本。因此,CMakeLists.txt 文件包含以下命令:

find_package(nlohmann_json REQUIRED)

该项目还有一个名为 conanfile.txt 的文件,该文件将 nlohmann_json 列为必需的包。当 Conan 执行安装时,它将读取此文件,下载并安装 nlohmann_json 包,并生成 CMake 构建所需的文件。启动安装的命令如下:

conan install .

因为 conanfile.txt 指定了 CMake 布局,Conan 将创建一个名为 build 的文件夹,一个名为 Release 的子文件夹,以及一个名为 generators 的孙子文件夹。build/Release/generators 目录包含几个生成的文件,例如 nlohmann_json-config.cmake,它告诉 CMake 如何访问 Conan 安装的 nlohmann_json 包。

一旦 Conan 完成安装包并为 CMake 生成文件,以下命令(在 build 文件夹中执行)将指示 CMake 生成构建文件:

cmake .. -DCMAKE_TOOLCHAIN_FILE="Release/generators/conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release

在此命令中,CMAKE_TOOLCHAIN_FILE 选项告诉 CMake 读取 Release/generators 目录中的 conan_toolchain.cmake 文件。执行此命令后,可以使用以下命令构建项目:

cmake --build .

如果此命令成功完成,构建将生成一个名为 read_json 的可执行文件。运行时,该可执行文件将解析 JSON 文本并将部分数据打印到标准输出。

4. 在 Python 中访问 Conan 和 CMake

前面的讨论解释了如何通过编写 conanfile.txt 配方来配置 Conan。这对于简单操作来说还可以,但为了获得更大的灵活性,我建议在第二种类型的 Conan 配方:conanfile.py 中添加方法。

本节解释如何访问 conanfile.py 中的 Conan 和 CMake,但在我们开始编写代码之前,最好先安装所需的库:

pip install conan

本节解释如何访问这两个库的功能。我们将研究 XYZ...

但首先,从一个简单的例子开始会很有帮助。

4.1 简单示例

前面,我解释了 conanfile.txt 的各个部分(requiresgeneratorlayout)如何告诉 Conan 如何管理构建过程。conanfile.py 提供了相同的信息,如下面的代码所示:

from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout

class SimpleExample(ConanFile):
    settings = 'os', 'compiler', 'build_type', 'arch'

    def requirements(self):
        self.requires('nlohmann_json/3.11.3')

    def generate(self):
        deps = CMakeDeps(self)
        deps.generate()
        tc = CMakeToolchain(self)
        tc.generate()

    def layout(self):
        cmake_layout(self)

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

这段代码创建了一个名为 SimpleExample 的类,该类继承自 ConanFile。它首先定义一个名为 settings 的变量,然后定义四个方法:

  1. requirements - 需要安装的依赖项。
  2. generate - 为构建生成必要的文件。
  3. layout - 设置生成文件的目录结构。
  4. build - 执行构建。

前三个方法应该看起来很熟悉,因为它们包含与 conanfile.txt 的部分相同的信息。最后一个方法访问 CMake 来构建项目。使用 conanfile.py 后,您可以通过执行 conan install . 来安装依赖项,然后通过在命令行上执行 conan build . 来运行构建。

4.2 创建和上传包

上一节解释了包是什么以及如何使用 Conan 下载和安装它们。使用 conanfile.py 而不是 conanfile.txt 的一个优点是您可以轻松地创建新包并将其上传到远程存储库。

创建包的一般过程可以通过向 conanfile.py 添加以下方法来定义:

  • source 方法下载和修改源文件。
  • requirements 方法标识需要安装的包。
  • generate 方法创建构建过程所需的文件。
  • configureconfig_options 方法设置构建过程中使用的变量。
  • build 方法启动构建过程。
  • package 方法标识要包含在包中的文件。
  • package_info 方法中提供包元数据。

如果定义了这些方法,您可以通过在包含 conanfile.py 的目录中执行 conan new 来创建一个新包。

创建包后,可以通过执行 conan upload 将其上传到远程存储库。它接受一个 -r 标志,用于标识远程存储库。例如,以下命令将名为 my_package 的包上传到名为 my_repo 的存储库:

conan upload my_package -r=my_repo

请记住,您可以通过执行命令 conan remote list 来获取可用存储库的列表。您可以通过阅读 Conan 的官方文档来获取更多信息。

历史

本文于 2024 年 8 月 6 日首次发布。于 2024 年 9 月 2 日添加了内容。

© . All rights reserved.