C++包管理工具:conan2使用教程
本部分的目的是通过实际示例引导你了解Conan的核心功能:从使用Conan打包的现成库,到如何封装自己的库并将其与所有预编译的二进制文件一同存储到远程服务器上。
1. 教程 (TUTORIAL)
本章节旨在通过实际示例引导您了解 Conan 的核心功能。从使用 Conan 中心或其他来源提供的预打包库,到如何打包您自己的库并将其与预编译的二进制文件一起存储在远程服务器上。
1.1 使用包 (Consuming packages)
本节展示如何使用 Conan 管理依赖项来构建您的项目。我们将从一个使用 CMake 并依赖 zlib 库的基本 C 项目示例开始。该项目将使用 conanfile.txt
文件声明其依赖项。
我们还将介绍如何不仅使用“常规”库,还能管理构建过程中可能需要的工具:如 CMake、msys2、MinGW 等。
接着,我们将解释 Conan 的概念,如设置 (settings
) 和选项 (options
),以及如何使用它们为不同的配置(如 Debug、Release、静态或共享库等)构建项目。
我们还将解释如何从第一个示例中使用的 conanfile.txt
文件过渡到功能更强大的 conanfile.py
。
之后,我们将介绍 Conan 的构建 (build
) 和主机 (host
) 配置文件的概念,并解释如何使用它们将应用程序交叉编译到不同的平台。
然后,在“版本控制介绍”部分,我们将学习使用不同版本、定义带有版本范围的依赖要求、修订 (revisions
) 的概念以及用于实现依赖图可重现性的锁定文件 (lockfiles
) 简介。
1.1.1 使用 Conan 构建一个简单的 CMake 项目
让我们从一个示例开始:我们将创建一个使用最流行的 C++ 库之一 Zlib 的字符串压缩应用程序。
在本例中,我们将使用 CMake 作为构建系统,但请注意,Conan 适用于任何构建系统,并不限于 CMake。您可以在“更多阅读”部分查看使用其他构建系统的更多示例。
请首先克隆源代码以重现此项目,您可以在 GitHub 的 examples2
仓库中找到它们:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/consuming_packages/simple_cmake_project
我们从一个非常简单的 C 语言项目开始,其结构如下:
.
├── CMakeLists.txt
├── conanfile.txt
└── src└── main.c
该项目包含一个基本的 CMakeLists.txt
(包含 zlib 依赖项)和位于 main.c
中的字符串压缩程序源代码。
让我们看看 main.c
文件的内容:
清单 1: main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>int main(void) {char buffer_in [256] = {"Conan is a C/C++ Package Manager for Developers."};char buffer_out [256] = {0};z_stream defstream;defstream.zalloc = Z_NULL;defstream.zfree = Z_NULL;defstream.opaque = Z_NULL;defstream.avail_in = (uInt) strlen(buffer_in);defstream.next_in = (Bytef *) buffer_in;defstream.avail_out = (uInt) sizeof(buffer_out);defstream.next_out = (Bytef *) buffer_out;deflateInit(&defstream, Z_BEST_COMPRESSION);deflate(&defstream, Z_FINISH);deflateEnd(&defstream);printf("Uncompressed size is: %lu\n", strlen(buffer_in));printf("Compressed size is: %lu\n", defstream.total_out);printf("ZLIB VERSION: %s\n", zlibVersion());return EXIT_SUCCESS;
}
另外,CMakeLists.txt
的内容是:
清单 2: CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(compressor C)# Find the ZLIB library
find_package(ZLIB REQUIRED)add_executable(${PROJECT_NAME} src/main.c)
target_link_libraries(${PROJECT_NAME} ZLIB::ZLIB)
我们的应用程序依赖于 Zlib 库。Conan 默认尝试从名为 ConanCenter 的远程服务器安装库。您可以在那里搜索库并检查可用版本。在我们的例子中,检查了 Zlib 的可用版本后,我们选择使用其中一个最新版本:zlib/1.2.11
。
使用 Conan 安装 Zlib 库并从我们的项目中找到它的最简单方法是使用 conanfile.txt
文件。让我们创建一个包含以下内容的文件:
清单 3: conanfile.txt
[requires]
zlib/1.2.11[generators]
CMakeDeps
CMakeToolchain
如您所见,我们向该文件添加了两个部分,语法类似于 INI 文件。
[requires]
部分是我们声明要在项目中使用的库的地方,本例中是zlib/1.2.11
。[generators]
部分告诉 Conan 生成编译器或构建系统将用于查找依赖项和构建项目的文件。在本例中,由于我们的项目基于 CMake,我们将使用CMakeDeps
生成有关 Zlib 库文件安装位置的信息,并使用CMakeToolchain
通过 CMake 工具链文件将构建信息传递给 CMake。
除了 conanfile.txt
,我们还需要一个 Conan 配置文件 (profile
) 来构建我们的项目。Conan 配置文件允许用户定义一组配置,如编译器、构建配置、架构、共享或静态库等。Conan 默认不会自动检测配置文件,因此我们需要创建一个。要让 Conan 尝试根据当前操作系统和已安装的工具猜测配置文件,请运行:
conan profile detect --force
这将根据环境检测操作系统、构建架构和编译器设置。它还会默认将构建配置设置为 Release
。生成的配置文件将以名称 default
存储在 Conan 主目录 (home
) 中,并且将被 Conan 在所有命令中默认使用,除非通过命令行指定另一个配置文件。此命令在 macOS 上的输出示例如下:
$ conan profile detect --force
Found apple-clang 14.0
apple-clang>=13, using the major as version
Detected profile:
[settings]
arch=x86_64
build_type=Release
compiler=apple-clang
compiler.cppstd=gnu17
compiler.libcxx=libc++
compiler.version=14
os=Macos
注意:关于 Conan 检测到的 C++ 标准的说明
Conan 将始终将默认的 C++ 标准设置为检测到的编译器版本默认使用的标准,但 macOS 使用 apple-clang
的情况除外。对于 apple-clang>=11
,它设置 compiler.cppstd=gnu17
。如果您想使用不同的 C++ 标准,可以直接编辑默认配置文件。首先,使用以下命令获取默认配置文件的位置:
$ conan profile path default
/Users/user/.conan2/profiles/default
然后打开并编辑该文件,将 compiler.cppstd
设置为您想要的 C++ 标准。
注意:使用非自动检测的编译器
如果您想更改 Conan 配置文件以使用默认编译器以外的编译器,您需要更改编译器设置,并使用 tools.build:compiler_executables
配置明确告诉 Conan 在哪里找到它。
我们将使用 Conan 安装 Zlib 并生成 CMake 查找该库和构建项目所需的文件。我们将在 build
文件夹中生成这些文件。为此,请运行:
$ conan install . --output-folder=build --build=missing
您将获得类似于此命令输出的内容:
$ conan install . --output-folder=build --build=missing
...
Computing dependency graph
zlib/1.2.11: Not found in local cache, looking in remotes...
zlib/1.2.11: Checking remote: conancenter
zlib/1.2.11: Trying with 'conancenter'...
Downloading conanmanifest.txt
Downloading conanfile.py
Downloading conan_export.tgz
Decompressing conan_export.tgz
zlib/1.2.11: Downloaded recipe revision f1fadf0d3b196dc0332750354ad8ab7b
Graph root
conanfile.txt: /home/conan/examples2/tutorial/consuming_packages/simple_cmake_project/conanfile.txt
Requirementszlib/1.2.11#f1fadf0d3b196dc0332750354ad8ab7b - Downloaded (conancenter)
Computing necessary packages
--------
Requirementszlib/1.2.11#f1fadf0d3b196dc0332750354ad8ab7b:cdc9a35e010a17fc90bb845108cf86cfcbce64bf#dd7bf2a1ab4eb5d1943598c09b616121 - Download (conancenter)
Installing packages
Installing (downloading, building) binaries...
zlib/1.2.11: Retrieving package cdc9a35e010a17fc90bb845108cf86cfcbce64bf from remote 'conancenter'
Downloading conanmanifest.txt
Downloading conaninfo.txt
Downloading conan_package.tgz
Decompressing conan_package.tgz
zlib/1.2.11: Package installed cdc9a35e010a17fc90bb845108cf86cfcbce64bf
zlib/1.2.11: Downloaded package revision dd7bf2a1ab4eb5d1943598c09b616121
Finalizing install (deploy, generators)
--------
conanfile.txt: Generator 'CMakeToolchain' calling 'generate()'
conanfile.txt: Generator 'CMakeDeps' calling 'generate()'
conanfile.txt: Generating aggregated env files
如输出所示,发生了以下几件事:
- Conan 从远程服务器(应该是默认的 Conan Center 服务器,如果该库可用的话)安装了 Zlib 库。该服务器存储 Conan 配方(定义如何构建库的文件)和可以重用的二进制文件,这样我们就不必每次都从源代码构建。
- Conan 在
build
文件夹下生成了几个文件。这些文件是由我们在conanfile.txt
中设置的CMakeToolchain
和CMakeDeps
生成器生成的。CMakeDeps
生成文件以便 CMake 找到我们刚刚下载的 Zlib 库。另一方面,CMakeToolchain
为 CMake 生成一个工具链文件,以便我们可以透明地使用 CMake 构建项目,并使用我们为默认配置文件检测到的相同设置。
现在,我们准备好构建并运行我们的压缩应用程序了:
清单 4: Windows
$ cd build
# 假设 Visual Studio 15 2017 是您的 VS 版本,并且它与您的默认配置文件匹配
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
$ cmake --build . --config Release
[100%] Built target compressor
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
清单 5: Linux, macOS
$ cd build
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
$ cmake --build .
...
[100%] Built target compressor
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
请注意,CMakeToolchain
可能会生成 CMake 预设文件 (CMakePresets.json
),允许使用现代 CMake (>=3.23) 的用户使用 cmake --preset
来代替传递工具链文件参数。请参阅 使用 CMake 预设构建。
另请参阅:
- 使用 CMake 预设构建
- Autotools 入门
- Meson 入门
- Bazel 入门
- Bazel 7.x 入门
1.1.2 将构建工具作为 Conan 包使用
在之前的示例中,我们构建了我们的 CMake 项目,并使用 Conan 来安装和定位 Zlib 库。我们使用了系统中已安装的 CMake 来构建我们的压缩器二进制文件。但是,如果您想使用与系统范围内已安装版本不同的特定 CMake 版本来构建我们的项目,该怎么办?Conan 也可以帮助您安装这些工具,并使用它们来编译消费者项目或其他 Conan 包。在这种情况下,您可以在 Conan 中使用一种称为 tool_requires
的特殊类型依赖项来声明此依赖项。让我们看一个如何向我们的项目添加 tool_requires
并使用不同版本的 CMake 来构建它的示例。
请首先克隆源代码以重现此项目。您可以在 GitHub 的 examples2
仓库中找到它们:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/consuming_packages/tool_requires
项目的结构与上一个示例相同:
.
├── conanfile.txt
├── CMakeLists.txt
└── src└── main.c
主要区别在于 conanfile.txt
文件中添加了 [tool_requires]
部分。在此部分中,我们声明我们希望使用 CMake v3.22.6 来构建我们的应用程序。
清单 6: conanfile.txt
[requires]
zlib/1.2.11[tool_requires]
cmake/3.22.6[generators]
CMakeDeps
CMakeToolchain
重要提示: 请注意,此 conanfile.txt
将分别安装 zlib/1.2.11
和 cmake/3.22.6
。但是,如果 Conan 在 Conan Center 中找不到 Zlib 的二进制文件并且需要从源代码构建,则您的系统上必须已经安装了 CMake,因为 conanfile.txt
中声明的 cmake/3.22.6
仅适用于您的当前项目,而不是所有依赖项。如果您想使用该 cmake/3.22.6
也来构建 Zlib(如果需要),您可以将 [tool_requires]
部分添加到您使用的配置文件中。请查看配置文件文档 获取更多信息。
我们还在 CMakeLists.txt
中添加了一条消息来输出 CMake 版本:
清单 7: CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(compressor C)
find_package(ZLIB REQUIRED)
message("Building with CMake version: ${CMAKE_VERSION}") # 添加此行
add_executable(${PROJECT_NAME} src/main.c)
target_link_libraries(${PROJECT_NAME} ZLIB::ZLIB)
现在,与前面的示例一样,我们将使用 Conan 安装 Zlib 和 CMake 3.22.6,并生成查找它们所需的文件。我们将在 build
文件夹中生成这些文件。为此,只需运行:
$ conan install . --output-folder=build --build=missing
注意: PowerShell 用户需要在上面的命令中添加 --conf=tools.env.virtualenv:powershell=<executable>
(例如 powershell.exe
或 pwsh
)以生成 .ps1
文件而不是 .bat
文件。从 Conan 2.11.0 开始,将此配置设置为 True
或 False
已被弃用。
为避免每次都需要添加此行,我们建议在配置文件的 [conf]
部分进行配置。有关详细信息,请参阅配置文件部分。
您可以检查输出:
-------- Computing dependency graph ----------
cmake/3.22.6: Not found in local cache, looking in remotes...
cmake/3.22.6: Checking remote: conancenter
cmake/3.22.6: Trying with 'conancenter'...
Downloading conanmanifest.txt
Downloading conanfile.py
cmake/3.22.6: Downloaded recipe revision 3e3d8f3a848b2a60afafbe7a0955085a
Graph root
conanfile.txt: /Users/user/Documents/developer/conan/examples2/tutorial/consuming_→˓˓packages/tool_requires/conanfile.txt
Requirementszlib/1.2.11#f1fadf0d3b196dc0332750354ad8ab7b - Cache
Build requirementscmake/3.22.6#3e3d8f3a848b2a60afafbe7a0955085a - Downloaded (conancenter)
-------- Computing necessary packages ----------
Requirementszlib/1.2.11#f1fadf0d3b196dc0332750354ad8ab7b:2a823fda5c9d8b4f682cb27c30caf4124c5726c8→˓˓#48bc7191ec1ee467f1e951033d7d41b2 - Cache
Build requirementscmake/3.22.6→˓˓#3e3d8f3a848b2a60afafbe7a0955085a:f2f48d9745706caf77ea883a5855538256e7f2d4→˓˓#6c519070f013da19afd56b52c465b596 - Download (conancenter)
-------- Installing packages ----------
Installing (downloading, building) binaries...
cmake/3.22.6: Retrieving package f2f48d9745706caf77ea883a5855538256e7f2d4 from remote→˓˓'conancenter'
Downloading conanmanifest.txt
Downloading conaninfo.txt
Downloading conan_package.tgz
Decompressing conan_package.tgz
cmake/3.22.6: Package installed f2f48d9745706caf77ea883a5855538256e7f2d4
cmake/3.22.6: Downloaded package revision 6c519070f013da19afd56b52c465b596
zlib/1.2.11: Already installed!
-------- Finalizing install (deploy, generators) ----------
conanfile.txt: Generator 'CMakeToolchain' calling 'generate()'
conanfile.txt: Generator 'CMakeDeps' calling 'generate()'
conanfile.txt: Generating aggregated env files
现在,如果您检查文件夹,您将看到 Conan 生成了一个名为 conanbuild.sh/bat
的新文件。这是我们在 conanfile.txt
中声明 tool_requires
时自动调用 VirtualBuildEnv
生成器的结果。
此文件设置了一些环境变量,如新的 PATH
,我们可以使用它来将 CMake v3.22.6 的位置注入到我们的环境中。
激活虚拟环境,并运行 cmake --version
以检查您是否在路径中安装了新的 CMake 版本。
清单 8: Windows
$ cd build
$ conanbuild.bat
# 如果使用 Powershell 则是 conanbuild.ps1
清单 9: Linux, macOS
$ cd build
$ source conanbuild.sh
Capturing current environment in deactivate_conanbuildenv-release-x86_64.sh
Configuring environment variables
运行 cmake 并检查版本:
$ cmake --version
cmake version 3.22.6
...
如您所见,激活环境后,CMake v3.22.6 的二进制文件夹被添加到路径中,并且现在是当前激活的版本。现在您可以像之前一样构建您的项目,但这次 Conan 将使用 CMake 3.22.6 来构建它:
清单 10: Windows
# 假设 Visual Studio 15 2017 是您的 VS 版本,并且它与您的默认配置文件匹配
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
$ cmake --build . --config Release
...
[100%] Built target compressor
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
清单 11: Linux, macOS
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
$ cmake --build .
...
Building with CMake version: 3.22.6
...
[100%] Built target compressor
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
请注意,当我们激活环境时,会在同一文件夹中创建一个名为 deactivate_conanbuild.sh/bat
的新文件。如果您获取 (source
) 或运行此文件,可以恢复之前的环境。
清单 12: Windows
$ deactivate_conanbuild.bat
清单 13: Linux, macOS
$ source deactivate_conanbuild.sh
Restoring environment
运行 cmake 并检查版本,它将是环境激活之前安装的版本:
$ cmake --version
cmake version 3.22.0
...
注意:最佳实践
tool_requires
和工具包旨在用于可执行应用程序,如 cmake
或 ninja
。不要使用 tool_requires
来依赖库或类似库的依赖项。
另请参阅:
- 在配置文件中使用
[system_tools]
。 - 为
tool_requires
创建配方:打包构建工具。 - 将同一个要求同时用作
requires
和tool_requires
。 - 使用 MinGW 作为
tool_requires
在 Windows 中使用 gcc 构建。 - 在配置文件中使用
tool_requires
。 - 使用
conf
从工具要求设置工具链。 - 为多种配置构建:Release、Debug、静态和共享。
1.1.3 为多种配置构建:Release、Debug、静态和共享
请首先克隆源代码以重现此项目。您可以在 GitHub 的 examples2
仓库中找到它们:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/consuming_packages/different_configurations
到目前为止,我们构建了一个依赖于 zlib 库的简单 CMake 项目,并学习了关于 tool_requires
的知识,这是一种用于像 CMake 这样的构建工具的特殊类型要求。在这两种情况下,我们都没有在任何地方指定我们希望以 Release 或 Debug 模式构建应用程序,或者是否希望链接静态库或共享库。这是因为 Conan,如果没有特别指示,将使用在“默认配置文件”中声明的默认配置。这个默认配置文件是在第一个示例中我们运行 conan profile detect
命令时创建的。Conan 将此文件存储在位于 Conan 用户主目录 (home
) 的 /profiles
文件夹中。您可以通过运行 conan config home
命令获取 Conan 用户主目录的位置,然后显示 /profiles
文件夹中默认配置文件的内容来检查其内容:
$ conan config home
Current Conan home: /Users/tutorial_user/.conan2
# 输出文件内容
$ cat /Users/tutorial_user/.conan2/profiles/default
[settings]
os=Macos
arch=x86_64
compiler=apple-clang
compiler.version=14.0
compiler.libcxx=libc++
compiler.cppstd=gnu11
build_type=Release
[options]
[tool_requires]
[env]
# 默认配置文件也可以通过命令 "conan profile show" 查看
如您所见,配置文件有不同的部分。[settings]
部分包含有关操作系统、架构、编译器、构建配置等信息。
当您调用带有 --profile
参数的 Conan 命令时,Conan 将从配置文件中获取所有信息并将其应用于您要构建或安装的包。如果您不指定该参数,则等同于使用 --profile=default
调用它。这两个命令的行为相同:
$ conan install . --build=missing
$ conan install . --build=missing --profile=default
您可以存储不同的配置文件,并使用它们为不同的设置构建。例如,要使用 build_type=Debug
,或者向使用该配置文件构建的所有包添加 tool_requires
。我们将创建一个调试配置文件来尝试使用不同的配置进行构建:
清单 14: /profiles/debug
[settings]
os=Macos
arch=x86_64
compiler=apple-clang
compiler.version=14.0
compiler.libcxx=libc++
compiler.cppstd=gnu11
build_type=Debug
修改设置:为应用程序及其依赖项使用 Debug 配置
使用配置文件并不是设置所需配置的唯一方法。您还可以在 Conan 命令中使用 --settings
参数覆盖配置文件设置。例如,您可以使用 Debug 配置而不是 Release 来构建前面示例中的项目。
在构建之前,请检查我们修改了前面示例中的源代码,以显示源代码构建时使用的配置:
#include <stdlib.h>
...
int main(void) {
...
#ifdef NDEBUGprintf("Release configuration!\n");
#elseprintf("Debug configuration!\n");
#endifreturn EXIT_SUCCESS;
}
现在让我们为 Debug 配置构建我们的项目:
$ conan install . --output-folder=build --build=missing --settings=build_type=Debug
如上所述,这等同于拥有一个 debug 配置文件并使用 --profile=debug
参数运行这些命令,而不是使用 --settings=build_type=Debug
参数。
这个 conan install
命令将检查我们是否在本地缓存中已有 Debug 配置所需的库(Zlib),如果没有则获取它们。它还会更新 conan_toolchain.cmake
和 CMakePresets.json
文件(由 CMakeToolchain
生成器创建)中的构建配置,以便在我们构建应用程序时以 Debug 配置构建。现在像前面的示例一样构建您的项目,并在输出中检查它是如何在 Debug 配置下构建的:
清单 15: Windows
# 假设 Visual Studio 15 2017 是您的 VS 版本,并且它与您的默认配置文件匹配
$ cd build
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
$ cmake --build . --config Debug
$ Debug\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
Debug configuration!
清单 16: Linux, macOS
$ cd build
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug
$ cmake --build .
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
Debug configuration!
修改选项:将应用程序依赖项链接为共享库
到目前为止,我们一直在应用程序中静态链接 Zlib。这是因为在 Zlib 的 Conan 包中,有一个属性默认设置为以静态模式构建。我们可以通过将 shared
选项设置为 True
来从静态链接更改为共享链接,使用 --options
参数。为此,请运行:
$ conan install . --output-folder=build --build=missing --options=zlib/1.2.11:shared=True
这样做,Conan 将安装 Zlib 共享库,生成用于构建它们的文件,以及定位这些动态库所需的文件(在运行应用程序时)。
注意: 选项是按包定义的。在这种情况下,我们定义我们希望该特定版本的 zlib/1.2.11
作为共享库。如果我们有其他依赖项,并且我们希望所有依赖项(尽可能)都作为共享库,我们可以使用 -o *:shared=True
,其中 *
是匹配所有包引用的模式。
让我们在配置为将 Zlib 链接为共享库后再次构建应用程序:
清单 17: Windows
$ cd build
# 假设 Visual Studio 15 2017 是您的 VS 版本,并且它与您的默认配置文件匹配
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
$ cmake --build . --config Release
...
[100%] Built target compressor
清单 18: Linux, macOS
$ cd build
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
$ cmake --build .
...
[100%] Built target compressor
现在,如果您尝试运行编译后的可执行文件,您将看到一个错误,因为可执行文件找不到我们刚刚安装的 Zlib 共享库。
清单 19: Windows
$ Release\compressor.exe
(on a pop-up window) The code execution cannot proceed because zlib1.dll was not found.␣␣␣→˓˓Reinstalling the program may fix this problem.
# 此错误取决于所使用的控制台,可能不会总是弹出。
# 如果控制台从其他路径获取 zlib dll,它可能会正确运行。
清单 20: Linux
$ ./compressor
./compressor: error while loading shared libraries: libz.so.1: cannot open shared object␣␣␣→˓˓file: No such file or directory
清单 21: macOS
$ ./compressor
./compressor: dyld[41259]: Library not loaded: @rpath/libz.1.dylib
这是因为共享库(Windows 中的 .dll
,macOS 中的 .dylib
,Linux 中的 .so
)是在运行时加载的。这意味着应用程序可执行文件在运行时需要知道所需的共享库在哪里。在 Windows 上,动态链接器将在同一目录中搜索,然后在 PATH
目录中搜索。在 macOS 上,它将在 DYLD_LIBRARY_PATH
声明的目录中搜索。在 Linux 上,它将使用 LD_LIBRARY_PATH
。
Conan 提供了一种机制来定义这些变量,并使可执行文件能够找到并加载这些共享库。这种机制是 VirtualRunEnv
生成器。如果您检查输出文件夹,您将看到 Conan 生成了一个名为 conanrun.sh/bat
的新文件。这是我们在执行 conan install
时激活 shared
选项时自动调用该 VirtualRunEnv
生成器的结果。这个生成的脚本将设置 PATH
、LD_LIBRARY_PATH
、DYLD_LIBRARY_PATH
和 DYLD_FRAMEWORK_PATH
环境变量,以便可执行文件可以找到共享库。
激活虚拟环境,然后再次运行可执行文件:
清单 22: Windows
$ conanrun.bat
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
...
清单 23: Linux, macOS
$ source conanrun.sh
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
...
与之前使用 VirtualBuildEnv
生成器的示例一样,当我们运行 conanrun.sh/bat
脚本时,会创建一个名为 deactivate_conanrun.sh/bat
的取消激活脚本来恢复环境。获取 (source
) 或运行它以执行取消激活:
清单 24: Windows
$ deactivate_conanrun.bat
清单 25: Linux, macOS
$ source deactivate_conanrun.sh
设置 (settings
) 和选项 (options
) 之间的区别
您可能已经注意到,在更改 Debug 和 Release 配置时我们使用了 Conan 设置 (setting
),但在将可执行文件设置为共享模式时我们使用了 Conan 选项 (option
)。请注意设置和选项之间的区别:
- 设置 (
settings
) 通常是客户端机器定义的全局项目配置。例如操作系统、编译器或构建配置,这些配置对于多个 Conan 包是通用的,并且仅为其中一个定义默认值是没有意义的。例如,一个 Conan 包声明“Visual Studio”作为默认编译器是没有意义的,因为这是由最终消费者定义的,并且如果他们在 Linux 上工作,很可能没有意义。 - 选项 (
options
) 用于包特定的配置,可以在配方中设置为默认值。例如,一个包可以定义其默认链接是静态的,如果消费者没有另外指定,则应使用此链接。
引入包 ID (Package ID
) 的概念
当使用不同的设置和选项(如 Zlib)使用包时,您可能想知道 Conan 如何确定从远程获取哪个二进制文件。答案在于包 ID (package_id
) 的概念。
package_id
是 Conan 用来确定包的二进制兼容性的标识符。它是基于多个因素计算出来的,包括包的设置、选项和依赖项。当您修改这些因素中的任何一个时,Conan 会计算一个新的 package_id
来引用相应的二进制文件。
以下是该过程的分解:
- 确定设置和选项: Conan 首先检索用户的输入设置和选项。这些可以来自命令行或配置文件,如
--settings=build_type=Debug
或--profile=debug
。 - 计算包 ID: 根据设置、选项和依赖项的当前值,Conan 计算一个哈希值。此哈希值用作
package_id
,表示二进制包的唯一标识。 - 获取二进制文件: Conan 然后检查其缓存或指定的远程服务器,寻找具有计算出的
package_id
的二进制包。如果找到匹配项,则检索该二进制文件。如果没有找到,Conan 可以从源代码构建该包或指示二进制文件缺失。
在我们的教程上下文中,当我们使用不同的设置和选项使用 Zlib 时,Conan 使用 package_id
来确保它获取与我们指定的配置匹配的正确二进制文件。
另请参阅:
- VirtualRunEnv 参考
- 使用
--profile:build
和--profile:host
进行交叉编译 - Conan 包的二进制兼容性:包 ID
- 使用
conan config install
安装配置 - VS 多配置
- 设置和选项如何影响包 ID
- 为设置和选项使用模式
- 理解使用
conanfile.py
与conanfile.txt
的灵活性
1.1.4 理解使用 conanfile.py
与 conanfile.txt
的灵活性
在前面的示例中,我们在 conanfile.txt
文件中声明了我们的依赖项(Zlib 和 CMake)。让我们看看该文件:
清单 26: conanfile.txt
[requires]
zlib/1.2.11[tool_requires]
cmake/3.22.6[generators]
CMakeDeps
CMakeToolchain
使用 conanfile.txt
来使用 Conan 构建项目对于简单的情况已经足够,但如果您需要更多灵活性,您应该使用 conanfile.py
文件,您可以在其中使用 Python 代码来执行诸如动态添加要求、根据其他选项更改选项或为您的依赖项设置选项等操作。让我们看一个如何迁移到 conanfile.py
并使用其中一些功能的示例。
请首先克隆源代码以重现此项目。您可以在 GitHub 的 examples2
仓库中找到它们:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/consuming_packages/conanfile_py
检查文件夹的内容,注意内容与之前的示例相同,但使用的是 conanfile.py
而不是 conanfile.txt
。
.
├── CMakeLists.txt
├── conanfile.py
└── src└── main.c
回想一下,前面的示例中 conanfile.txt
包含以下信息:
清单 27: conanfile.txt
[requires]
zlib/1.2.11[tool_requires]
cmake/3.22.6[generators]
CMakeDeps
CMakeToolchain
我们将把相同的信息转换为 conanfile.py
。这个文件通常被称为“Conan 配方”。它可以用于使用包,如此例所示,也可以用于创建包。对于我们当前的情况,它将定义我们的要求(库和构建工具)以及修改选项和设置我们如何使用这些包的逻辑。在将此文件用于创建包的情况下,它可以定义(除其他外)如何下载包的源代码、如何从这些源代码构建二进制文件、如何打包二进制文件,以及为未来消费者提供关于如何使用该包的信息。我们将在后面的“创建包”部分解释如何使用 Conan 配方来创建包。
conanfile.txt
的等效 Conan 配方可能如下所示:
清单 28: conanfile.py
from conan import ConanFileclass CompressorRecipe(ConanFile):settings = "os", "compiler", "build_type", "arch"generators = "CMakeToolchain", "CMakeDeps"def requirements(self):self.requires("zlib/1.2.11")def build_requirements(self):self.tool_requires("cmake/3.22.6")
要创建 Conan 配方,我们声明了一个继承自 ConanFile
类的新类。这个类具有不同的类属性和方法:
settings
类属性定义了项目范围的变量,如编译器、其版本或操作系统本身,这些变量在我们构建项目时可能会改变。这与 Conan 如何管理二进制兼容性有关,因为这些值将影响 Conan 包的包 ID 的值。我们稍后将解释 Conan 如何使用这个值来管理二进制兼容性。generators
类属性指定了当我们调用conan install
命令时将运行哪些 Conan 生成器。在本例中,我们添加了CMakeToolchain
和CMakeDeps
,与conanfile.txt
中相同。- 在
requirements()
方法中,我们使用self.requires()
方法声明zlib/1.2.11
依赖项。 - 在
build_requirements()
方法中,我们使用self.tool_requires()
方法声明cmake/3.22.6
依赖项。
注意: 将工具依赖项添加到 build_requirements()
中并不是严格必要的,因为理论上此方法中的所有内容都可以在 requirements()
方法中完成。然而,build_requirements()
提供了一个专门的地方来定义 tool_requires
和 test_requires
,这有助于保持结构的有组织和清晰。有关更多信息,请查看 requirements() 和 build_requirements() 文档。
您可以检查运行与前面示例相同的命令将导致与之前相同的结果。
清单 29: Windows
$ conan install . --output-folder=build --build=missing
$ cd build
$ conanbuild.bat
# 假设 Visual Studio 15 2017 是您的 VS 版本,并且它与您的默认配置文件匹配
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
$ cmake --build . --config Release
...
Building with CMake version: 3.22.6
...
[100%] Built target compressor
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
$ deactivate_conanbuild.bat
清单 30: Linux, macOS
$ conan install . --output-folder build --build=missing
$ cd build
$ source conanbuild.sh
Capturing current environment in deactivate_conanbuildenv-release-x86_64.sh
Configuring environment variables
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
$ cmake --build .
...
Building with CMake version: 3.22.6
...
[100%] Built target compressor
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
$ source deactivate_conanbuild.sh
到目前为止,我们已经实现了与使用 conanfile.txt
相同的功能。让我们看看如何利用 conanfile.py
的功能来定义我们想要遵循的项目结构,以及如何使用 Conan 设置和选项添加一些逻辑。
使用 layout()
方法
在之前的示例中,每次我们执行 conan install
命令时,都必须使用 --output-folder
参数来定义我们希望 Conan 生成文件的位置。有一种更简洁的方法来决定我们希望 Conan 为构建系统生成文件的位置,例如,允许我们根据使用的 CMake 生成器类型决定是否使用不同的输出文件夹。您可以直接在 conanfile.py
的 layout()
方法中定义这一点,并使其适用于每个平台而无需进行更多更改。
清单 31: conanfile.py
import os
from conan import ConanFileclass CompressorRecipe(ConanFile):settings = "os", "compiler", "build_type", "arch"generators = "CMakeToolchain", "CMakeDeps"def requirements(self):self.requires("zlib/1.2.11")if self.settings.os == "Windows":self.requires("base64/0.4.0")def build_requirements(self):if self.settings.os != "Windows":self.tool_requires("cmake/3.22.6")def layout(self):# 我们假设如果编译器是 msvc,则 CMake 生成器是多配置的multi = True if self.settings.get_safe("compiler") == "msvc" else Falseif multi:self.folders.generators = os.path.join("build", "generators")self.folders.build = "build"else:self.folders.generators = os.path.join("build", str(self.settings.build_→˓˓type), "generators")self.folders.build = os.path.join("build", str(self.settings.build_type))
如您所见,我们在 layout()
方法中定义了 self.folders.generators
属性。这是所有由 Conan 生成的辅助文件(CMake 工具链和 cmake 依赖文件)将被放置的文件夹。
请注意,文件夹的定义对于多配置生成器(如 Visual Studio)或单配置生成器(如 Unix Makefiles)是不同的。在第一种情况下,文件夹是相同的,与构建类型无关,构建系统将在该文件夹内管理不同的构建类型。但像 Unix Makefiles 这样的单配置生成器必须为每个配置使用不同的文件夹(作为不同的 build_type
Release/Debug)。在这种情况下,我们添加了一个简单的逻辑,如果编译器名称是 msvc
,则认为是多配置。
检查运行与前面示例相同的命令(不带 --output-folder
参数)是否会导致与之前相同的结果:
清单 32: Windows
$ conan install . --build=missing
$ cd build
$ generators\conanbuild.bat
# 假设 Visual Studio 15 2017 是您的 VS 版本,并且它与您的默认配置文件匹配
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=generators\conan_toolchain.→˓˓cmake
$ cmake --build . --config Release
...
Building with CMake version: 3.22.6
...
[100%] Built target compressor
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
$ generators\deactivate_conanbuild.bat
清单 33: Linux, macOS
$ conan install . --build=missing
$ cd build/Release
$ source ./generators/conanbuild.sh
Capturing current environment in deactivate_conanbuildenv-release-x86_64.sh
Configuring environment variables
$ cmake ../.. -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake -DCMAKE_BUILD_→˓˓TYPE=Release
$ cmake --build .
...
Building with CMake version: 3.22.6
...
[100%] Built target compressor
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.2.11
$ source ./generators/deactivate_conanbuild.sh
没有必要总是在 conanfile.py
中编写此逻辑。有一些预定义的布局可以导入并在您的配方中直接使用。例如,对于 CMake 情况,Conan 中已经定义了一个 cmake_layout()
:
清单 34: conanfile.py
from conan import ConanFile
from conan.tools.cmake import cmake_layoutclass CompressorRecipe(ConanFile):settings = "os", "compiler", "build_type", "arch"generators = "CMakeToolchain", "CMakeDeps"def requirements(self):self.requires("zlib/1.2.11")def build_requirements(self):self.tool_requires("cmake/3.22.6")def layout(self):cmake_layout(self)
使用 validate()
方法为非支持的配置引发错误
validate()
方法在 Conan 加载 conanfile.py
时被评估,您可以使用它来执行输入设置的检查。例如,如果您的项目不支持 macOS 上的 armv8 架构,您可以引发 ConanInvalidConfiguration
异常以使 Conan 返回一个特殊的错误代码。这将指示用于设置或选项的配置不受支持。
清单 35: conanfile.py
...
from conan.errors import ConanInvalidConfigurationclass CompressorRecipe(ConanFile):...def validate(self):if self.settings.os == "Macos" and self.settings.arch == "armv8":raise ConanInvalidConfiguration("ARM v8 not supported in Macos")
使用 conanfile.py
进行条件要求
您可以向 requirements()
方法添加一些逻辑来有条件地添加或删除要求。想象一下,例如,您想在 Windows 上添加额外的依赖项,或者您想使用系统的 CMake 安装而不是使用 Conan 的 tool_requires
:
清单 36: conanfile.py
from conan import ConanFileclass CompressorRecipe(ConanFile):# Binary configurationsettings = "os", "compiler", "build_type", "arch"generators = "CMakeToolchain", "CMakeDeps"def requirements(self):self.requires("zlib/1.2.11")# 为 Windows 添加 base64 依赖if self.settings.os == "Windows":self.requires("base64/0.4.0")def build_requirements(self):# 为 Windows 使用系统的 CMakeif self.settings.os != "Windows":self.tool_requires("cmake/3.22.6")
使用 generate()
方法从包中复制资源
在某些场景下,Conan 包包含的文件对于库的使用非常有用,甚至是必需的。这些文件可以是配置文件、资源文件,或者是项目构建或正确运行所需的特定文件。使用 generate()
方法,您可以将这些文件从 Conan 缓存复制到项目的文件夹中,确保所有必需的资源都直接可用。
以下是一个示例,展示如何将依赖项的 resdirs
目录中的所有资源复制到项目中的 assets
目录:
import os
from conan import ConanFile
from conan.tools.files import copyclass MyProject(ConanFile):...def generate(self):# 将依赖项资源目录中的所有资源复制到项目源目录中的 "assets" 文件夹dep = self.dependencies["dep_name"]copy(self, "*", dep.cpp_info.resdirs[0], os.path.join(self.source_folder, "assets→˓˓"))
然后,在 conan install
步骤之后,所有这些资源文件都将被本地复制,允许您在项目的构建过程中使用它们。有关如何在 generate()
方法中从包导入文件的完整示例,可以参考关于使用 Dear ImGui 库的博客文章,该文章演示了如何根据图形 API 导入库的绑定。
注意: 澄清一点很重要:复制库(无论是静态库还是共享库)是不必要的。Conan 设计为使用来自 Conan 本地缓存位置的库,使用生成器和环境工具,而无需将它们复制到本地文件夹。
另请参阅:
- 使用 “cmake_layout” + “CMakeToolchain” + “CMakePresets 功能” 构建项目。
- 理解 Conan 包布局。
- 所有
conanfile.py
可用方法的文档。 configure()
中的条件生成器。
1.1.5 如何使用 Conan 交叉编译您的应用程序:主机 (host
) 和构建 (build
) 上下文
请首先克隆源代码以重现此项目。您可以在 GitHub 的 examples2
仓库中找到它们:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/consuming_packages/cross_building
在之前的示例中,我们学习了如何使用 conanfile.py
或 conanfile.txt
来构建一个使用 Zlib 和 CMake Conan 包的应用程序,该程序压缩字符串。此外,我们解释了您可以在名为 Conan 配置文件 (profile
) 的文件中设置信息,如操作系统、编译器或构建配置。您可以将该配置文件作为参数 (--profile
) 传递给 conan install
命令。我们还解释了不指定该配置文件等同于使用 --profile=default
参数。
对于所有这些示例,我们使用相同的平台来构建和运行应用程序。但是,如果您想在运行 Ubuntu Linux 的机器上构建应用程序,然后在另一个平台(如 Raspberry Pi)上运行它,该怎么办?Conan 可以使用两个不同的配置文件来建模这种情况,一个用于构建应用程序的机器(Ubuntu Linux),另一个用于运行应用程序的机器(Raspberry Pi)。我们将在下一节解释这种“双配置文件”方法。
Conan 双配置文件模型:构建 (build
) 和主机 (host
) 配置文件
即使您只指定一个 --profile
参数调用 Conan,Conan 内部也会使用两个配置文件。一个用于构建二进制文件的机器(称为构建配置文件 build profile
),另一个用于运行这些二进制文件的机器(称为主机配置文件 host profile
)。调用此命令:
$ conan install . --build=missing --profile=someprofile
等同于:
$ conan install . --build=missing --profile:host=someprofile --profile:build=default
如您所见,我们使用了两个新参数:
profile:host
:定义构建的二进制文件将在其上运行的平台的配置文件。对于我们的字符串压缩应用程序,这将是应用于将在 Raspberry Pi 上运行的 Zlib 库的配置文件。profile:build
:定义将在其上构建二进制文件的平台的配置文件。对于我们的字符串压缩应用程序,这将是 Ubuntu Linux 机器上用于构建它的 CMake 工具的配置文件。
请注意,当您只使用一个参数 --profile
时,它等同于 --profile:host
。如果您不指定 --profile:build
参数,Conan 将在内部使用默认配置文件。
因此,如果我们想在 Ubuntu Linux 机器上构建压缩应用程序,但要在 Raspberry Pi 上运行它,我们应该使用两个不同的配置文件。对于构建机器,我们可以使用默认配置文件,在我们的例子中如下所示:
清单 37: /profiles/default
[settings]
os=Linux
arch=x86_64
build_type=Release
compiler=gcc
compiler.cppstd=gnu14
compiler.libcxx=libstdc++11
compiler.version=9
以及用于 Raspberry Pi(主机)的配置文件:
清单 38: /profiles/raspberry
[settings]
os=Linux
arch=armv7hf
compiler=gcc
build_type=Release
compiler.cppstd=gnu14
compiler.libcxx=libstdc++11
compiler.version=9[buildenv]
CC=arm-linux-gnueabihf-gcc-9
CXX=arm-linux-gnueabihf-g++-9
LD=arm-linux-gnueabihf-ld
重要提示: 请注意,为了成功构建此示例,您应该安装一个包含编译器以及为正确架构构建应用程序所需的所有工具的工具链。在本例中,主机是带有 armv7hf 架构操作系统的 Raspberry Pi 3,并且我们在 Ubuntu 机器上安装了 arm-linux-gnueabihf
工具链。
如果您查看 raspberry
配置文件,其中有一个名为 [buildenv]
的部分。该部分用于设置构建应用程序所需的环境变量。在本例中,我们声明了 CC
、CXX
和 LD
变量,分别指向交叉构建工具链的编译器、C++编译器和链接器。将此部分添加到配置文件中将在我们执行 conan install
时调用 VirtualBuildEnv
生成器。该生成器将把环境信息添加到我们将要获取的 conanbuild.sh
脚本中,以便在构建 CMake 之前使用,这样它就可以使用交叉构建工具链。
注意: 在某些情况下,您的构建平台上可能没有可用的工具链。对于这些情况,您可以使用 Conan 包来获取交叉编译器,并将其添加到配置文件的 [tool_requires]
部分。有关使用工具链包进行交叉构建的示例,请查看此示例。
构建 (build
) 和主机 (host
) 上下文
现在我们有了两个配置文件,让我们看看我们的 conanfile.py
:
清单 39: conanfile.py
from conan import ConanFile
from conan.tools.cmake import cmake_layoutclass CompressorRecipe(ConanFile):settings = "os", "compiler", "build_type", "arch"generators = "CMakeToolchain", "CMakeDeps"def requirements(self):self.requires("zlib/1.2.11")def build_requirements(self):self.tool_requires("cmake/3.22.6")def layout(self):cmake_layout(self)
如您所见,这与我们之前示例中使用的 conanfile.py
几乎相同。我们将需要 zlib/1.2.11
作为常规依赖项,需要 cmake/3.22.6
作为构建应用程序所需的工具。
我们需要应用程序为 Raspberry Pi 使用交叉构建工具链进行构建,并链接为同一平台构建的 zlib/1.2.11
库。另一方面,我们需要 cmake/3.22.6
二进制文件在 Ubuntu Linux 上运行。Conan 在依赖图内部管理这一点,区分我们所谓的“构建上下文 (build context
)”和“主机上下文 (host context
)”:
- 主机上下文 (
host context
) 填充有根包(在conan install
或conan create
命令中指定的包)及其通过self.requires()
添加的所有要求。在本例中,这包括压缩应用程序和zlib/1.2.11
依赖项。 - 构建上下文 (
build context
) 包含在构建机器上使用的工具要求。此类别通常包括所有开发工具,如 CMake、编译器和链接器。在本例中,这包括cmake/3.22.6
工具。
这些上下文定义了 Conan 将如何管理每个依赖项。例如,由于 zlib/1.2.11
属于主机上下文,我们在 raspberry
配置文件(主机配置文件)中定义的 [buildenv]
构建环境将仅在构建 zlib/1.2.11
库时应用,而不会影响属于构建上下文的任何内容,如 cmake/3.22.6
依赖项。
现在,让我们构建应用程序。首先,使用构建和主机平台的配置文件调用 conan install
。这将安装为 armv7hf
架构构建的 zlib/1.2.11
依赖项和一个在 64 位架构上运行的 cmake/3.22.6
版本。
$ conan install . --build missing -pr:b=default -pr:h=./profiles/raspberry
然后,让我们调用 CMake 来构建应用程序。与前面的示例一样,我们必须通过运行 source Release/generators/conanbuild.sh
来激活构建环境。这将设置定位交叉构建工具链和构建应用程序所需的环境变量。
$ cd build
$ source Release/generators/conanbuild.sh
Capturing current environment in deactivate_conanbuildenv-release-armv7hf.sh
Configuring environment variables
$ cmake .. -DCMAKE_TOOLCHAIN_FILE=Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_→˓˓TYPE=Release
$ cmake --build .
...
-- Conan toolchain: C++ Standard 14 with extensions ON
-- The C compiler identification is GNU 9.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/arm-linux-gnueabihf-gcc-9 - skipped
-- Detecting C compile features
-- Detecting C compile features - done
[100%] Built target compressor
...
$ source Release/generators/deactivate_conanbuild.sh
您可以运行 Linux 实用程序 file
来检查我们是否为正确的架构构建了应用程序:
$ file compressor
compressor: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3,
BuildID[sha1]=2a216076864a1b1f30211debf297ac37a9195196, for GNU/Linux 3.2.0, not stripped
另请参阅:
- 为工具链创建 Conan 包。
- 使用 NDK 交叉构建到 Android。
- VirtualBuildEnv 参考。
- 使用
tool_requires
进行交叉构建。 - 如何要求像 gtest 这样的测试框架:使用
test_requires
。 - 使用 Conan 为 iOS 构建。
1.2 创建包 (Creating packages)
本节展示如何使用 Conan 配方 (conanfile.py
) 创建您自己的 Conan 包。我们将从一个简单的“Hello World” C++ 库示例开始,逐步讲解配方中的关键部分和方法,包括如何处理源代码、添加依赖项、自定义构建流程等。
1.2.1 创建您的第一个 Conan 包
(内容同上,此处省略)
1.2.2 在包中处理源代码 (Handle sources in packages)
在之前的教程部分,我们创建了一个“Hello World” C++ 库的 Conan 包。我们使用了 exports_sources
属性声明源代码的位置(与配方文件在同一文件夹)。然而,有时源代码存储在远程仓库或服务器文件中。本节将修改之前的配方,添加 source()
方法,并解释如何:
- 从远程仓库的 zip 文件检索源代码。
- 从 git 仓库的分支检索源代码。
从远程 zip 文件获取源代码
请克隆源代码以重现此项目:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/creating_packages/handle_sources
项目结构与之前的示例相同,但库源代码不包含在本地文件夹中。查看 conanfile.py
的变化:
from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout
from conan.tools.files import get # 导入 get 工具class helloRecipe(ConanFile):name = "hello"version = "1.0"...# 不再使用 exports_sources# exports_sources = "CMakeLists.txt", "src/*", "include/*"def source(self):# 警告:使用分支的 HEAD 而不是不可变标签或提交是错误做法get(self, "https://github.com/conan-io/libhello/archive/refs/heads/main.zip", strip_root=True)...
关键变化:
- 移除了
exports_sources
属性。 - 添加了
source()
方法,使用conan.tools.files.get()
工具下载并解压 zip 文件。get()
下载指定的 URL。strip_root=True
:如果解压后的内容都在一个顶级文件夹中,则将其内容移动到父目录(根目录)。
重要警告: 源代码来源必须是不可变的(例如特定标签、提交哈希)。使用可变引用(如分支 HEAD)是强烈反对的做法,会导致不可预测的行为和潜在问题。
运行 conan create
将下载源代码:
$ conan create .
...
-------- Installing packages ----------
hello/1.0: Calling source() in /Users/user/.conan2/p/0fcb5ffd11025446/s/.
Downloading main.zip
hello/1.0: Unzipping 3.7KB
Unzipping 100 %
hello/1.0: Copying sources to build folder
...
从 git 仓库分支获取源代码
修改 source()
方法使用 Git 工具:
...
from conan.tools.scm import Git # 导入 Git 工具class helloRecipe(ConanFile):...def source(self):git = Git(self)git.clone(url="https://github.com/conan-io/libhello.git", target=".")# 警告:使用分支的 HEAD 而不是不可变标签或提交是错误做法git.checkout("require_fmt") # 检出特定分支
这里使用了 conan.tools.scm.Git()
工具:
git.clone()
:克隆仓库到当前目录 (target="."
)。git.checkout()
:检出特定分支(同样,应使用标签或提交哈希而非分支名)。
使用 conandata.yml
文件
我们可以创建一个 conandata.yml
文件(与 conanfile.py
同目录)。该文件会被 Conan 自动导出和解析,我们可以在配方中读取其信息。这对于存储外部源代码仓库的 URL 非常方便。
示例 conandata.yml
:
sources:"1.0":url: "https://github.com/conan-io/libhello/archive/refs/heads/main.zip"sha256: "7bc71c682895758a996ccf33b70b91611f51252832b01ef3b4675371510ee466"strip_root: true"1.1":url: ...sha256: ...
在配方中使用:
def source(self):get(self, **self.conan_data["sources"][self.version])# 等价于:# data = self.conan_data["sources"][self.version]# get(self, data["url"], sha256=data["sha256"], strip_root=data["strip_root"])
另请参阅:
- 修补源代码 (Patching sources)
- 捕获 Git SCM 源信息 (Capturing Git SCM source information)
source()
方法参考
1.2.3 向包添加依赖项 (Add dependencies to packages)
在之前的教程部分,我们创建了一个不依赖其他 Conan 包的“Hello World” C++ 库包。现在我们将为我们的库添加一些彩色输出功能,使用 fmt
库作为依赖项。
请克隆源代码以重现此项目:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/creating_packages/add_requires
查看 conanfile.py
的相关变化:
...
from conan.tools.build import check_max_cppstd, check_min_cppstd
...
class helloRecipe(ConanFile):name = "hello"version = "1.0"...generators = "CMakeDeps" # 添加 CMakeDeps 生成器...def validate(self):# 验证 C++ 标准兼容性check_min_cppstd(self, "11")check_max_cppstd(self, "20")def requirements(self):self.requires("fmt/8.1.1") # 添加 fmt 依赖def source(self):git = Git(self)git.clone(url="https://github.com/conan-io/libhello.git", target=".")git.checkout("require_fmt") # 检出包含 fmt 集成的分支
...
关键变化:
- 添加
generators = "CMakeDeps"
: 因为我们现在有依赖项 (fmt
),需要生成 CMake 查找该库的文件。 - 在
requirements()
方法中添加依赖:self.requires("fmt/8.1.1")
声明了对fmt
库的依赖。 - 添加
validate()
方法: 使用check_min_cppstd()
和check_max_cppstd()
验证fmt
库所需的 C++ 标准(fmt
至少需要 C++11)。 - 检出特定分支:
source()
方法检出一个修改了源代码的分支,该分支使用fmt
库添加了彩色输出功能(并修改了CMakeLists.txt
以声明依赖)。
头文件传递性 (Headers transitivity)
默认情况下,Conan 假设依赖项的头文件是当前包的实现细节(不对外暴露)。如果 hello/1.0
包的公共头文件(如 hello.h
)包含了 fmt
的头文件(如 #include <fmt/color.h>
),那么这些头文件必须传播给 hello/1.0
的消费者才能成功编译。这需要在配方中明确声明:
def requirements(self):self.requires("fmt/8.1.1", transitive_headers=True) # 传播头文件
注意:最佳实践
- 如果
hello/1.0
的消费者直接包含了fmt
头文件(如#include <fmt/color.h>
),那么该消费者应该在自己的配方中声明对fmt/8.1.1
的直接依赖 (self.requires("fmt/8.1.1")
)。即使移除了对hello/1.0
的依赖,它仍然依赖于fmt
,因此不能滥用hello
传递的fmt
头文件。
另请参阅:
requirements()
方法参考- 版本控制介绍
1.2.4 准备构建 (Preparing the build)
在前面的教程部分中,我们向“Hello World” C++库的Conan包添加了fmt依赖,以便为输出添加彩色效果。本节将聚焦于配方中的generate()
方法。该方法的目标是生成构建步骤所需的所有信息,主要包括:
- 编写构建步骤中使用的文件,例如注入环境变量的脚本、传递给构建系统的文件等。
- 配置工具链,以基于设置和选项提供额外信息,或移除Conan默认生成的、可能不适用于特定场景的信息。
我们将通过一个基于前一节的简单示例,解释如何使用此方法:为配方添加with_fmt
选项,根据该选项的值决定是否依赖fmt库。我们使用generate()
方法修改工具链,使其向CMake传递变量,从而有条件地添加库并在源代码中使用fmt。
操作步骤
- 克隆项目源代码
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/creating_packages/preparing_the_build
- 查看配方文件的变化
在conanfile.py
中,我们添加了with_fmt
选项,并根据其值条件性地依赖fmt库,同时在generate()
方法中修改CMake工具链:
from conan.tools.build import check_max_cppstd, check_min_cppstdclass helloRecipe(ConanFile):name = "hello"version = "1.0"generators = "CMakeDeps" # 新增生成器以支持依赖解析def validate(self):if self.options.with_fmt:check_min_cppstd(self, "11")check_max_cppstd(self, "14")def source(self):git = Git(self)git.clone(url="https://github.com/conan-io/libhello.git", target=".")git.checkout("optional_fmt") # 切换到包含fmt支持的分支def requirements(self):if self.options.with_fmt:self.requires("fmt/8.1.1")def generate(self):tc = CMakeToolchain(self)if self.options.with_fmt:tc.variables["WITH_FMT"] = True # 向CMake工具链注入变量tc.generate()def build(self):cmake = CMake(self)cmake.configure()cmake.build()def package(self):cmake = CMake(self)cmake.install()def package_info(self):self.cpp_info.libs = ["hello"]
- CMakeLists.txt的变化
在CMakeLists.txt
中,我们使用注入的WITH_FMT
变量条件性地链接fmt库:
cmake_minimum_required(VERSION 3.15)
project(hello CXX)add_library(hello src/hello.cpp)
target_include_directories(hello PUBLIC include)
set_target_properties(hello PROPERTIES PUBLIC_HEADER "include/hello.h")if(WITH_FMT)find_package(fmt)target_link_libraries(hello fmt::fmt)target_compile_definitions(hello PRIVATE USING_FMT=1)
endif()install(TARGETS hello)
- 源代码中的条件编译
在hello.cpp
中,根据USING_FMT
宏条件性地使用fmt库:
#include <iostream>
#include "hello.h"#if USING_FMT == 1
#include <fmt/color.h>
#endifvoid hello() {
#if USING_FMT == 1#ifdef NDEBUGfmt::print(fg(fmt::color::crimson) | fmt::emphasis::bold, "hello/1.0: Hello World Release! (with color!)\n");#elsefmt::print(fg(fmt::color::crimson) | fmt::emphasis::bold, "hello/1.0: Hello World Debug! (with color!)\n");#endif
#else#ifdef NDEBUGstd::cout << "hello/1.0: Hello World Release! (without color)" << std::endl;#elsestd::cout << "hello/1.0: Hello World Debug! (without color)" << std::endl;#endif
#endif
}
构建与测试
- 启用fmt支持的构建
$ conan create . --build=missing -o with_fmt=True
输出将显示带颜色的消息:
hello/1.0: Hello World Release! (with color!)
- 禁用fmt支持的构建
$ conan create . --build=missing -o with_fmt=False
输出将显示不带颜色的消息:
hello/1.0: Hello World Release! (without color)
进阶用法
generate()
方法还可用于:
- 创建自定义工具链以满足特定构建需求。
- 访问包依赖信息,例如:
- 通过
conf_info
获取配置信息。 - 获取依赖项的选项。
- 使用
copy
工具从依赖项导入文件,生成清单或收集许可证信息。
- 通过
- 使用环境工具生成系统环境信息。
- 基于设置添加除
Release
和Debug
之外的自定义配置(如ReleaseShared
)。
相关参考
- 使用generate()方法从依赖项导入文件
- generate()方法参考文档
1.2.5 配置配方中的设置和选项 (Configure settings and options in recipes)
我们已经介绍了Conan的设置(settings)和选项(options),以及如何使用它们为不同配置(如Debug、Release、静态或共享库等)构建项目。在本节中,我们将解释如何在Conan配方中配置这些设置和选项,特别是当某些设置或选项不适用于特定Conan包时的处理方式。我们还将简要介绍Conan如何建模二进制兼容性,以及这与选项和设置的关系。
操作步骤
- 克隆项目源代码
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/creating_packages/configure_options_settings
- 查看配方文件的变化
在conanfile.py
中,我们添加了config_options()
和configure()
方法,用于根据条件删除不必要的选项:
class helloRecipe(ConanFile):name = "hello"version = "1.0"options = {"shared": [True, False],"fPIC": [True, False],"with_fmt": [True, False]}default_options = {"shared": False,"fPIC": True,"with_fmt": True}def config_options(self):if self.settings.os == "Windows":del self.options.fPIC # 在Windows上删除fPIC选项def configure(self):if self.options.shared:self.options.rm_safe("fPIC") # 当构建共享库时删除fPIC选项
- CMakeLists.txt的条件配置
在CMakeLists.txt
中,我们使用注入的变量条件性地链接fmt库:
cmake_minimum_required(VERSION 3.15)
project(hello CXX)add_library(hello src/hello.cpp)
target_include_directories(hello PUBLIC include)
set_target_properties(hello PROPERTIES PUBLIC_HEADER "include/hello.h")if(WITH_FMT)find_package(fmt)target_link_libraries(hello fmt::fmt)target_compile_definitions(hello PRIVATE USING_FMT=1)
endif()install(TARGETS hello)
- 源代码中的条件编译
在hello.cpp
中,根据USING_FMT
宏条件性地使用fmt库:
#include <iostream>
#include "hello.h"#if USING_FMT == 1
#include <fmt/color.h>
#endifvoid hello() {
#if USING_FMT == 1#ifdef NDEBUGfmt::print(fg(fmt::color::crimson) | fmt::emphasis::bold, "hello/1.0: Hello World Release! (with color!)\n");#elsefmt::print(fg(fmt::color::crimson) | fmt::emphasis::bold, "hello/1.0: Hello World Debug! (with color!)\n");#endif
#else#ifdef NDEBUGstd::cout << "hello/1.0: Hello World Release! (without color)" << std::endl;#elsestd::cout << "hello/1.0: Hello World Debug! (without color)" << std::endl;#endif
#endif
}
二进制兼容性与包ID(Package ID)
Conan通过包ID(Package ID) 机制标识与配置兼容的包。包ID是将包的设置、选项和依赖信息转换为唯一标识符的哈希值。当我们在配方中删除设置或选项时,这些值将不再参与包ID的计算,从而可能使不同配置生成相同的包ID。
- 构建不同配置并查看包ID
# 构建Release配置
$ conan create . --build=missing -s build_type=Release -tf=""
# 构建Debug配置
$ conan create . --build=missing -s build_type=Debug -tf=""
- 查看生成的包ID
$ conan list "hello/1.0:*"
输出将显示不同配置的包ID,例如:
Local Cache
hello
hello/1.0
revisions
d8e4debf31f0b7b5ec7ff910f76f1e2a (2024-05-10 09:13:16 UTC)
packages
522dcea5982a3f8a5b624c16477e47195da2f84f
info
settings
arch: x86_64
build_type: Release
os: Windows
- 测试删除选项对包ID的影响
# 构建共享库(删除fPIC选项)
$ conan create . --build=missing -o shared=True -tf=""
$ conan create . --build=missing -o shared=True -o fPIC=False -tf=""
由于fPIC
选项已被删除,两次构建将生成相同的包ID:
$ conan list "hello/1.0:*"
packages
2a899fd0da3125064bf9328b8db681cd82899d56
info
settings
arch: x86_64
build_type: Release
os: Macos
options
shared: True
特殊场景处理
- C库的设置配置
对于C库,可删除与C++相关的设置(如compiler.cppstd
和compiler.libcxx
),因为它们不影响二进制结果:
def configure(self):self.settings.rm_safe("compiler.cppstd")self.settings.rm_safe("compiler.libcxx")
- 仅头文件库(Header-only)的包ID配置
对于仅头文件的库,可通过package_id()
方法清空包ID计算,确保所有配置使用同一包:
def package_id(self):self.info.clear()
相关参考
- 头文件包文档
- 二进制兼容性配置(compatibility.py)扩展
- Conan包类型文档
- 为依赖项设置package_id_mode
- 二进制模型完整文档
1.2.6 构建包:build()方法
我们已经使用过包含build()方法的Conan配方,并学习了如何利用该方法调用构建系统并构建包。在本教程中,我们将修改此方法,并解释如何使用它来执行以下任务:
- 构建和运行测试
- 有条件地修补源代码
- 有条件地选择要使用的构建系统
操作步骤
- 克隆项目源代码
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/creating_packages/build_method
- 为项目构建和运行测试
您会注意到conanfile.py文件相对于之前的配方有一些变化。以下是相关部分:
class helloRecipe(ConanFile):name = "hello"version = "1.0"def source(self):git = Git(self)git.clone(url="https://github.com/conan-io/libhello.git", target=".")git.checkout("with_tests") # 切换到包含测试的分支def requirements(self):if self.options.with_fmt:self.requires("fmt/8.1.1")self.test_requires("gtest/1.11.0") # 添加测试依赖gtestdef generate(self):tc = CMakeToolchain(self)if self.options.with_fmt:tc.variables["WITH_FMT"] = Truetc.generate()def build(self):cmake = CMake(self)cmake.configure()cmake.build()# 条件运行测试(除非配置中跳过测试)if not self.conf.get("tools.build:skip_test", default=False):test_folder = os.path.join("tests")if self.settings.os == "Windows":test_folder = os.path.join("tests", str(self.settings.build_type))self.run(os.path.join(test_folder, "test_hello"))
- 源代码中的测试修改
- 我们在库源代码中添加了新函数
compose_message()
,并为该函数添加了单元测试。 - CMakeLists.txt中使用
BUILD_TESTING
变量条件性地添加测试目录:
- 我们在库源代码中添加了新函数
if (NOT BUILD_TESTING STREQUAL OFF)add_subdirectory(tests)
endif()
- 测试文件使用googletest框架:
#include "../include/hello.h"
#include "gtest/gtest.h"namespace {TEST(HelloTest, ComposeMessages) {EXPECT_EQ(std::string("hello/1.0: Hello World Release! (with color!)\n"), compose_message("Release", "with color!"));}
}
执行构建和测试
- 构建并运行测试(默认启用测试)
$ conan create . --build=missing -tf=""
输出将显示测试通过:
[ RUN ] HelloTest.ComposeMessages
[ OK ] HelloTest.ComposeMessages (0 ms)
[----------] 1 test from HelloTest (0 ms total)
[----------] Global test environment tear-down
[ PASSED ] 1 test.
- 跳过测试构建
$ conan create . -c tools.build:skip_test=True -tf=""
此时将只构建库,不运行测试:
[ 50%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX static library libhello.a
[100%] Built target hello
有条件地修补源代码
如果需要修补源代码,推荐在source()
方法中执行。但若修补依赖于设置或选项,可在build()
方法中使用replace_in_file
工具:
import os
from conan.tools.files import replace_in_fileclass helloRecipe(ConanFile):def build(self):replace_in_file(self, os.path.join(self.source_folder, "src", "hello.cpp"),"Hello World","Hello {} Friends".format("Shared" if self.options.shared else "Static"))
有条件地选择构建系统
在多平台场景中,可根据平台选择不同的构建系统:
class helloRecipe(ConanFile):def generate(self):if self.settings.os == "Windows":tc = CMakeToolchain(self)tc.generate()deps = CMakeDeps(self)deps.generate()else:tc = AutotoolsToolchain(self)tc.generate()deps = PkgConfigDeps(self)deps.generate()def build(self):if self.settings.os == "Windows":cmake = CMake(self)cmake.configure()cmake.build()else:autotools = Autotools(self)autotools.autoreconf()autotools.configure()autotools.make()
1.2.7 打包文件:package()方法
我们已在hello包中使用package()
方法调用CMake的install步骤。本教程将详细解释CMake.install()的用法,并介绍如何修改此方法以:
- 使用conan.tools.files工具将生成的工件从构建文件夹复制到包文件夹
- 复制包许可证
- 管理符号链接的打包
使用CMake install步骤打包
当在CMakeLists.txt中定义了安装逻辑时,可直接调用cmake.install()
:
cmake_minimum_required(VERSION 3.15)
project(hello CXX)add_library(hello src/hello.cpp)
target_include_directories(hello PUBLIC include)
set_target_properties(hello PROPERTIES PUBLIC_HEADER "include/hello.h")install(TARGETS hello)
def package(self):cmake = CMake(self)cmake.install()
构建后,执行conan create
将自动将头文件和库复制到包文件夹:
-- Installing: /Users/.../p/tmp/b5857f2e70d1b2fd/p/include/hello.h
-- Installing: /Users/.../p/tmp/b5857f2e70d1b2fd/p/lib/libhello.a
使用conan.tools.files.copy()打包
若不依赖CMake的安装功能,可使用copy
工具手动复制文件:
from conan.tools.files import copydef package(self):# 复制许可证copy(self, "LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses"))# 复制头文件copy(self, pattern="*.h", src=os.path.join(self.source_folder, "include"), dst=os.path.join(self.package_folder, "include"))# 复制库文件(根据平台选择后缀)copy(self, pattern="*.a", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)copy(self, pattern="*.so", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)copy(self, pattern="*.lib", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False)copy(self, pattern="*.dll", src=self.build_folder, dst=os.path.join(self.package_folder, "bin"), keep_path=False)
管理符号链接
Conan提供工具将绝对符号链接转换为相对链接,确保包的可移植性:
from conan.tools.files.symlinks import absolute_to_relative_symlinksdef package(self):# 复制文件...absolute_to_relative_symlinks(self, self.package_folder)
1.2.8 为消费者定义信息:package_info()方法
在之前的教程中,我们解释了如何使用package()
方法将头文件和二进制文件存储在Conan包中。消费者依赖该包时,需提供额外信息以便Conan将其传递给构建系统。
例如,我们构建了名为hello的静态库,生成libhello.a
(Linux/macOS)或hello.lib
(Windows),并打包了头文件hello.h
。消费者需知道:
- 包含文件夹的位置(查找hello.h)
- 库文件的名称和位置(链接时使用)
配置package_info()方法
class helloRecipe(ConanFile):def package_info(self):self.cpp_info.libs = ["hello"] # 声明需链接的库名# 以下为默认值,可省略,但为清晰起见显式声明self.cpp_info.libdirs = ["lib"] # 库文件位置self.cpp_info.includedirs = ["include"] # 头文件位置
条件性设置信息
若库名随配置变化,可根据选项动态设置:
def package_info(self):if self.options.shared:self.cpp_info.libs = ["hello-shared"]else:self.cpp_info.libs = ["hello-static"]
为生成器设置属性
通过set_property
为特定生成器设置信息,例如修改CMake目标名称:
def package_info(self):if self.options.shared:self.cpp_info.libs = ["hello-shared"]else:self.cpp_info.libs = ["hello-static"]self.cpp_info.set_property("cmake_target_name", "hello::myhello") # 修改CMake目标名
此时消费者的CMakeLists.txt需使用新目标名:
target_link_libraries(example hello::myhello)
传递环境和配置信息
通过runenv_info
和buildenv_info
传递环境变量,通过conf_info
传递配置值:
def package_info(self):self.runenv_info.define("MY_APP_ENV_VAR", "value") # 运行时环境变量self.buildenv_info.define("BUILD_TOOL_PATH", "/path/to/tool") # 构建时环境变量self.conf_info.define("myconf.option", "enabled") # 配置值
1.2.9 测试Conan包
在之前的教程中,我们使用了test_package
,它在conan create
结束时自动调用,验证包的正确性。本节将详细解释test_package
。
test_package的关键特性
- 与单元测试的区别:
test_package
用于验证包的可消费性,而非库的功能正确性。 - 独立Conan项目:包含自己的conanfile.py和构建脚本,依赖被测试的包。
- 不包含在包中:仅存在于源代码仓库,不随包发布。
- 可自定义测试文件夹:通过
--test-folder
参数或test_package_folder
属性指定。
test_package的结构
以hello/1.0为例,其test_package
包含:
test_package
├── CMakeLists.txt
├── conanfile.py
└── src└── example.cpp
example.cpp
:简单使用hello库的示例程序
#include "hello.h"
int main() {hello();
}
CMakeLists.txt
:构建示例程序
cmake_minimum_required(VERSION 3.15)
project(PackageTest CXX)
find_package(hello CONFIG REQUIRED)
add_executable(example src/example.cpp)
target_link_libraries(example hello::hello)
conanfile.py
:依赖被测试的包并执行测试
from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout
from conan.tools.build import can_runclass helloTestConan(ConanFile):generators = "CMakeDeps", "CMakeToolchain"settings = "os", "compiler", "build_type", "arch"def requirements(self):self.requires(self.tested_reference_str) # 动态依赖被测试的包def build(self):cmake = CMake(self)cmake.configure()cmake.build()def test(self):if can_run(self):cmd = os.path.join(self.cpp.build.bindir, "example")self.run(cmd, env="conanrun") # 使用运行时环境执行程序
执行测试
使用conan test
命令单独测试包(需先创建包):
$ conan test test_package hello/1.0
输出将显示示例程序的运行结果:
hello/1.0: Hello World Release! (with color!)
1.2.10 其他类型的包
除经典C++库外,Conan还支持多种包类型。本节将介绍如何创建仅头文件库、预构建库、工具依赖和应用程序的配方。
仅头文件包(Header-only)
仅头文件库仅包含头文件,消费者无需链接库文件。由于无需编译,只需一个二进制配置:
from conan import ConanFile
from conan.tools.files import copyclass SumConan(ConanFile):name = "sum"version = "0.1"exports_sources = "include/*"no_copy_source = True # 优化:不复制源文件到构建文件夹package_type = "header-library" # 声明包类型def package(self):copy(self, "*.h", self.source_folder, self.package_folder)def package_info(self):self.cpp_info.bindirs = [] # 清空二进制目录self.cpp_info.libdirs = [] # 清空库目录
头文件内容(如sum.h
):
inline int sum(int a, int b){return a + b;
}
预构建库包
当需要打包已存在的二进制文件(如第三方库或其他流程生成的文件),可直接复制文件而不执行构建:
import os
from conan import ConanFile
from conan.tools.files import copyclass helloRecipe(ConanFile):name = "hello"version = "0.1"settings = "os", "arch"def layout(self):_os = str(self.settings.os).lower()_arch = str(self.settings.arch).lower()self.folders.build = os.path.join("vendor_hello_library", _os, _arch)self.folders.source = self.folders.buildself.cpp.source.includedirs = ["include"]self.cpp.build.libdirs = ["."]def package(self):copy(self, pattern="*.h", src=os.path.join(self.source_folder, "include"), dst=os.path.join(self.package_folder, "include"))copy(self, pattern="*.lib", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"))copy(self, pattern="*.a", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"))
工具依赖包(Tool requires)
工具依赖包用于提供可执行工具(如cmake、ninja),供其他包在构建时使用:
import os
from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout
from conan.tools.files import copyclass secure_scannerRecipe(ConanFile):name = "secure_scanner"package_type = "application" # 声明为应用程序包version = "1.0"def layout(self):cmake_layout(self)def build(self):cmake = CMake(self)cmake.configure()cmake.build()def package(self):extension = ".exe" if self.settings.os == "Windows" else ""copy(self, f"*secure_scanner{extension}", self.build_folder, os.path.join(self.package_folder, "bin"), keep_path=False)def package_info(self):self.buildenv_info.define("MY_VAR", "23") # 定义构建时环境变量
1.3 操作Conan仓库(Working with Conan repositories)
我们已学习从Conan Center下载和使用包,以及在本地缓存中创建和存储包。本节将介绍如何使用Conan仓库上传配方和二进制文件,以便在其他机器、项目中复用或共享。
搭建Conan远程仓库
-
Artifactory社区版(推荐)
使用Docker运行Artifactory CE:$ docker run --name artifactory -d -e JF_SHARED_DATABASE_TYPE=derby -e JF_SHARED_DATABASE_ALLOWNONPOSTGRESQL=true -p 8081:8081 -p 8082:8082 releases-docker.jfrog.io/jfrog/artifactory-cpp-ce:latest
访问http://localhost:8081,使用默认账号admin:password登录,创建名为“conan-local”的Conan本地仓库。
-
配置Conan客户端连接仓库
$ conan remote add artifactory http://localhost:8081/artifactory/api/conan/conan-local $ conan remote login artifactory <user> -p <password>
上传包到远程仓库
-
检查远程仓库列表
$ conan remote list
-
上传包(配方和二进制文件)
$ conan upload hello -r=artifactory
-
从远程仓库下载包
$ conan remove hello -c # 先删除本地包 $ conan install --requires=hello/1.0 -r=artifactory
1.4 本地开发包 (Developing packages locally)
使用Conan创建包时,每次运行conan create
都需在缓存中执行下载、解压、编译等耗时操作。本节介绍本地开发流程,直接在项目目录中处理包,无需先导出到缓存。
本地包开发流程
- 克隆项目并初始化
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/developing_packages/local_package_development_flow
- 分步测试配方方法
- conan source:单独测试
source()
方法,下载源码到临时文件夹
$ conan source
- conan install:安装依赖并生成构建文件
$ conan install .
- conan build:执行
build()
方法
$ conan build .
- conan export-pkg:打包本地构建的工件到缓存
$ conan export-pkg .
- conan source:单独测试
可编辑模式(Editable mode)
可编辑模式允许消费者直接使用本地开发的包,无需每次修改后重新打包:
- 将包设为可编辑模式
$ conan editable add say
- 构建本地包
$ cd say
$ conan install . -s build_type=Release
$ cmake --preset conan-release
$ cmake --build --preset conan-release
- 消费可编辑包
$ cd ../hello
$ conan install . -s build_type=Release
$ cmake --preset conan-release
$ cmake --build --preset conan-release
$ ./hello # 直接使用本地say包
1.5 版本控制 (Versioning)
本节介绍包版本控制的多个概念:显式版本更新、版本范围自动更新、无版本变更时的修订追踪,以及使用锁文件确保依赖的可复现性。
版本管理基础
-
手动更新版本
修改conanfile.py中的version
属性:class pkgRecipe(ConanFile):name = "pkg"version = "1.1" # 从1.0更新为1.1
重新创建包:
$ conan create .
-
命令行指定版本
无需修改配方,直接在命令行指定版本:$ conan create . --version=1.2
-
动态获取版本(如从文件或Git标签)
from conan.tools.files import load from conan.tools.scm import Gitclass pkgRecipe(ConanFile):def set_version(self):self.version = load(self, "version.txt") # 从文件获取版本# 或从Git标签获取git = Git(self)self.version = git.run("describe --tags")
版本范围(Version ranges)
使用版本范围自动解析最新兼容版本:
class appRecipe(ConanFile):requires = "pkg/[>=1.0 <2.0]" # 匹配1.0到2.0之间的版本
~1
:匹配1.x版本(如1.2, 1.8)^1.2
:匹配1.2.x版本(如1.2.3, 1.2.9)[>1 <2.0 || ^3.2]
:多条件匹配
修订(Revisions)
Conan使用修订追踪配方或源码的变更(即使版本未更新):
$ conan create . # 首次创建,生成修订A
$ vi src/hello.cpp # 修改源码
$ conan create . # 生成新修订B,版本仍为1.0
$ conan list "hello/1.0#*" # 查看所有修订
锁文件(Lockfiles)
锁文件确保依赖的可复现性,即使存在新版本:
$ conan lock create . # 生成conan.lock
$ cat conan.lock # 包含具体版本和修订
{"version": "0.5","requires": ["pkg/1.0#905c3f0babc520684c84127378fefdd0"]
}
$ conan install . # 自动使用锁文件,锁定pkg/1.0
1.6 其他重要Conan特性 (Other important Conan features)
python_requires
复用其他配方中的代码:
# myutils/1.0/conanfile.py
class MyUtils(ConanFile):def helper_function(self):print("复用的工具函数")# consumer/conanfile.py
python_requires = "myutils/1.0"
from myutils import MyUtilsclass Consumer(ConanFile):def build(self):MyUtils(self).helper_function()
包列表(Packages lists)
批量管理包:
$ conan create . --format=json > build.json # 保存构建的包
$ conan list --graph=build.json --graph-binaries=build > pkglist.json # 提取二进制包
$ conan upload --list=pkglist.json -r=myremote # 批量上传
清理缓存(LRU策略)
移除长时间未使用的包:
$ conan remove "*:*" --lru=4w -c # 移除4周未使用的二进制包
$ conan remove "*" --lru=4w -c # 移除4周未使用的配方和二进制包
请注意,LRU(最近最少使用)时间遵循 remove
命令的规则:
- 如果我们使用
"*"
模式删除配方(conan remove "*" -c
),则仅检查配方的 LRU 时间:- 如果一个配方最近被使用过,它将保留其所有二进制文件。
- 如果一个配方最近未被使用,它将删除自身及其所有二进制文件。
- 如果我们使用
"*:*"
模式(conan remove "*:*" -c
),则仅检查二进制文件的 LRU 时间:- 它将删除未使用的二进制文件,但始终保留配方本身。
使用 conan list
命令(请注意,与 remove
命令不同,conan list
默认不会列出所有修订版本,因此如果需要选择所有修订版本,必须显式指定 #*
)可以创建最近最少使用包的列表:
# 列出所有最近4周未使用(LRU=4w)的配方修订版本
$ conan list "*#*" --lru=4w --format=json > old.json
# 删除这些配方修订版本(及其二进制文件)
$ conan remove --list=old.json -c