理解 conanfile.py 与 conanfile.txt 的使用灵活性

在前面的示例中,我们在 conanfile.txt 文件中声明了我们的依赖项(ZlibCMake)。让我们看一下那个文件:

conanfile.txt
[requires]
zlib/1.3.1

[tool_requires]
cmake/3.27.9

[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 包含以下信息:

conanfile.txt
[requires]
zlib/1.3.1

[tool_requires]
cmake/3.27.9

[generators]
CMakeDeps
CMakeToolchain

我们将把这些相同的信息翻译成 conanfile.py。该文件通常被称为 “Conan 配置文件”。它可以用于消费包(如本例所示),也可以用于创建包。对于我们当前的情况,它将定义我们的需求(库和构建工具)以及用于修改选项和设置我们希望如何消费这些包的逻辑。如果使用此文件创建包,它可以(除其他外)定义如何下载包的源代码、如何从这些源代码构建二进制文件、如何打包二进制文件,以及未来消费者如何消费该包的信息。我们将在后面的 创建包 部分解释如何使用 Conan 配置文件来创建包。

以 Conan 配置文件形式的 conanfile.txt 的等价物可能如下所示:

conanfile.py
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 包的包 ID 的值。我们将在后面解释 Conan 如何使用此值来管理二进制兼容性。

  • generators 类属性指定在调用 conan install 命令时将运行哪些 Conan 生成器。在本例中,我们像在 conanfile.txt 中一样添加了 CMakeToolchainCMakeDeps

  • requirements() 方法中,我们使用 self.requires() 方法声明 zlib/1.3.1 依赖项。

  • build_requirements() 方法中,我们使用 self.tool_requires() 方法声明 cmake/3.27.9 依赖项。

注意

build_requirements() 中添加工具的依赖项并非严格必需,因为理论上该方法中的所有内容都可以在 requirements() 方法中完成。但是,build_requirements() 提供了一个专用位置来定义 tool_requirestest_requires,这有助于保持结构有序和清晰。有关更多信息,请查阅 requirements()build_requirements() 文档。

您可以检查运行与先前示例相同的命令将导致与之前相同的结果。

Windows
$ 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.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
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.pylayout() 方法中定义此项,并使其在所有平台上都能正常工作,而无需进行更多更改。

conanfile.py
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 参数,将导致与之前相同的结果。

Windows
$ 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.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
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()

conanfile.py
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 返回一个特殊的错误代码。这将表明使用的设置或选项配置不受支持。

conanfile.py
...
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

conanfile.py
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 导入该库的绑定。

注意

需要澄清的是,复制库(无论是静态的还是共享的)是不必要的。Conan 的设计是通过 生成器环境工具 从 Conan 本地缓存中的位置使用库,而无需将它们复制到本地文件夹。

使用 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.2.11#bfceb3f8904b735f75c2b0df5713b1e6 - Downloaded (conancenter)
Build requirements
    cmake/3.22.6#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 相同的参数(profile、settings、options、lockfile 等)。

注意

最佳实践

conan build 命令无意取代或改变使用 CMake 和其他构建工具以及使用其 IDE 的典型开发人员流程。它只是在某些情况下提供了一个便捷的快捷方式,我们希望轻松地在本地构建一个项目,而无需键入多个命令或使用 IDE。