Trying Conan with Modern CMake: Dependencies
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:
-
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 settingCMAKE_MODULE_PATH
. FMs are generally expected to be authored by dependents and to exist within the dependent source tree. -
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 includesCMAKE_INSTALL_PREFIX
by default and that we can add one or more directories to the front of it by settingCMAKE_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:
-
It checks that the version it exports is compatible with the version that was requested (via the
VERSION
argument tofind_package
), which is supplied to it during its execution as the variablePACKAGE_FIND_VERSION
. -
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:
-
The dependency installs a PCF that is built by the dependency and not by Conan.
-
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
orMODULE
mode for each call tofind_package
in ourCMakeLists.txt
. -
Use both the
cmake_find_package
andcmake_paths
generators in ourconanfile.{txt,py}
. -
Pass
conan_paths.cmake
as theCMAKE_TOOLCHAIN_FILE
to CMake.
In my next post, I'll talk about how we can package a Modern CMake project with Conan.