Understanding the Conan Package layout

In the previous section, we introduced the concept of editable packages and mentioned that the reason they work out of the box when put in editable mode is due to the current definition of the information in the layout() method. Let’s examine this feature in more detail.

In this tutorial, we will continue working with the say/1.0 package and the hello/1.0 consumer used in the editable packages tutorial.

Please, first of all, 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/tutorial/developing_packages/package_layout

Note

We use CMake presets in this example. This requires CMake >= 3.23 because the “include” from CMakeUserPresets.json to CMakePresets.json is only supported since that version. If you prefer not to use presets you can use something like:

cmake <path> -G <CMake generator> -DCMAKE_TOOLCHAIN_FILE=<path to
conan_toolchain.cmake> -DCMAKE_BUILD_TYPE=Release

Conan will show the exact CMake command everytime you run conan install in case you can’t use the presets feature.

As you can see, the main folder structure is the same:

.
├── hello
│   ├── CMakeLists.txt
│   ├── conanfile.py
│   └── src
│       └── hello.cpp
└── say
    ├── CMakeLists.txt
    ├── conanfile.py
    ├── include
    │   └── say.h
    └── src
        └── say.cpp

The main difference here is that we are not using the predefined cmake_layout() in the say/1.0 ConanFile, but instead, we are declaring our own custom layout. Let’s see how we describe the information in the layout() method so that it works both when we create the package in the Conan local cache and also when the package is in editable mode.

say/conanfile.py
import os
from conan import ConanFile
from conan.tools.cmake import CMake


class SayConan(ConanFile):
    name = "say"
    version = "1.0"

    exports_sources = "CMakeLists.txt", "src/*", "include/*"

    ...

    def layout(self):

        ## define project folder structure

        self.folders.source = "."
        self.folders.build = os.path.join("build", str(self.settings.build_type))
        self.folders.generators = os.path.join(self.folders.build, "generators")

        ## cpp.package information is for consumers to find the package contents in the Conan cache

        self.cpp.package.libs = ["say"]
        self.cpp.package.includedirs = ["include"] # includedirs is already set to 'include' by
                                                   # default, but declared for completion
        self.cpp.package.libdirs = ["lib"]         # libdirs is already set to 'lib' by
                                                   # default, but declared for completion

        ## cpp.source and cpp.build information is specifically designed for editable packages:

        # this information is relative to the source folder that is '.'
        self.cpp.source.includedirs = ["include"] # maps to ./include

        # this information is relative to the build folder that is './build/<build_type>', so it will
        self.cpp.build.libdirs = ["."]  # map to ./build/<build_type> for libdirs

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

Let’s review the layout() method. You can see that we are setting values for self.folders and self.cpp. Let’s explain what these values do.

self.folders

Defines the structure of the say project for the source code and the folders where the files generated by Conan and the built artifacts will be located. This structure is independent of whether the package is in editable mode or exported and built in the Conan local cache. Let’s define the folder structure for the say package:

say
 ├── CMakeLists.txt
 ├── conanfile.py
 ├── include
 │   └── say.h
 ├── src
 │   └── say.cpp
 └── build
     ├── Debug            --> Built artifacts for Debug
     │   └── generators   --> Conan generated files for Debug config
     └── Release          --> Built artifacts for Release
         └── generators   --> Conan generated files for Release config
  • As we have our CMakeLists.txt in the . folder, self.folders.source is set to ..

  • We set self.folders.build to be ./build/Release or ./build/Debug depending on the build_type setting. These are the folders where we want the built binaries to be located.

  • The self.folders.generators folder is the location we set for all the files created by the Conan generators. In this case, all the files generated by the CMakeToolchain generator will be stored there.

Note

Please note that the values above are for a single-configuration CMake generator. To support multi-configuration generators, such as Visual Studio, you should make some changes to this layout. For a complete layout that supports both single-config and multi-config, please check the cmake_layout() in the Conan documentation.

self.cpp

This attribute is used to define where consumers will find the package contents (headers files, libraries, etc.) depending on whether the package is in editable mode or not.

cpp.package

First, we set the information for cpp.package. This defines the contents of the package and its location relative to the folder where the package is stored in the local cache. Please note that defining this information is equivalent to defining self.cpp_info in the package_info() method. This is the information we defined:

  • self.cpp.package.libs: we add the say library so that consumers know that they should link with it. This is equivalent to declaring self.cpp_info.libs in the package_info() method.

  • self.cpp.package.libdirs: we add the lib folder so that consumers know that they should search there for the libraries. This is equivalent to declaring self.cpp_info.libdirs in the package_info() method. Note that the default value for libdirs in both the cpp_info and cpp.package is ["lib"] so we could have omitted that declaration.

  • self.cpp.package.includedirs: we add the include folder so that consumers know that they should search there for the library headers. This is equivalent to declaring self.cpp_info.includedirs in the package_info() method. Note that the default value for includedirs in both the cpp_info and cpp.package is ["include"] so we could have omitted that declaration.

To check how this information affects consumers we are going to do first do a conan create on the say package:

$ cd say
$ conan create . -s build_type=Release

When we call conan create, Conan moves the recipe and sources declared in the recipe to be exported to the local Cache to a recipe folder and after that, it will create a separate package folder to build the binaries and store the actual package contents. If you check in the [YOUR_CONAN_HOME]/p folder, you will find two new folders similar to these:

Tip

You could get the exact locations for this folders using the conan cache command or checking the output of the conan create command.

<YOUR_CONAN_HOME>/p
├── sayb3ea744527a91      --> folder for sources
│   └── ...
│
└── say830097e941e10      --> folder for building and storing the package binaries
    ├── b
    │   ├── build
    │   │   └── Release
    │   ├── include
    │   │   └── say.h
    │   └── src
    │       ├── hello.cpp
    │       └── say.cpp
    └── p
        ├── include       --> defined in cpp.package.includedirs
        │   └── say.h
        └── lib           --> defined in cpp.package.libdirs
            └── libsay.a  --> defined in self.cpp.package.libs

You can identify there the structure we defined in the layout() method. If you build the hello consumer project now, it will search for all the headers and libraries of say in that folder inside the local Cache in the locations defined by cpp.package:

$ cd ../hello
$ conan install . -s build_type=Release

# Linux, MacOS
$ cmake --preset conan-release --log-level=VERBOSE
# Windows
$ cmake --preset conan-default --log-level=VERBOSE

...
-- Conan: Target declared 'say::say'
-- Conan: Library say found <YOUR_CONAN_HOME>p/say8938ceae216fc/p/lib/libsay.a
-- Created target CONAN_LIB::say_say_RELEASE STATIC IMPORTED
-- Conan: Found: <YOUR_CONAN_HOME>p/p/say8938ceae216fc/p/lib/libsay.a
-- Configuring done
...

$ cmake --build --preset conan-release
[ 50%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello

cpp.source and cpp.build

We also defined cpp.source and cpp.build attributes in our recipe. These are only used when the package is in editable mode and point to the locations that consumers will use to find headers and binaries. We defined:

  • self.cpp.source.includedirs set to ["include"]. This location is relative to the self.folders.source that we defined to .. In the case of editable packages, this location will be the local folder where we have our project.

  • self.cpp.build.libdirs set to ["."]. This location is relative to the self.folders.build that we defined to ./build/<build_type>. In the case of editable packages, this location will point to <local_folder>/build/<build_type>.

Note that other cpp.source and cpp.build definitions are also possible, with different meanings and purposes, for example:

  • self.cpp.source.libdirs and self.cpp.source.libs could be used if we had pre-compiled libraries in the source repo, committed to git, for example. They are not a product of the build, but rather part of the sources.

  • self.cpp.build.includedirs could be use for folders containing headers generated at build time, as it usually happens by some code generators that are fired by the build before starting to compile the project.

To check how this information affects consumers, we are going to first put the say package in editable mode and build it locally.

$ cd ../say
$ conan editable add . --name=say --version=1.0
$ conan install . -s build_type=Release
$ cmake --preset conan-release
$ cmake --build --preset conan-release

You can check the contents of the say project’s folder now, you can see that the output folders match the ones we defined with self.folders:

.
├── CMakeLists.txt
├── CMakeUserPresets.json
├── build
│   └── Release       --> defined in cpp.build.libdirs
│       ├── ...
│       ├── generators
│       │   ├── CMakePresets.json
│       │   ├── ...
│       │   └── deactivate_conanrun.sh
│       └── libsay.a  --> no need to define
├── conanfile.py
├── include           --> defined in cpp.source.includedirs
│   └── say.h
└── src
    ├── hello.cpp
    └── say.cpp

Now that we have the say package in editable mode, if we build the hello consumer project, it will search for all the headers and libraries of say in the folders defined by cpp.source and cpp.build:

$ cd ../hello
$ conan install . -s build_type=Release

# Linux, MacOS
$ cmake --preset conan-release --log-level=VERBOSE
# Windows
$ cmake --preset conan-default --log-level=VERBOSE

...
-- Conan: Target declared 'say::say'
-- Conan: Library say found <local_folder>/examples2/tutorial/developing_packages/package_layout/say/build/Release/libsay.a
-- Conan: Found: <local_folder>/examples2/tutorial/developing_packages/package_layout/say/build/Release/libsay.a
-- Configuring done
...

$ cmake --build --preset conan-release
[ 50%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello

$ conan editable remove --refs=say/1.0

Note

Please, note that we did not define self.cpp.build.libs = ["say"]. This is because the information set in self.cpp.source and self.cpp.build will be merged with the information set in self.cpp.package so that you only have to define things that change for the editable package. For the same reason, you could also omit setting self.cpp.source.includedirs = ["include"] but we left it there to show the use of cpp.source.