自从 CMake 生态系统和 Conan 诞生以来,它们一直在不断发展。基于这种演变,我们很高兴地推出一种使用 CMake 创建 Conan 包的新统一方法。在不久的将来,这些新的生成器将在conan-center-index中引入。

一点历史...

在 Conan 最初创建的时候,CMake 的方法是基于“全局变量”。“现代 CMake”和“目标”的概念当时不存在或非常不常见。

我们创建了第一个cmake生成器来解决这个问题。使用cmake生成器使用 Conan 包的方法是包含一个conanbuildinfo.cmake文件并调用一个conan_basic_setup()宏,该宏调整必要的 CMake 全局变量以定位包含目录、要链接的库等等。

后来cmake_multi生成器为 Visual Studio 等多配置项目提供了支持,为ReleaseDebug创建不同的conanbuildinfo.cmake文件。

在 CMake 社区中,使用find_package()变得越来越流行,因此我们创建了cmake_find_package生成器。Conan 为每个依赖项生成不同的FindXXX.cmake模块,因此您可以调用find_package(XXX),并且会设置一堆变量,以便您可以链接到您的需求。

很快,一个名为“现代 CMake”的新概念被提出。“目标方法”假设不应该使用全局变量,并且关于库或可执行文件的所有信息都应该与一个“目标”相关联。

这就是为什么我们创建了cmake_find_package_multi,它基于目标、配置文件而不是模块,并且能够支持多配置项目,使用生成器表达式

最后,我们创建了cmake_paths生成器来将CMAKE_PREFIX_PATHCMAKE_MODULE_PATH指向包,以便支持人们在 Conan 包中打包模块和配置 cmake 文件。

太多的 CMake 集成,对吧?

一种新方法

我们有这么多的 CMake 集成,但我们觉得好像缺少了一些东西。我们希望在构建 CMake 项目时拥有相同的用户体验和结果,无论是在执行conan create,还是在命令行中开发一个库并调用“cmake”,或者在 IDE 中点击“构建”按钮。

为了帮助解决这个缺失的部分,我们引入了两个新的生成器

  • CMakeToolchain生成器用于创建conan_toolchain.cmake。此文件在基于设置和选项的conan install之后保存,并且可以像任何其他 CMake 工具链一样使用
$ cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
  • CMakeDeps生成器,用于管理需求并生成我们创建的配置和/或模块文件。

如何迁移配方

在 Conan 2.0 发布之后,唯一的 CMake 集成将是CMakeToolchain + CMakeDeps。这些生成器已经在 Conan 1.40 中得到支持,并且我们将在每个版本中继续改进它们。我们正在努力尽快在conan-center-index的配方中引入这些生成器。

有关更多信息,您可以查看创建包入门

命名空间更改

新集成的导入位于conan.tools命名空间中,而不是conan**s**

from conan.tools.cmake import CMakeToolchain, CMakeDeps

generate()方法。

“Conan 2.0 兼容配方”中的一个重要更改是generate()方法。此方法负责生成所有所需的文件,以便构建帮助程序(在build()方法中)几乎可以直接调用构建系统而无需任何计算。这使用户能够在conan create或在命令行中构建时获得相同的构建结果。

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

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    requires = "foo/1.0", "bar/2.0"

    def generate(self):
        tc = CMakeToolchain(self)
        # This writes the "conan_toolchain.cmake"
        tc.generate()


        deps = CMakeDeps(self)
        # This writes all the config files (xxx-config.cmake)
        deps.generate()

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

    ...

由于我们没有在CMakeToolchainCMakeDeps中调整任何内容,因此前面的示例可以简化为

from conans import ConanFile
from conan.tools.cmake import CMake

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    requires = "foo/1.0", "bar/2.0"
    generators = "CMakeToolchain", "CMakeDeps"

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

    ...    

自定义CMakeToolchain

最常更改的代码是旧的CMake()构建帮助程序中的.definitions。您必须将其迁移到工具链作为.variables

from conans import ConanFile, CMake

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    options = {"xxx_feature_enabled": [True, False]}
    default_options = {"xxx_feature_enabled": False}
    requires = "foo/1.0", "bar/2.0"
    generators = "cmake_find_package", "cmake_find_package_multi", "cmake" # any of them

    def build(self):
        cmake = CMake(self)
        cmake.definitions["DISABLE_XXX_FEATURE"] = not self.options.xxx_feature_enabled
        cmake.configure()
        cmake.build()

    ...

from conans import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    options = {"xxx_feature_enabled": [True, False]}
    default_options = {"xxx_feature_enabled": False}
    requires = "foo/1.0", "bar/2.0"
    generators = "CMakeDeps"

    def generate(self):
      toolchain = CMakeToolchain(self)
      toolchain.variables["DISABLE_XXX_FEATURE"] = not self.options.xxx_feature_enabled
      toolchain.generate()

查看完整的 CMakeToolchain 参考以了解所有自定义项。

自定义CMakeDeps

  • generate()方法中,您可以调整一些新内容,例如使用cmake.configurations添加新的自定义用户 CMake 配置(除了标准配置(Release、Debug 等)之外)并使用cmake.configuration选择当前配置
def generate(self):
    cmake = CMakeDeps(self)
    cmake.configurations.append("ReleaseShared")
    if self.options["hello"].shared:
        cmake.configuration = "ReleaseShared"
    cmake.generate()

要详细了解这一点,请参阅完整的 CMakeDeps 参考

  • package_info()方法中,您可以配置几个属性以指示生成器在使用者使用它(对您的包有需求)时如何行为。这是一个示例
def package_info(self):
    ...
    # Generate MyFileName-config.cmake
    self.cpp_info.set_property("cmake_file_name", "MyFileName")
    # Foo:: namespace for the targets (Foo::Foo if no components)
    self.cpp_info.set_property("cmake_target_name", "Foo")
    # self.cpp_info.set_property("cmake_target_namespace", "Foo")  # This can be omitted as the value is the same

    # Foo::Var target name for the component "mycomponent"
    self.cpp_info.components["mycomponent"].set_property("cmake_target_name", "Var")
    # Automatically include the lib/mypkg.cmake file when calling find_package()
    self.cpp_info.components["mycomponent"].set_property("cmake_build_modules", [os.path.join("lib", "mypkg.cmake")])

    # Skip this package when generating the files for the whole dependency tree in the consumer
    # note: it will make useless the previous adjustements.
    # self.cpp_info.set_property("cmake_find_mode", "none")

    # Generate both MyFileName-config.cmake and FindMyFileName.cmake
    self.cpp_info.set_property("cmake_find_mode", "both")

CMake构建帮助程序

要利用新的CMakeToolchainCMakeDeps,您必须从新的conan.tools.cmake命名空间导入新的CMake构建帮助程序。

此构建帮助程序仅调用cmake并传递-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake

由于此构建帮助程序不再具有内部状态,因此有一个反模式需要避免:保留构建帮助程序的实例以在build()方法中使用,然后在package()方法中使用。这被认为是一种反模式,因为在配方的各个方法之间保留状态可能会在 conan 本地方法(如conan build + conan export-pkg)中失败,在这些方法中执行是隔离的。

from conans import ConanFile, CMake, tools

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    _cmake = None

    ...

    def _configure_cmake(self):
        if not hasattr(self, "_cmake"):
            self._cmake = CMake(self)
            self._cmake.definitions["tests"] = False
            self._cmake.configure()
        return self._cmake

    def build(self):
        cmake = self._configure_cmake()
        cmake.build()

    def package(self):
        cmake = self._configure_cmake()
        cmake.install()

    ...

from conans import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"

    def generate(self):
        toolchain = CMakeToolchain(self)
        toolchain.variables["tests"] = False
        toolchain.generate()

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

    def package(self):
        cmake = CMake()
        cmake.install()

layout()方法

您可以在配方中声明一个[layout()](https://docs.conan.org.cn/en/latest/developing_packages/package_layout.html)方法来描述包内容,不仅是缓存中的最终包,还有开发过程中的包。由于包在缓存和本地目录中将具有相同的结构,因此配方开发变得更容易,甚至可以开箱即用地处理可编辑的包。

您可以使用layout()方法避免以下一些经典模式

conandata.yml

...

patches:
  "0.1":
    - patch_file: "patches/001-fix-curl-define.patch"
      base_path: "source_subfolder"

conanfile.py

from conans import ConanFile, CMake, tools

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    exports_sources = ["patches/**"]

    @property
    def _source_subfolder(self):
        return "source_subfolder"

    @property
    def _build_subfolder(self):
        return "build_subfolder"


    def source(self):
        tools.get("https://www.foo.bar/sources.tgz")
        extracted_dir = "{}-{}".format(self.name, self.version)
        os.rename(extracted_dir, self._source_subfolder)
        for patch in self.conan_data.get("patches", {}).get(self.version, []):
            tools.patch(**patch)


    def build(self):
        cmake = CMake()
        cmake.configure(build_folder=self._build_subfolder, source_folder=self._source_subfolder)
        cmake.build()
    ...

conandata.yml

...

patches:
  "0.1":
    - patch_file: "patches/001-fix-curl-define.patch"

conanfile.py

from conans import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake
from conan.tools.layout import cmake_layout
from conan.tools.files import apply_conandata_patches

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeToolchain"
    exports_sources = ["patches/**"]

    def layout(self):
        cmake_layout(self)
        self.folders.source = "{}-{}".format(self.name, self.version)

    def source(self):
        tools.get("https://www.foo.bar/sources.tgz")
        apply_conandata_patches(self)

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

在前面的示例中,我们使用了预定义的布局,即cmake_layout。有关cmake_layout的更多信息,请参见。

您可以在调用它之后调整任何值以匹配您的包结构。

此外,新的工具apply_conandata_patches已经知道在哪里可以找到源文件,这要感谢layout,因此可以在conandata.yml文件中省略base_path

查看layout()以了解更多信息。

访问依赖项

有时在配方中,您需要访问依赖项以检查某些内容,通常是版本和根包文件夹。以前,这可以通过在配方的几乎任何方法中访问deps_cpp_info对象来完成。在新模型中,应在generate()validate()方法中使用新的self.dependencies对象来访问依赖项。

generate(self)

from conans import ConanFile, CMake

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    requires = "foo/1.0"

    def build(self):
        cmake = CMake(self)
        cmake.definitions["FOO_ROOT_DIR"] = self.deps_cpp_info["foo"].rootpath
        cmake.configure()
        cmake.build()

    ...

from conans import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    requires = "foo/1.0", "bar/2.0"
    generators = "CMakeDeps"

    def generate(self):
        toolchain = CMakeToolchain(self)
        toolchain.variables["FOO_ROOT_DIR"] = self.dependencies["foo"].package_folder


        # Other possible dependencies access
        # info = self.dependencies["foo"].cpp_info
        # include_dirs = info.includedirs
        #
        # ref = self.dependencies["foo"].ref
        # version = ref.version

        toolchain.generate()

validate(self)方法中

from conans import ConanFile

class HelloConan(ConanFile):
    name = "hello"
    version = "0.1"
    settings = "os", "compiler", "build_type", "arch"
    requires = "foo/1.0"

    def validate(self):
        if self.dependencies["foo"].ref.version == "1.2":
            raise ConanInvalidConfiguration("Foo 1.2 not supported")


        if self.dependencies["foo"].options.shared:
            raise ConanInvalidConfiguration("Foo shared not supported")

查看self.dependencies对象的完整参考

结论

这篇博文重点介绍了新的 CMake 集成。请注意,layout()generate()self.dependencies、补丁等对于其他集成(如新的AutotoolsMSBuild等)是通用的。

通过所有这些新的改进,我们希望为即将推出的 Conan 2.0 提供语法兼容的配方,以帮助在发布之前迁移它们。

我们现在已经发展和完善了这个模型几个版本,并且这些功能在 2.0 之前不应遭受重大界面更改,但请注意,它们仍然是实验性的,并且可能会发生重大更改,如文档中所述。

查看新的conan.tools 命名空间以了解所有新的集成。