Installing a Config.cmake file
In this article I describe how to setup your CMake based C++ project so that it generates and installs a
Config.cmake
file. I learned most of what is written here while improving the CMake project
of my own library, the UI automation tool Spix.
Why?
Generating a Config.cmake
file and installing it makes it easier for other projects (and people) to use your library, as they no longer
have to write their own Find*.cmake
modules. find_package(Spix)
will work right away after installing Spix.
If you install the library in a local directory, you can even move it around and the Config.cmake
file will still work.
Background on CMake modules and packages
When your CMake project has dependencies, you will usually use find_package(<name>)
in your CMakeLists.txt
to find
them and make them available.
find_package
can run in two modes: “Module Mode” and “Config Mode”.
Module Mode
In “Module Mode” a Find<library name>.cmake
file is searched in the module path and then run. Usually these Find-Modules
will search in the standard system paths and possibly try some magic to locate the library and its headers on your system.
Once found, the module will add an imported library target and with that make the library available to other cmake targets
(like your executable).
The important part is that every project linking to a certain library will usually have to write its own Find-Module (at least if you are unlucky and CMake doesn’t already ship with one). This leads to a lot of duplicated work which wouldn’t be necessary if the library already shipped with a generic module. This is where “Config Mode” comes to the rescue.
Config Mode
In “Config Mode”, CMake looks for a <library name>Config.cmake
file (or <library name>-config.cmake
). This file
is created by installing the library. This means that a very simple Config.cmake
file could just contain
hardcoded paths to installed files and register an imported library-target with them.
CMake alredy comes with tools to auto-generate most parts of this Config.cmake
file and this post explains how to
use those.
Generating and Installing Config.cmake
Installing Targets
If you are already installing the library, you probably have an install()
command like the following in your
CMakeLists.txt
file:
install(
TARGETS YourLibrary
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
The first step is to now associate the exported targets with an “export name”. This is done by adding the
EXPORT
option. Later, the “export name” can be used to reference the installed targets and actually
export them into a file.
The new install()
command looks like this:
install(
TARGETS YourLibrary
EXPORT YourLibraryTargets
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
Now that we can keep track of the installed targets, we can actually install a CMake file that contains them:
install(
EXPORT YourLibraryTargets
FILE YourLibraryTargets.cmake
DESTINATION "${CMAKE_INSTALL_DATADIR}/YourLibrary/cmake"
NAMESPACE YourLibrary::
)
It is good practice to use a namespace for the exported targets. Applications using this library could
now use find_package(YourLibrary)
and then link with YourLibrary::YourLibrary
. This seems
a bit redundant in this small example, but can be really useful once you export and install more than
one target.
When you run CMake now to build and install the library, you will see that several additional files
got installed: A main YourLibraryTargets.cmake
file as well as several YourLibraryTargets-<config>.cmake
files, one for each build configuration. The per-config files are all included from the
main YourLibraryTargets.cmake
file.
The exported files with the targets are not enough for a proper Config Module, as they do not handle
the dependencies of the library (i.e. they do not contain any find_package()
commands).
In the next step, we will create the root Config.cmake
file.
Root Config.cmake file
The root YourLibraryConfig.cmake
file loads the dependencies of the library and then includes the
exported YourLibraryTargets.cmake
file. An easy way to create a Config.cmake
file that can later
be moved to a different location (together with the library) is the CMakePackageConfigHelpers
module.
We start by writing a template, YourLibraryConfig.cmake.in
:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(Threads)
include("${CMAKE_CURRENT_LIST_DIR}/YourLibraryTargets.cmake")
check_required_components(Spix)
The @PACKAGE_INIT@
part will be replaced by the CMakePackageConfigHelpers
.
This example assumes that the library depends on Threads
and the find_package(Threads)
call
in the library has been transformed to find_dependency
, which is a helper macro that takes care of
forwarding parameters from the original find_package
call.
Once you have this template, you need to configure it and then install it. This is done in your CMakeLists.txt with the following commands:
include(CMakePackageConfigHelpers)
configure_package_config_file(
cmake/YourLibraryConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/YourLibraryConfig.cmake"
INSTALL_DESTINATION "${CMAKE_INSTALL_DATADIR}/YourLibrary/cmake"
)
install(
FILES "${CMAKE_CURRENT_BINARY_DIR}/YourLibraryConfig.cmake"
DESTINATION "${CMAKE_INSTALL_DATADIR}/YourLibrary/cmake"
)
The first call builds the final YourLibraryConfig.cmake
file and the install()
call installs
it in its final destination.
Adapting include paths
Depending on how you setup the public include directories of your library, you might be seing an error like this once you run cmake for your library:
CMake Error in CMakeLists.txt:
Target "YourLibrary" INTERFACE_INCLUDE_DIRECTORIES property contains path:
"/some/path/to/lib/include"
which is prefixed in the source directory.
This error occurs if you set the PUBLIC
header in target_include_directories
to a path in the
source tree of the library. While this is just fine for building the library, the include path for
the installed target needs to be the path to the installed headers.
You can fix this by using generator expressions to configure different header directories for building and installing.
The original code:
target_include_directories(YourLibrary
PUBLIC
include
PRIVATE
src
)
then turns into:
target_include_directories(YourLibrary
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
src
)