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





5.00/5 (3投票s)
本文介绍了如何通过结合 Conan 和 CMake 来自动化和简化 C++ 项目的构建。
C++ 开发者们都是非常独立的群体。我们处理内存管理,自己进行垃圾回收,从不牺牲性能来换取安全性。在依赖管理方面,情况也是如此。如果一个项目需要一套复杂的相互依赖的库,大多数 C++ 开发者会选择编写 makefile,而不是依赖一些奇怪的依赖管理工具。
当我第一次听说 Conan 时,我觉得它没必要。但随着我的依赖树变成了一片依赖的森林,我变得越来越感兴趣。Conan 真的能自动下载和安装库吗?通过指定版本?按照特定顺序?还能处理依赖项之间的依赖关系吗?
Conan 可以做到这一切,本文的目标就是展示如何将 CMake 和 Conan 结合起来。我将从对 CMake 的基本介绍开始,然后展示如何使用 Conan 来管理 CMake 构建的包。
1. CMake 概述
在处理嵌入式项目或仅限 Linux 的可执行文件时,我更喜欢 GNU makefile 的简洁。对于更复杂的情况,我使用 CMake。使用 CMake,可以轻松地为多个操作系统构建多个目标配置。它学习起来并不容易,但其功能足以弥补不便之处。
CMake 可在此处免费下载。对于大多数项目,使用 CMake 涉及三个步骤:
- 在 CMakeLists.txt 中定义构建指令。
- 执行
cmake
命令来读取 CMakeLists.txt 并生成构建系统。 - 执行
cmake --build
命令来从构建系统中构建目标。
本节将逐步介绍这些步骤,并展示如何使用它们来构建一个名为 hello_cmake 的简单项目。
1.1 编写 CMakeLists.txt
CMakeLists.txt 中的指令是通过称为命令的语句提供的。关于命令有三点需要了解:
- 每个命令都有一个名称,后面跟着零个或多个括号内的参数。
- 命令的参数之间用空格分隔,而不是逗号。
- 控制流依赖于开始/结束命令,而不是花括号:
if()
/endif()
、while()
/endwhile()
、foreach()
/endforeach()
等等。
我不能代表所有开发者发言,但我的 CMakeLists.txt 文件中的命令通常执行四个主要步骤:
- 设置项目范围的属性。
- 定义变量。
- 创建目标。
- 设置目标属性。
本讨论将介绍每个步骤所需的命令,然后我将解释 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_DIR
。SOURCE_DIR
的值为 src
,INCLUDE_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 项目生成构建系统需要五个步骤:
- 将 example_code.zip 下载到您的计算机并解压该存档。
- 打开终端或命令提示符,然后更改到 hello_cmake 目录。
- 使用命令创建
build
目录:mkdir build
- 使用命令更改到 build 目录:
cd build
- 使用命令生成构建系统:
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 特有文件。使用包有三个主要优点:
- 模块化 - 包的库和头文件始终分开存放。
- 可重用性 - 没有硬编码的依赖项位置,因此 CMakeLists.txt 可以与不同的操作系统一起使用。
- 版本管理 - 易于跟踪包版本并根据需要进行更新。
当您访问包中的依赖项时,CMakeLists.txt 文件需要进行三处更改:
find_package
命令告诉 CMake 搜索一个包。target_link_libraries
命令应指定库应通过其包访问。- 对于包中包含的库和头文件,不需要
target_include_directories
和target_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 列出了其中的六个。
命令 | 描述 |
---|---|
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 在安装过程中应创建哪些文件的关键字。在此示例中,有两个关键字:
CMakeDeps
— 为每个依赖项创建一个名为xyz-config.cmake
的文件,其中xyz
是包的名称。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 的各个部分(requires
、generator
和 layout
)如何告诉 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
的变量,然后定义四个方法:
requirements
- 需要安装的依赖项。generate
- 为构建生成必要的文件。layout
- 设置生成文件的目录结构。build
- 执行构建。
前三个方法应该看起来很熟悉,因为它们包含与 conanfile.txt 的部分相同的信息。最后一个方法访问 CMake 来构建项目。使用 conanfile.py 后,您可以通过执行 conan install .
来安装依赖项,然后通过在命令行上执行 conan build .
来运行构建。
4.2 创建和上传包
上一节解释了包是什么以及如何使用 Conan 下载和安装它们。使用 conanfile.py 而不是 conanfile.txt 的一个优点是您可以轻松地创建新包并将其上传到远程存储库。
创建包的一般过程可以通过向 conanfile.py 添加以下方法来定义:
source
方法下载和修改源文件。requirements
方法标识需要安装的包。generate
方法创建构建过程所需的文件。configure
和config_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 日添加了内容。