Conan 的新 CMake 集成提供了一种透明的体验
自从 CMake 生态系统和 Conan 诞生以来,它们一直在不断发展。基于这种演变,我们很高兴地推出一种使用 CMake 创建 Conan 包的新统一方法。在不久的将来,这些新的生成器将在conan-center-index中引入。
一点历史...
在 Conan 最初创建的时候,CMake 的方法是基于“全局变量”。“现代 CMake”和“目标”的概念当时不存在或非常不常见。
我们创建了第一个cmake
生成器来解决这个问题。使用cmake生成器使用 Conan 包的方法是包含一个conanbuildinfo.cmake
文件并调用一个conan_basic_setup()
宏,该宏调整必要的 CMake 全局变量以定位包含目录、要链接的库等等。
后来cmake_multi生成器为 Visual Studio 等多配置项目提供了支持,为Release
或Debug
创建不同的conanbuildinfo.cmake
文件。
在 CMake 社区中,使用find_package()
变得越来越流行,因此我们创建了cmake_find_package
生成器。Conan 为每个依赖项生成不同的FindXXX.cmake
模块,因此您可以调用find_package(XXX)
,并且会设置一堆变量,以便您可以链接到您的需求。
很快,一个名为“现代 CMake”的新概念被提出。“目标方法”假设不应该使用全局变量,并且关于库或可执行文件的所有信息都应该与一个“目标”相关联。
这就是为什么我们创建了cmake_find_package_multi
,它基于目标、配置文件而不是模块,并且能够支持多配置项目,使用生成器表达式。
最后,我们创建了cmake_paths
生成器来将CMAKE_PREFIX_PATH
和CMAKE_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()
...
由于我们没有在CMakeToolchain或CMakeDeps中调整任何内容,因此前面的示例可以简化为
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
构建帮助程序
要利用新的CMakeToolchain
和CMakeDeps
,您必须从新的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
、补丁等对于其他集成(如新的Autotools、MSBuild等)是通用的。
通过所有这些新的改进,我们希望为即将推出的 Conan 2.0 提供语法兼容的配方,以帮助在发布之前迁移它们。
我们现在已经发展和完善了这个模型几个版本,并且这些功能在 2.0 之前不应遭受重大界面更改,但请注意,它们仍然是实验性的,并且可能会发生重大更改,如文档中所述。
查看新的conan.tools 命名空间以了解所有新的集成。