理解使用 conanfile.py 与 conanfile.txt 的灵活性¶
在之前的示例中,我们在 conanfile.txt 文件中声明了我们的依赖项(Zlib 和 CMake)。让我们看一下那个文件
[requires]
zlib/1.3.1
[tool_requires]
cmake/3.27.9
[generators]
CMakeDeps
CMakeToolchain
使用 conanfile.txt 构建项目对于简单的情况来说足够了,但如果您需要更多的灵活性,您应该使用 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 包含此信息
[requires]
zlib/1.3.1
[tool_requires]
cmake/3.27.9
[generators]
CMakeDeps
CMakeToolchain
我们将把相同的信息翻译成 conanfile.py。这个文件通常被称为 **“Conan 菜谱”**。它可以用于消费包,就像在本例中一样,也可以用于创建包。对于我们当前的情况,它将定义我们的需求(库和构建工具),以及修改选项和设置我们想要消费这些包的方式的逻辑。在用于创建包的情况下,它可以定义(除其他事项外)如何下载包的源代码,如何从这些源代码构建二进制文件,如何打包二进制文件,以及未来消费者如何消费包的信息。我们将在 创建包 部分稍后解释如何使用 Conan 菜谱来创建包。
conanfile.txt 的等效形式的 Conan 菜谱如下所示
from conan import ConanFile
class CompressorRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
def requirements(self):
self.requires("zlib/1.3.1")
def build_requirements(self):
self.tool_requires("cmake/3.27.9")
为了创建 Conan 菜谱,我们声明了一个继承自 ConanFile 类的新的类。这个类有不同的类属性和方法
settings 类属性定义了项目范围内的变量,例如编译器、其版本或操作系统本身,这些变量可能会在我们构建项目时发生变化。这与 Conan 管理二进制兼容性的方式有关,因为这些值会影响 Conan 包的 **package ID** 值。我们稍后会解释 Conan 如何使用此值来管理二进制兼容性。
generators 类属性指定在调用 conan install 命令时将运行哪些 Conan 生成器。在这种情况下,我们添加了 CMakeToolchain 和 CMakeDeps,就像在 conanfile.txt 中一样。
在 requirements() 方法中,我们使用
self.requires()方法来声明 zlib/1.3.1 依赖项。在 build_requirements() 方法中,我们使用
self.tool_requires()方法来声明 cmake/3.27.9 依赖项。
注意
不严格地说,不必将依赖项添加到 build_requirements() 中的工具中,因为理论上 requirements() 方法可以完成此方法中的所有操作。但是,build_requirements() 提供了一个专门的地方来定义 tool_requires 和 test_requires,这有助于保持结构组织清晰。有关更多信息,请查看 requirements() 和 build_requirements() 文档。
您可以检查运行与之前示例相同的命令将导致与之前相同的结果。
$ conan install . --output-folder=build --build=missing
$ cd build
$ conanbuild.bat
# assuming Visual Studio 15 2017 is your VS version and that it matches your default profile
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
$ cmake --build . --config Release
...
Building with CMake version: 3.27.9
...
[100%] Built target compressor
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.3.1
$ deactivate_conanbuild.bat
$ 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.27.9
...
[100%] Built target compressor
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.3.1
$ source deactivate_conanbuild.sh
到目前为止,我们已经实现了使用 conanfile.txt 具有相同的功能。让我们看看如何利用 conanfile.py 的功能来定义我们想要遵循的项目结构,并使用 Conan 设置和选项添加一些逻辑。
使用 layout() 方法¶
在之前的示例中,每次我们执行 conan install 命令时,我们都必须使用 –output-folder 参数来定义我们想要创建 Conan 生成的文件所在的目录。有一种更简洁的方法来决定 Conan 生成文件的位置,它允许我们决定,例如,我们是否希望根据我们使用的 CMake 生成器类型使用不同的输出文件夹。您可以在 conanfile.py 中的 layout() 方法中直接定义此方法,并使其适用于所有平台,而无需进行更多更改。
import os
from conan import ConanFile
class CompressorRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
def requirements(self):
self.requires("zlib/1.3.1")
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.27.9")
def layout(self):
# We make the assumption that if the compiler is msvc the
# CMake generator is multi-config
multi = True if self.settings.get_safe("compiler") == "msvc" else False
if 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 参数,将导致与之前相同的结果
$ conan install . --build=missing
$ cd build
$ generators\conanbuild.bat
# assuming Visual Studio 15 2017 is your VS version and that it matches your default profile
$ cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=generators\conan_toolchain.cmake
$ cmake --build . --config Release
...
Building with CMake version: 3.27.9
...
[100%] Built target compressor
$ Release\compressor.exe
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.3.1
$ generators\deactivate_conanbuild.bat
$ 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.27.9
...
[100%] Built target compressor
$ ./compressor
Uncompressed size is: 233
Compressed size is: 147
ZLIB VERSION: 1.3.1
$ source ./generators/deactivate_conanbuild.sh
不必总是将此逻辑写入 conanfile.py。有一些预定义的布局您可以导入并直接在您的菜谱中使用。例如,对于 CMake 的情况,Conan 中已经定义了一个 cmake_layout()
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class CompressorRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
def requirements(self):
self.requires("zlib/1.3.1")
def build_requirements(self):
self.tool_requires("cmake/3.27.9")
def layout(self):
cmake_layout(self)
使用 validate() 方法来引发非支持配置的错误¶
在 Conan 加载 conanfile.py 时,将评估 validate() 方法,您可以使用它来执行输入设置的检查。例如,如果您的项目不支持 macOS 上的 armv8 架构,您可以引发 ConanInvalidConfiguration 异常,使 Conan 以特殊的错误代码返回。这将指示用于设置或选项的配置不受支持。
...
from conan.errors import ConanInvalidConfiguration
class 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
from conan import ConanFile
class CompressorRecipe(ConanFile):
# Binary configuration
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
def requirements(self):
self.requires("zlib/1.3.1")
# Add base64 dependency for Windows
if self.settings.os == "Windows":
self.requires("base64/0.4.0")
def build_requirements(self):
# Use the system's CMake for Windows
if self.settings.os != "Windows":
self.tool_requires("cmake/3.27.9")
使用 generate() 方法从包中复制资源¶
在某些情况下,Conan 包包含对库的消费有用甚至必要的的文件。这些文件可以从配置文件、资产到项目构建或运行正确所需的特定文件。使用 generate() 方法,您可以将这些文件从 Conan 缓存复制到您的项目文件夹中,确保所有必需的资源可直接用于使用。
这里有一个示例,展示了如何将依赖项的 resdirs 目录中的所有资源复制到项目中的 assets 目录
import os
from conan import ConanFile
from conan.tools.files import copy
class MyProject(ConanFile):
...
def generate(self):
# Copy all resources from the dependency's resource directory
# to the "assets" folder in the source directory of your project
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 导入库的绑定。
使用 build() 方法和 conan build 命令¶
如果您的菜谱实现了 build() 方法,那么可以自动调用完整的 conan install + cmake <configure> + cmake <build>(或调用 build() 方法使用的构建系统)流程,只需一个命令。虽然这可能不是典型的开发流程,但在某些情况下可能是一个方便的快捷方式。
让我们将此 build() 方法添加到我们的菜谱
from conan import ConanFile
from conan.tools.cmake import CMake
class CompressorRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
...
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
所以现在我们可以直接调用 conan build .
$ conan build .
...
Graph root
conanfile.py: ...\conanfile.py
Requirements
zlib/1.3.1#bfceb3f8904b735f75c2b0df5713b1e6 - Downloaded (conancenter)
Build requirements
cmake/3.27.9#32cced101c6df0fab43e8d00bd2483eb - Downloaded (conancenter)
======== Calling build() ========
conanfile.py: Calling build()
conanfile.py: Running CMake.configure()
conanfile.py: RUN: cmake -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
...
conanfile.py: Running CMake.build()
conanfile.py: RUN: cmake --build "...\conanfile_py" --config Release
我们将看到它如何首先安装依赖项,然后调用 build() 方法,然后为我们调用 CMake 配置和构建步骤。由于 conan build . 内部执行 conan install,因此它可以接收与 conan install 相同的参数(配置文件、设置、选项、lockfile 等)。
注意
最佳实践
conan build 命令并不旨在取代或更改使用 CMake 和其他构建工具及其 IDE 的典型开发流程。它只是在某些情况下,我们想要轻松地在本地构建项目而无需键入多个命令或使用 IDE 时的一种便捷快捷方式。