Falko's Blog

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
)

Resources