Define components for Conan packages that provide multiple libraries

At the section of the tutorial about the package_info() method, we learned how to define information in a package for consumers, such as library names or include and library folders. In the tutorial, we created a package with only one library that consumers linked to. However, in some cases, libraries provide their functionalities separated into different components. These components can be consumed independently, and in some cases, they may require other components from the same library or others. For example, consider a library like OpenSSL that provides libcrypto and libssl, where libssl depends on libcrypto.

Conan provides a way to abstract this information using the components attribute of the CppInfo object to define the information for each separate component of a Conan package. Consumers can also select specific components to link against but not the rest of the package.

Let’s take a game-engine library as an example, which provides several components such as algorithms, ai, rendering, and network. Both ai and rendering depend on the algorithms component.

digraph components { node [fillcolor="lightskyblue", style=filled, shape=box] algorithms -> ai; algorithms -> rendering; ai; algorithms; network; }

components of the game-engine package

Please, first clone the sources to recreate this project. You can find them in the examples2 repository in GitHub:

$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/examples/conanfile/package_info/components

You can check the contents of the project:

.
├── CMakeLists.txt
├── conanfile.py
├── include
│   ├── ai.h
│   ├── algorithms.h
│   ├── network.h
│   └── rendering.h
├── src
│   ├── ai.cpp
│   ├── algorithms.cpp
│   ├── network.cpp
│   └── rendering.cpp
└── test_package
    ├── CMakeLists.txt
    ├── CMakeUserPresets.json
    ├── conanfile.py
    └── src
        └── example.cpp

As you can see, there are sources for each of the components and a CMakeLists.txt file to build them. We also have a test_package that we are going to use to test the consumption of the separate components.

First, let’s have a look at package_info() method in the conanfile.py and how we declared the information for each component that we want to provide to the consumers of the game-engine package:

...

def package_info(self):
    self.cpp_info.components["algorithms"].libs = ["algorithms"]
    self.cpp_info.components["algorithms"].set_property("cmake_target_name", "algorithms")

    self.cpp_info.components["network"].libs = ["network"]
    self.cpp_info.components["network"].set_property("cmake_target_name", "network")

    self.cpp_info.components["ai"].libs = ["ai"]
    self.cpp_info.components["ai"].requires = ["algorithms"]
    self.cpp_info.components["ai"].set_property("cmake_target_name", "ai")

    self.cpp_info.components["rendering"].libs = ["rendering"]
    self.cpp_info.components["rendering"].requires = ["algorithms"]
    self.cpp_info.components["rendering"].set_property("cmake_target_name", "rendering")

There are a couple of relevant things:

  • We declare the libraries generated by each of the components by setting information in the cpp_info.components attribute. You can set the same information for each of the components as you would for the self.cpp_info object. The cpp_info for components has some defaults defined, just like it does for self.cpp_info. For example, the cpp_info.components object provides the .includedirs and .libdirs properties to define those locations, but Conan sets their value as ["lib"] and ["include"] by default, so it’s not necessary to add them in this case.

  • We are also declaring the components’ dependencies using the .requires attribute. With this attribute, you can declare requirements at the component level, not only for components in the same recipe but also for components from other packages that are declared as requires of the Conan package.

  • We are changing the default target names for the components using the properties model. By default, Conan sets a target name for components like <package_name::component_name>, but for this tutorial we will set the component target names just with the component names omitting the ::.

You can have a look at the consumer part by checking the test_package folder. First the conanfile.py:

...

def generate(self):
    deps = CMakeDeps(self)
    deps.check_components_exist = True
    deps.generate()

You can see that we are setting the check_components_exist property for CMakeDeps. This is not needed, just to show how you can do if you want your consumers to fail if the component does not exist. So, the CMakeLists.txt could look like this:

cmake_minimum_required(VERSION 3.15)
project(PackageTest CXX)

find_package(game-engine REQUIRED COMPONENTS algorithms network ai rendering)

add_executable(example src/example.cpp)

target_link_libraries(example algorithms
                              network
                              ai
                              rendering)

And the find_package() call would fail if any of the components targets do not exist.

Let’s run the example:

$ conan create .
...
game-engine/1.0: RUN: cmake --build "/Users/barbarian/.conan2/p/t/game-d6e361d329116/b/build/Release" -- -j16
[ 12%] Building CXX object CMakeFiles/algorithms.dir/src/algorithms.cpp.o
[ 25%] Building CXX object CMakeFiles/network.dir/src/network.cpp.o
[ 37%] Linking CXX static library libnetwork.a
[ 50%] Linking CXX static library libalgorithms.a
[ 50%] Built target network
[ 50%] Built target algorithms
[ 62%] Building CXX object CMakeFiles/ai.dir/src/ai.cpp.o
[ 75%] Building CXX object CMakeFiles/rendering.dir/src/rendering.cpp.o
[ 87%] Linking CXX static library libai.a
[100%] Linking CXX static library librendering.a
[100%] Built target ai
[100%] Built target rendering
...

======== Launching test_package ========

...
-- Conan: Component target declared 'algorithms'
-- Conan: Component target declared 'network'
-- Conan: Component target declared 'ai'
-- Conan: Component target declared 'rendering'
...
[ 50%] Building CXX object CMakeFiles/example.dir/src/example.cpp.o
[100%] Linking CXX executable example
[100%] Built target example


======== Testing the package: Executing test ========
game-engine/1.0 (test package): Running test()
game-engine/1.0 (test package): RUN: ./example
I am the algorithms component!
I am the network component!
I am the ai component!
└───> I am the algorithms component!
I am the rendering component!
└───> I am the algorithms component!

You could check that requiring a component that does not exist will raise an error. Add the nonexistent component to the find_package() call:

cmake_minimum_required(VERSION 3.15)
project(PackageTest CXX)

find_package(game-engine REQUIRED COMPONENTS nonexistent algorithms network ai rendering)

add_executable(example src/example.cpp)

target_link_libraries(example algorithms
                              network
                              ai
                              rendering)

And test the package again:

$ conan test test_package game-engine/1.0

...

Conan: Component 'nonexistent' NOT found in package 'game-engine'
Call Stack (most recent call first):
CMakeLists.txt:4 (find_package)

-- Configuring incomplete, errors occurred!

...

ERROR: game-engine/1.0 (test package): Error in build() method, line 22
        cmake.configure()
        ConanException: Error 1 while executing

See also

If you want to use recipes defining components in editable mode, check the example in Using components and editable packages.