Conan's New CMake integration offers a transparent experience
The CMake ecosystem and Conan have continually evolved since their conception. Building on this evolution we are pleased to present a new unified way to create Conan packages with CMake. In the near future these new generators will be introduced in the conan-center-index.
A Bit of History…
Back in the day when Conan was first created the CMake approach was based on “global variables.” The concepts of “modern CMake” and “Targets” didn’t exist or were very uncommon.
We created the first cmake
generator to address this issue. The way to consume Conan packages with the cmake generator was including a conanbuildinfo.cmake
file and calling a conan_basic_setup()
macro that adjusted the necessary CMake global variables to locate the include directories, the libraries to link with, and so on.
Later the cmake_multi generator gave support to multi-config projects like Visual Studio creating different conanbuildinfo.cmake
files for Release
or Debug
.
The usage of find_package()
in the CMake community was getting popular, so we created the cmake_find_package
generator. Conan generates different FindXXX.cmake
modules for each dependency, so you could call find_package(XXX)
and a bunch of variables were set, so you could link with your requirements.
Soon, a new concept called “modern CMake” was raised. The “target approach” assumed that global variables should not be used, and all the information about a library or executable should be associated with a “target.”
That’s why we created the cmake_find_package_multi
, based on targets, config files instead of modules, and it was able to support multi-configuration projects, using generator expressions.
Finally, we created the cmake_paths
generator to point the CMAKE_PREFIX_PATH
and CMAKE_MODULE_PATH
to the packages, in order to support people packaging modules and config cmake files in the Conan packages.
Too many CMake integrations, right?
A New Approach
We had so many CMake integrations but we felt like we were missing something. We wanted to have the same user experience and results when building a CMake project no matter if doing a conan create
, or developing a library calling “cmake” in the command line, or clicking a “build” button in the IDE.
To help resolve this missing piece we introduced two new generators:
- The CMakeToolchain generator to create a
conan_toolchain.cmake
. This file is saved after aconan install
based on the settings and options and can be used like any other CMake toolchain:
$ cmake . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
- The CMakeDeps generator, to manage the requirements and to generate the config and/or module files we created.
How to Migrate the Recipes
After the release of Conan 2.0 the only CMake integration will be CMakeToolchain
+ CMakeDeps
. These generators are already supported in Conan 1.40 and we will keep improving them in every release. We are hard at work to introduce these generators in the recipes of conan-center-index as soon as possible.
For more information you can check the Creating packages getting started.
Namespace change
The imports from the new integrations are in the conan.tools
namespace, not in the conan**s**
.
from conan.tools.cmake import CMakeToolchain, CMakeDeps
The generate()
method.
One important change in the “Conan 2.0 compatible recipes” is the generate()
method. This method is responsible for generating all the files needed so the build helpers (at the build()
method) can almost directly call the build system without any calculation. This enables the user to get the same build results in a conan create
or building in the command line.
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()
...
As we are not adjusting anything in the CMakeToolchain nor in the CMakeDeps, the previous example can be simplified to:
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()
...
Customizing the CMakeToolchain
The most common code to change is the .definitions
from the old CMake()
build helper. You have to migrate to the toolchain as .variables
:
From:
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()
...
To:
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()
See the full CMakeToolchain reference for all the customization.
Customizing the CMakeDeps
- At the
generate()
method there are some new things you can now adjust, like adding new custom user CMake configurations besides the standard ones (Release, Debug, etc) withcmake.configurations
and selecting the current configuration withcmake.configuration
:
def generate(self):
cmake = CMakeDeps(self)
cmake.configurations.append("ReleaseShared")
if self.options["hello"].shared:
cmake.configuration = "ReleaseShared"
cmake.generate()
To elaborate on this, see the full CMakeDeps reference.
- At the
package_info()
method, there are several properties you can configure to indicate to the generator how to behave when a consumer uses it (having a requirement to your package). Here is an example:
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")
The CMake
Build Helper
To leverage the new CMakeToolchain
and CMakeDeps
you have to import the new CMake build helper from the new conan.tools.cmake
namespace.
This build helper only calls cmake
passing the -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
.
As this build helper has no internal state anymore, there is an anti-pattern to avoid: keeping an instance of the build helper to use in the build()
method and later in the package()
method. This is considered an anti-pattern, because keeping the state between the methods of the recipe might fail in the conan local methods like conan build
+ conan export-pkg
where the execution is isolated.
From:
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()
...
To:
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()
The layout()
method
You can declare a [layout()](https://docs.conan.io/en/latest/developing_packages/package_layout.html)
method in the recipe to describe the package contents, not only the final package in the cache but also the package while developing. As the package will have the same structure in the cache and in our local directory, the recipe development becomes easier, even working with editable packages out of the box.
A couple of classic patterns you can avoid using the layout()
method are the following:
From:
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()
...
To:
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()
In the previous example, we used a predefined layout, the cmake_layout
. See more information about cmake_layout.
You can adjust any value after calling it to match your package structure.
Also the new tool apply_conandata_patches already knows where to locate the sources thanks to the layout
, so the base_path can be omitted in the conandata.yml
file.
See the layout() to learn more about it.
Access to the Dependencies
Sometimes in a recipe you need to access the dependencies to check something, typically the version and the root package folder. Previously this could be done by accessing the deps_cpp_info
object in almost any method of the recipe. With the new model the access to the dependencies should be done at the generate()
and the validate()
methods, using the new self.dependencies
object.
In the generate(self)
:
From:
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()
...
To:
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()
In the validate(self)
method:
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")
See the complete reference of the self.dependencies
object.
Conclusion
This blog post provided highlights about the new CMake integration. Please note that the layout()
, the generate()
, the self.dependencies
, the patches, etc, are common to other integrations like the new Autotools, MSBuild, etc.
With all these new improvements we want to provide syntax compatible recipes with the coming Conan 2.0 to help to migrate them before the release.
We have been evolving and improving this model for some releases now and these features should not suffer important interface changes before 2.0, but please take into account that they are still experimental and subject to breaking changes as noticed in the documentation.
See the new conan.tools namespace to learn more about all the new integrations.