may contain source code

published: • tags:

CMake has come a long way since the 2.x dark ages. But even in the newest versions some things can still feel confusing and rather convoluted. Installing is one of them. It’s relatively straight forward for applications meant for end users. However libraries that are supposed to be used as dependencies by other developers from other projects, those are a different story.

This article is about exactly that: installing a library with everything required that other CMake projects can make convenient use of it. Note that this is different from packaging. Creating Windows installers, packages for a Linux package manager, or Apple bundles is a whole topic in itself. We’ll not go into it here.

The Goal

What we want to end up with is a make install command (or its equivalent for Ninja, MSBuild, etc.) that takes care of the following:

  • Installs all binaries.
  • Installs the public header files.
  • Creates and installs a complete exported project so that other projects can use our library via find_package(). Complete means it includes versioning information and takes care of finding transitive dependencies. For example, if we depend on Boost we must not force our dependees to call find_package(Boost). That’s our job.

We’ll use CppUuid as an example, a small C++ library for UUIDs I’m currently playing with. The details aren’t particularly important. You only need to know that the library depends on one generated configuration header file and has an optional dependency on Qt, controlled by the ENABLE_QSTRING CMake option.

Structure of the Exported Project

Installing binaries and headers is straight forward. Setting up the exported project needs a closer look. Let’s step back for a moment and consider how CMake finds dependencies.

An exported project is what CMake looks for when it encounters a find_package(<projectname> <version>) call. Usually an exported project is located in the lib/cmake/<projectname> directory. Without any customization the following files can be found there:

<projectname>Config.cmake
That’s the main file find_package() looks for. The main configuration of the exported project happens here. Also that file includes all available <projectname>Config-<buildtype>.cmake files.
<projectname>Config-<buildtype>.cmake
Each of these files contains the configuration for a specific build type (release, debug, etc.). It’s CMake’s way to install, for example, a release and debug build in parallel.
<projectname>ConfigVersion.cmake
Contains the version number info for the exported project. This file is not included anywhere else, but run by find_package() directly.

Unfortunately CMake generates the Config and Config-<buildtype> files on its own without a way to add custom code. For our CppUuid example that’s not sufficient. We may have a dependency on Qt. Somwhere the exported project must perform the appropriate find_package(Qt5) call. However CMake allows us to pick the names of those generated config files. So, let’s add another level of indirection :-). CppUuid creates the following files in the lib directory for its exported project:

<prefix dir>
\__ lib
    \__ cmake
        \__ cppuuid
            \__ cppuuidConfig.cmake
            \__ cppuuidConfigVersion.cmake
            \__ cppuuidTargets.cmake
            \__ cppuuidTargets-release.cmake

The original Config and Config-<buildtype> files are renamed to Targets and Targets-<buildtype>. Then there’s the new cppuuidConfig.cmake. That file is completely controlled by us and contains any custom code necessary.

When our users call something like find_package(cppuuid 1.0) CMake finds cppuuidConfig.cmake and runs it. That file uses the include() command to pull in cppuuidTargets.cmake, which in turn includes all cppuuidTargets-<buildmode>.cmake files it can find. Also, cppuuidConfigVersion.cmake is run to find out if the requested version is compatible with the installed one.

When you look through your lib/cmake directory you’ll find a whole bunch of projects using this exact layout. It’s not enforced by CMake, but it’s a strong convention.

Code Overview

A Macro for Logging find_package() Calls

As mentioned above, CppUuid has an optional dependency on Qt. Forcing our users to make the matching find_package() call themselves won’t do. Therefore it must become a part of the exported project. But how do we know which exact find_package() calls to put there without duplicating them manually? CMake provides no such functionality.

The following find_package_and_export() macro is a first attempt at solving the problem. it works reasonably smoothly, but I wouldn’t rule out a more elegant solution just yet. This is the code:

macro(find_package_and_export)
    find_package(${ARGN})

    string(REPLACE ";" " " argn_with_spaces "${ARGN}")
    string(APPEND ${PROJECT_NAME}_FIND_DEPENDENCY_CALLS
        "find_dependency("
        "${argn_with_spaces}"
        ")\n"
    )
endmacro()

The macro is a 1:1 replacement for the equivalent find_package() call. It takes an arbitrary list of parameters and calls find_package() with them. Then it rebuilds the exact call as a string and appends it to a per-project variable. As a result each project where the macro is called has a <projectname>_FIND_DEPENDENCY_CALLS variable that contains all the logged calls separated by newlines. Incidentally that’s axactly the format we need for our exported project.

Installation and Exporting Code

Here is the rest of the relevant CMake code. We’ll go over it step by step in the following sections.

snippet from CMakeLists.txt

project(cppuuid LANGUAGES CXX VERSION 0.1.0)
add_library(${PROJECT_NAME} "<all the source and header files>")

if (ENABLE_QSTRING)
    find_package_and_export(Qt5 REQUIRED COMPONENTS Core)
    target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Core)
endif()

include("cmake/setup_installation.cmake")

The installation code is in its own file to keep the main CMakeLists.txt as uncluttered as possible.

cmake/setup_installation.cmake

include(CMakePackageConfigHelpers)

set(exported_targets_name "${PROJECT_NAME}Targets")
set(exported_targets_filename "${exported_targets_name}.cmake")
set(export_dirpath "lib/cmake/cppuuid")
set(config_basename "${PROJECT_NAME}Config")
set(config_filename "${config_basename}.cmake")
set(version_filename "${config_basename}Version.cmake")

write_basic_package_version_file(
    ${version_filename}
    COMPATIBILITY SameMajorVersion
)

configure_file("cmake/${config_filename}.in" "${config_filename}" @ONLY)

install(
    TARGETS ${PROJECT_NAME}
    EXPORT ${exported_targets_name}
    ARCHIVE DESTINATION "lib"
    PUBLIC_HEADER DESTINATION "include/cppuuid"
)
install(
    EXPORT ${exported_targets_name}
    FILE ${exported_targets_filename}
    NAMESPACE cppuuid::
    DESTINATION ${export_dirpath}
)
install(
    FILES
        "${CMAKE_CURRENT_BINARY_DIR}/${config_filename}"
        "${CMAKE_CURRENT_BINARY_DIR}/${version_filename}"
    DESTINATION
        ${export_dirpath}
)
install(
    DIRECTORY
        "${CMAKE_CURRENT_SOURCE_DIR}/include/cppuuid"
        "${CMAKE_CURRENT_BINARY_DIR}/include/cppuuid"
    DESTINATION
        "include"
)

cmake/cppuuidConfig.cmake.in

include(CMakeFindDependencyMacro)
@cppuuid_FIND_DEPENDENCY_CALLS@
include("${CMAKE_CURRENT_LIST_DIR}/@exported_targets_filename@")

Code in Detail

Including Helper Functions

include(CMakePackageConfigHelpers)

CMake ships with the CMakePackageConfigHelpers module containing helper functions for exporting projects. We use it further down to generate the version info file.

Variables for Often Needed Strings

set(exported_targets_name "${PROJECT_NAME}Targets")
set(exported_targets_filename "${exported_targets_name}.cmake")
set(export_dirpath "lib/cmake/cppuuid")
set(config_basename "${PROJECT_NAME}Config")
set(config_filename "${config_basename}.cmake")
set(version_filename "${config_basename}Version.cmake")

Quite a few strings – mostly paths and file names – are needed in multiple places. Duplication is bad, hence the variables.

Most variables should be self explanatory, except possibly exported_targets_name. We need to define an internal name for the core part of the exported project. To keep things consistent that name follows the same pattern as the Targets files of the exported project.

Exporting Version Information

To specify the version of your library prefer the VERSION parameter of the project() command if your version number scheme doesn’t prevent that entirely. For installing purposes it only simplifies creating the version info file slightly. But specifying the version in a way that CMake understands has a few more benefits. For example, shared libraries automatically get a versioned file name (Unix) or version metadata (Windows).

CMakeLists.txt

project(cppuuid LANGUAGES CXX VERSION 0.1.0)

cmake/setup_installation.cmake

write_basic_package_version_file(
    ${version_filename}
    COMPATIBILITY SameMajorVersion
)

The write_basic_package_version_file() is provided by the CMakePackageConfigHelpers module we included earlier. By default it uses the version number from the currently active project(). You can also provide a version explicitly by adding VERSION <version number> to the call.

With COMPATIBILITY you specify which versions are considered to be backwards compatible. What you choose here depends on the guarantees you want to provide. Does “compatible” mean API or ABI compatibility? How is your version number structured? Because CppUuid uses simplified semantic versioning and is only concerned with API compatibility all versions with the same major version number are backwards compatible.

We don’t need any further customization, so the basic versioning file CMake can create is perfectly sufficient. If your project needs something more complicated have a look at the documentation of the write_basic_package_version_file() function. It gives some pointers about where to start if you want or have to create a versioning file on your own.

The version file is written to the CMake build directory. We’ll install it into its final destination later.

Exporting Custom Functionality

CppUuid is a simple library. The only custom functionality we need is finding the Qt dependency in the exported project.

Just as a reminder, this is the relevant part from the main CMakeLists.txt:

if (ENABLE_QSTRING)
    find_package_and_export(Qt5 REQUIRED COMPONENTS Core)
    target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Core)
endif()

The find_package_and_export() call appends to the cppuuid_FIND_DEPENDENCY_CALLS variable. We use its content when generating cppuuidConfig.cmake.

snippet from cmake/setup_installation.cmake

configure_file("cmake/${config_filename}.in" "${config_filename}" @ONLY)

template file cmake/cppuuidConfig.cmake.in

include(CMakeFindDependencyMacro)
@cppuuid_FIND_DEPENDENCY_CALLS@
include("${CMAKE_CURRENT_LIST_DIR}/@exported_targets_filename@")

The configure_file() takes the template file, replaces all CMake variables between @ characters with the content of the variable of the same name and writes the output file into the CMake build folder. Just like the version file we’ll install it to its final destination later.

The result with QString support enabled looks like this:

include(CMakeFindDependencyMacro)
find_dependency(Qt5 REQUIRED COMPONENTS Core)
include("${CMAKE_CURRENT_LIST_DIR}/cppuuidTargets.cmake")

CMakeFindDependencyMacro must be included first because it provides the find_dependency() function. That function works like find_package() but is designed to be used from an exported project.

The last line pulls in the rest of the exported project – the part that is auto-generated by CMake.

Installing and Exporting Targets

This brings us to the whole bunch of install() calls. The first two of them are concerned with installing the binaries and the Targets files. Always consider them in combination. As long as you want to not only install binaries, but also create and install an exported project, you need them both.

install(
    TARGETS ${PROJECT_NAME}
    EXPORT ${exported_targets_name}
    ARCHIVE DESTINATION "lib"
)
install(
    EXPORT ${exported_targets_name}
    FILE ${exported_targets_filename}
    NAMESPACE cppuuid::
    DESTINATION ${export_dirpath}
)

The first install() does the following:

  • It defines the output directories. They default to the usual bin/include/lib structure. ARCHIVE DESTINATION should be specified explicitly because otherwise MinGW aborts with a complaint about not finding the archive directory.

  • It defines an internal name for the exported project (the EXPORT parameter) and associates it with the target or list of targets defined with TARGETS. However it does not install any files for the exported project.

    Don’t be confused by the ${PROJECT_NAME} variable expansion. For our CppUuid example the library has the same name as the whole project. That’s quite common. But it’s important to remember that in the context of the install(TARGETS) command we’re always talking about targets.

  • It installs the binaries for the given targets.

The second install() call is the one that generates and installs the Targets files for the exported project. Its NAMESPACE parameter defines the first part of the fully qualified names of the exported targets. By convention it is always used and set to something that sensibly represents your whole project or library. For example, Boost uses Boost:: as its namespace. Qt uses Qt5:: for the current version 5.x series. If you have a dependency on Boost’s filesystem library you’d use something like this in you own CMake code:

target_link_libraries(target PUBLIC Boost::filesystem)

Because the double colon (::) is just a naming convention you have to include it in the namespace name.

Installing Additional Files

Some files are still left to install. CMake provides two commands to install files or whole directories: install(FILES) and install(DIRECTORY). For CppUuid the following commands are needed:

install(
    FILES
        "${CMAKE_CURRENT_BINARY_DIR}/${config_filename}"
        "${CMAKE_CURRENT_BINARY_DIR}/${version_filename}"
    DESTINATION
        ${export_dirpath}
)
install(
    DIRECTORY
        "${CMAKE_CURRENT_SOURCE_DIR}/include/cppuuid"
        "${CMAKE_CURRENT_BINARY_DIR}/include/cppuuid"
    DESTINATION
        "include"
)

The above install(FILES) command installs the rest of the exported project that is only present in the build directory until now.

Then install(DIRECTORY) takes care of installing the public header files. You can keep track of the public header files in several ways. In my opinion keeping them in a separate directory is the easiest solution. Because our CppUuid example has a generated header as well we have two directory trees to install and merge into one.

The syntax with DESTINATION "include" shown here is the older way of writing this command. Duplicating the destination name is not great, it’s just the path to the normal installation include directory after all, and technically CMake knows about it already. The CMake folks realized that too. Since CMake 3.14 it is possible to use TYPE INCLUDE instead to reuse the include directory path defined in the install(TARGETS) command earlier.

Conclusion

Wow, this has gotten longer than I expected. And it’s only for a relatively simple example library. Clearly CMake has still a lot of potential to improve when installing ist concerned.

If you have a more complex project you might want to continue reading with the documentation of the install() command, especially the part about components, and the documentation of the configure_package_config_file() command.

Comments