Trying Conan with Modern CMake: Dependencies

2019 May 22

Conan is a cross-platform package manager targeting C and C++ projects. It is one of the leading options for package management in C++, but there are notable alternatives like vcpkg from Microsoft. There is not yet a clear winner in this format war. As a C++ developer who wants to try Conan, I want an easy pattern for integrating it into my packages, while leaving them as open as possible to switching to another package manager in case the landscape changes.

This is unlike the situation among C++ build systems, where CMake is the choice for cross-platform development. CMake likes to remind everyone that it is a build system generator, not a build system, but it is reaching a level of abstraction that lets us think of it as a cross-platform build system. We can build, test, and install our packages using only cmake and ctest.

# Works on Linux, OSX, and Windows.
$ cmake -DCMAKE_BUILD_TYPE=${build_type} ${source_dir}
$ ncpus=$(python -c 'import multiprocessing as mp; print(mp.cpu_count())')
$ cmake --build . --parallel ${ncpus}
$ ctest --parallel ${ncpus}
$ cmake --build . --target install

Then, assuming that I am using CMake for building, testing, and installing, how can I best integrate Conan non-intrusively for importing dependencies and packaging my project? That means without editing CMakeLists.txt if at all possible.

Dependencies

There is (thankfully) exactly one convention for declaring a dependency in Modern CMake:

# Step 1: Import the declarations of the dependency's targets.
find_package(${package_name} REQUIRED)
# Step 2: Declare that my targets depend on their targets.
target_link_libraries(${my_target} ${package_name}::${their_target})

find_package runs in one of two different modes:

  1. Module Mode has a limited set of options and searches on the Module Search Path for a file with the name Find<PackageName>.cmake, called a Find Module (FM). The default Module Search Path just points to a private directory in the installation of CMake itself that has FMs for many popular libraries. We can add one or more directories to the front of the Module Search Path by setting CMAKE_MODULE_PATH. FMs are generally expected to be authored by dependents and to exist within the dependent source tree.

  2. Config Mode adds some more (advanced) options, and searches on the Installation Prefix Path for a file with a name like <PackageName>Config.cmake or <package-name-lower>-config.cmake, called a Package Configuration File (PCF). The "Installation Prefix Path" is an oversimplification of the actual search procedure, but we only need to know that it includes CMAKE_INSTALL_PREFIX by default and that we can add one or more directories to the front of it by setting CMAKE_PREFIX_PATH. PCFs are generally expected to be authored by dependencies and to be installed alongside their artifacts.

It is possible to pass an option to select the mode (MODULE for Module Mode; NO_MODULE or CONFIG for Config Mode), but it is recommended to leave these options out. In that case, find_package will look for an FM first (which is the "classic" behavior, since FMs predate PCFs), and if it fails to find one, it will look for a PCF second.

Once a file is found, it is included and carries out two important functions:

  1. It checks that the version it exports is compatible with the version that was requested (via the VERSION argument to find_package), which is supplied to it during its execution as the variable PACKAGE_FIND_VERSION.

  2. It defines targets for users to reference in target_link_libraries, which is the way to declare that one target depends on another.

Regardless of how our dependencies are installed, we want to follow this general pattern of find_package and target_link_libraries. When installing our packages with Conan, there are two cases we need to consider:

  1. The dependency installs a PCF that is built by the dependency and not by Conan.

  2. The dependency does not install a PCF. In this case, Conan can generate an FM for us.

Let's cover these cases in the reverse order, because the way we handle the first depends on how we handle the second.

Generated Find Modules

When a dependency does not install a PCF, we can get Conan to generate an FM by using the cmake_find_package generator. We just need to add the build directory (where Conan puts the FM) to the CMake module search path so that CMake can find it with find_package:

$ conan install ${source_dir}
# We MUST use absolute paths for CMAKE_MODULE_PATH.
$ cmake -DCMAKE_MODULE_PATH=${PWD} ${source_dir}

The generated FM exports only one target. It carries everything that the dependency author declared in their Conan recipe. The target is named after the package and scoped within a namespace named after the package. Using doctest as an example, the target will be doctest::doctest:

find_package(doctest REQUIRED)
add_executable(my_test my_test.cpp)
target_link_libraries(my_test doctest::doctest)

Installed Package Configuration Files

When a dependency installs its own PCF, we should let CMake find it because it likely gives us richer targets carefully constructed by the dependency's authors. We can achieve this with Conan by using the cmake_paths generator. It generates a CMake script, conan_paths.cmake, that adds all of our dependencies' installation prefixes (managed for us in the local Conan cache) to CMAKE_PREFIX_PATH, which is searched by find_package for PCFs. When we configure a build directory with CMake, we need CMake to include that script. There are two variables we can use: CMAKE_TOOLCHAIN_FILE or CMAKE_PROJECT_<PROJECT-NAME>_INCLUDE.

$ conan install ${source_dir}
$ cmake -DCMAKE_TOOLCHAIN_FILE=conan_paths.cmake ${source_dir}
# -- or --
$ cmake -DCMAKE_PROJECT_${project_name}_INCLUDE=conan_paths.cmake ${source_dir}

In the call to find_package, we must be sure to use the name of the package as it is spelled in the file name of the PCF, which may (but most likely will not) differ from the name of the package in Conan. When in doubt, we can manually search the installation prefixes of our dependencies—they will be listed in the generated conan_paths.cmake—for PCFs.

Both

Likely, not all of our dependencies will fit neatly into only one category above. As a general, always-works approach, we can use both the cmake_paths and cmake_find_package generators. The conan_paths.cmake script will set the CMAKE_MODULE_PATH—so that we don't have to—and it will include the directory containing conan_paths.cmake, which should also contain the generated FMs.

At this point, we should be worried about a conflict between the installed PCFs and the generated FMs. Conan will indiscriminately generate an FM for every dependency, whether it installs a PCF or not, and since CMake searches for FMs before PCFs, it will never find the PCFs. Fortunately, as I mentioned above, we can pass an option to find_package to select its mode:

find_package(pcf_package CONFIG REQUIRED)
find_package(fm_package MODULE REQUIRED)

(An upcoming alternative is to set the variable CMAKE_FIND_PACKAGE_PREFER_CONFIG to TRUE, which flips the search preference for find_package. That feature was implemented just hours before this writing and has not shipped yet.)

If a dependency does not install a PCF and the generated FM does not work, then you will have to write your own FM. That endeavor is out of scope for this post.

Summary

Putting it all together so far, this is how we should import dependencies with CMake and Conan:

  • Intentionally choose CONFIG or MODULE mode for each call to find_package in our CMakeLists.txt.

  • Use both the cmake_find_package and cmake_paths generators in our conanfile.{txt,py}.

  • Pass conan_paths.cmake as the CMAKE_TOOLCHAIN_FILE to CMake.

In my next post, I'll talk about how we can package a Modern CMake project with Conan.