Creating a Consumable Package

Learn how to make your libraries consumable by other projects using CMake by turning your build-tree into a distributable install-tree.

Greg Filak
Published

In the last lesson, we learned how to use find_package() to locate and use external libraries that are already installed on our system. This is the main way we consume packages created by other people and shared with us.

But how do those packages get created in the first place? How do libraries like Boost provide the Config.cmake files and imported targets that make find_package() work so smoothly?

This lesson flips the script. How do we produce a package that people can use in the same way? We're going to take our own GreeterLib and turn it into a distributable CMake package that we can share for any other project to consume.

From Build-Tree to Install-Tree

Currently, our GreeterApp uses GreeterLib via the add_subdirectory() command. This creates what's known as a build-tree dependency. Both components are part of the same build process; their source code is visible to each other, and they are compiled together.

This is great for tightly-coupled modules within a single project. But what if we want to share GreeterLib with a completely separate project that doesn't have its source code?

To do this, we need to create an install-tree. An install tree is simply a directory that is created on the file system that holds everything a consumer needs to use our library:

  • The compiled library files (.a, .lib, .so, .dll).
  • The public header files (Greeter.h).
  • The CMake configuration files that find_package() looks for.

It's called a "tree" because it is not just a flat directory of files - it is structured to make our library easier to use. For example, we might place the compiled libraries in a lib subdirectory, whilst the header files are in a subdirectory called headers.

In this lesson, we will first make GreeterLib installable, meaning we'll configure what this install tree should look like, and set up the commands that enable CMake to generate it automatically.

Then, we will modify GreeterApp to find and consume this newly installed version, as if it were a standalone third-party library.

The GNUInstallDirs Module

CMake lets us structure our install tree however we want, but if should follow some standard practices. Doing so makes the structure of our directory immediately recognizable to other developers, and helps CMake (and other tools) find the components they need.

To help with this, CMake comes bundled with a GNUInstallDirs module, which we can load into our CMakeLists.txt file in the usual way:

include(GNUInstallDirs)

This module defines some variables that store recommended locations for different types of components, which we'll use throughout the lesson. For example:

include(GNUInstallDirs)

message("Headers go in ${CMAKE_INSTALL_INCLUDEDIR}")
message("Libraries go in ${CMAKE_INSTALL_LIBDIR}")
message("Binaries go in ${CMAKE_INSTALL_BINDIR}")
Headers go in include
Libraries go in lib
Binaries go in bin

All of our destination directories will be relative paths. The person installing our library gets to decide the overall location on their system. The paths we define control the structure of our install tree at that location.

For example, the user gets to decide the package should be located at C:\Libraries\GreeterLib on their system, but we get to decide that the header files should be located at \include within that directory.

Making GreeterLib Installable and Exportable

We'll start by focusing on greeter/CMakeLists.txt. We need to add a series of install() commands to tell CMake how to create a proper package.

There is a lot of complexity in this section, and a lot of new commands we'll be adding to our CMakeLists.txt. However, it's almost entirely boilerplate. These steps, and the final CMakeLists.txt file would look broadly similar for any project, so this will become a familiar and repeatable pattern.

Step 1: Declaring a project()

So far, GreeterLib has been a component within our larger Greeter project. To prepare it for distribution as a standalone package, we should make it more independent. The first step is to give it its own project() definition.

Adding a project() command to greeter/CMakeLists.txt formally declares it as a self-contained sub-project. This has several consequences:

  • It gets its own identity: Most importantly, it can have its own version number. By giving GreeterLib its own project, we can version it independently of the main application.
  • It becomes self-contained: This change makes it possible to build GreeterLib on its own, completely separate from GreeterApp.
  • Variable Scopes Change: When this CMakeLists.txt is processed, variables like PROJECT_NAME and PROJECT_SOURCE_DIR will now refer to GreeterLib and the greeter/ directory, respectively, instead of the top-level project.

Let's add our command:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(GreeterLib VERSION 1.0.0)

add_library(GreeterLib src/Greeter.cpp)

target_include_directories(GreeterLib PUBLIC
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

Step 2: Updating Include Directories

Often, our configuration will need to change slightly depending on whether our library is loaded using as a build tree dependency using add_subdirectory(), or as an install tree using find_package().

We handle this in the same way we handled different build types: by using generator expressions.

We cover generator expressions in full detail later in the course but, for now, the expressions we need are BUILD_INTERFACE and INSTALL_INTERFACE.

For example, our library's include directives are currently specified as locations within the build tree, using the CMAKE_CURRENT_SOURCE_DIR variable:

target_include_directories(GreeterLib PUBLIC
  # This location is in the build tree
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

This won't work when our library is loaded as an external dependency. Let's update our command to handle both scenarios:

CMakeLists.txt

# ...

target_include_directories(GreeterLib PUBLIC
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

The CMAKE_INSTALL_INCLUDEDIR variable we're using here is from the GNUInstallDirs module we included. It stores the recommended location for header files within a library we package and share.

We'll use this variable again in a later step to ensure our header files are indeed copied to this location when we run our installation.

Given we've now declared that our GreeterLib is a full project(), we can also replace the CMAKE_CURRENT_SOURCE_DIR variable with PROJECT_SOURCE_DIR. They will now have the same value, most people prefer using the project variation:

greeter/CMakeLists.txt

# ...

target_include_directories(GreeterLib PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Step 3: Specifying Output Destinations

Next, we'll tell CMake which files to copy into the install-tree and where they should go. We do this with install(TARGETS ...):

greeter/CMakeLists.txt

# ...

install(
  TARGETS GreeterLib
  EXPORT GreeterLibTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

Let's break down this command:

  • install(TARGETS GreeterLib ...): We're creating an install rule for targets. We could add more targets here if needed, but our hypothetical package only contains one library - GreeterLib.
  • EXPORT GreeterLibTargets: This tells CMake to not just copy files, but to record the properties of the targets we specified , such as their include directories and dependencies. This information is saved into an "export-set" named GreeterLibTargets. We'll use this set in the next step.
  • ARCHIVE DESTINATION: This tells CMake where to place static libraries that our targets output. We'll store them in the CMAKE_INSTALL_LIBDIR recommended by GNUInstallDirs (which will be /lib).
  • LIBRARY DESTINATION: This tells CMake where to place shared libraries (.so and .dylib files) for Linux and macOS. By convention, these go in the same location as the static libraries.
  • RUNTIME DESTINATION: This tells CMake where to place shared libraries (.dll files) on Windows. It would also control where executables go, if our targets output any. Again, we'll go with the GNU recommended location, which will be /bin.

Step 4: Specifying Header Destinations

Our public header files are not output by our targets - they are just files in our build tree that we also need to copy across to our install tree.

One way to do this is using an install(DIRECTORY ...) command:

greeter/CMakeLists.txt

# ...

install(DIRECTORY "${PROJECT_SOURCE_DIR}/include"
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
  FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)

We specify the current DIRECTORY within our build tree, and the desired DESTINATION within our future install tree. Again, we'll use GNU's recommendation (which will be /include).

By default, this would copy everything in the directory, but we can optionally apply a filter to the files we select using the FILES_MATCHING argument followed by one or more PATTERN arguments. This is generally recommended as the source directory will often have files we don't need to copy, such as .DS_Store on macOS.

Step 5: Exporting the Target Information

In the previous step, the EXPORT GreeterLibTargets argument told CMake to create an "export-set". This is an in-memory collection of the targets and their properties. In our case, it's just one target - GreeterLib.

Now, we need to write this information to a file on disk. This file will contain the CMake commands needed to recreate our library as an IMPORTED target in any project that requests it using find_package(). We do this with the install(EXPORT ...) command.

greeter/CMakeLists.txt

# ...

install(EXPORT GreeterLibTargets
  FILE GreeterLibTargets.cmake
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib
  NAMESPACE Greeter::
)

Let's look at the arguments:

  • EXPORT GreeterLibTargets: We're telling CMake to install the export-set we defined earlier.
  • FILE GreeterLibTargets.cmake: This is the name of the file that will be generated. It will contain the IMPORTED target definition. If we omit this, CMake will call the file [ExportName].cmake by default. This matches the GreeterLibTargets.cmake argument we're specifying here so these arguments technically make no difference, but they're commonly included anyway.
  • DESTINATION: This sets where we want the generated GreeterLibTargets.cmake file to be placed within our install tree. By convention, the CMake files go within the /lib/cmake/[PackageName] directory.
  • NAMESPACE Greeter::: This prepends Greeter:: to the target name within the generated file. In an we discussed the technique and rationale for using a namespaced ALIAS for our library. Those techniques apply to the build tree, and this NAMESPACE keyword is how we achieve the same effect within the install tree. Our library will be called Greeter::GreeterLib with this configuration, but we'll demonstrate how to rename it later in this section.

At this point, we have a GreeterLibTargets.cmake file that defines our IMPORTED target, but find_package() doesn't look for a <PackageName>Targets.cmake file. It looks for a <PackageName>Config.cmake file. We'll create that next, and explain why there are two different files.

Step 6: Creating the Main Package Configuration File

The GreeterLibTargets.cmake describes the targets that our library contains and their properties, but packages often need some additional logic to help consumers. This is what the GreeterLibConfig.cmake file is for, and it is what CMake will load when a consumer uses find_package().

This file will include our target definitions, but it is also where we would do things like:

  • Check that our package is compatible with the consumer's environment
  • Provide some variables or functions that might be helpful to consumers
  • Find its own dependencies - if GreeterLib itself depended on another library (like Boost), its GreeterLibConfig.cmake file would need to call find_package(Boost) to ensure that dependency is also available to the consumer

In our case, we don't need any of that, so our GreeterLibConfig.cmake can be very simple - it just needs provide the target definitions. It can do that via the include() command, providing the path to the GreeterLibTargets.cmake file we just generated.

The standard way to create a <PackageName>Config.cmake file is using a template, which CMake will process to generate the final version. By convention, these templates are stored in a cmake subdirectory and have a .in extension.

greeter/cmake/GreeterLibConfig.cmake.in

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/GreeterLibTargets.cmake")
  • @PACKAGE_INIT@: This is a special placeholder that will be replaced by boilerplate code generated by a helper module we'll use to load this file in the next step.
  • include(...): This line simply includes our targets file.

The CMAKE_CURRENT_LIST_DIR variable holds the location of the file that CMake is currently processing. During a find_package() command, CMake will load and process the GreeterLibConfig.cmake file, which we've just written the template for. Within this file, CMAKE_CURRENT_LIST_DIR is its location.

Eventually, our GreeterLibConfig.cmake and our GreeterLibTargets.cmake file are going to be in the same location within our install tree. Therefore, this include() expression using CMAKE_CURRENT_LIST_DIR will correctly point to our GreeterLibTargets.cmake file.

Now, let's update greeter/CMakeLists.txt to process this template. We'll use the CMakePackageConfigHelpers module, which provides the necessary functions:

greeter/CMakeLists.txt

# ...

# Use a standard helper module for packaging
include(CMakePackageConfigHelpers) 

# Generate the main Config file from our template
configure_package_config_file(
  "cmake/GreeterLibConfig.cmake.in"
  "GreeterLibConfig.cmake"
  INSTALL_DESTINATION
  "${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
)

The configure_package_config_file() command reads our .in template, generates the final GreeterLibConfig.cmake file, and we provide the INSTALL_DESTINATION to specify where it should go within our install tree. We place it in the same directory as the GreeterLibTargets.cmake file we generated in the previous step.

Step 7: Creating a Package Version File

In most real world use cases, our libraries will be updated over time, so we should keep track of our versions. We'd like consumers to have the option request a specific version with find_package(GreeterLib 1.2.3 ...).

The CMakePackageConfigHelpers module makes this easy with the write_basic_package_version_file() command. This command generates a GreeterLibConfigVersion.cmake file that find_package() can use to check for compatibility.

greeter/CMakeLists.txt

# ...

# Generate a version file for version checking
write_basic_package_version_file( 
  "GreeterLibConfigVersion.cmake" 
  VERSION ${PROJECT_VERSION} 
  COMPATIBILITY AnyNewerVersion 
)
  • "GreeterLibConfigVersion.cmake": The name of the output file. This convention is to use <PackageName>ConfigVersion.cmake.
  • VERSION ${PROJECT_VERSION}: The current version of the library. We already declared the version in our project() command, which generates a PROJECT_VERSION variable that we can reuse here.
  • COMPATIBILITY AnyNewerVersion: This tells find_package() that this version of the package is fully backwards compatible. For example, if the consumer requested find_library(GreeterLib VERSION 1.2) and version 2.0 with COMPATIBILITY AnyNewerVersion was found, it can satisfy the request.

The other common option for backwards compatibility is SameMajorVersion - eg, 2.5 can satisfy a request for 2.3, but not for 1.1.

If we our library has no backwards compatibility at all, we can set this to ExactVersion, meaning the installed version and requested version must exactly match.

Step 8: Installing the Generated Package Files

Our final task is to install the two files we just generated - GreeterLibConfig.cmake and GreeterLibConfigVersion.cmake. They were created in our build directory, but they need to be copied into the final install-tree.

We use a simple install(FILES ...) command for this:

greeter/CMakeLists.txt

# ...

# Install the generated config and version files
install(FILES 
  "${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfig.cmake" 
  "${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfigVersion.cmake" 
  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib" 
)

This command takes the generated files from the current build directory (CMAKE_CURRENT_BINARY_DIR) and copies them to the same destination as our targets file, completing our package.

Step 9: Renaming Things

If we built and installed our package with the current configuration, the library it outputs would be called like libGreeterLib.a or GreeterLib.lib, and the exported target name would be Greeter::GreeterLib.

We can modify these by setting the OUTPUT_NAME and EXPORT_NAME properties of our target:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(GreeterLib VERSION 1.0.0 LANGUAGES CXX)

add_library(GreeterLib src/Greeter.cpp)

set_target_properties(GreeterLib PROPERTIES
  OUTPUT_NAME Greeter
  EXPORT_NAME Lib
)

# ...

This would give them the more sensible names libGreeter.a and Greeter::Lib. As a reminder, this Greeter:: prefix on the target's name comes from the NAMESPACE argument we provided to install(EXPORT ...).

We should allow our library to be referenced in the same way whether it's loaded by find_package() or add_subdirectory(). We can support the Greeter::Lib name in our build tree using the ALIAS technique we covered in an earlier chapter:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(GreeterLib VERSION 1.0.0 LANGUAGES CXX)

add_library(GreeterLib src/Greeter.cpp)
add_library(Greeter::Lib ALIAS GreeterLib)

# ...

Our GreeterLib is now a fully exportable CMake package. It's ready for the installation step, which we'll cover in the next section. Our complete greeter/CMakeLists.txt is included below:

greeter/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

project(GreeterLib VERSION 1.0.0)

add_library(GreeterLib src/Greeter.cpp)
add_library(Greeter::Lib ALIAS GreeterLib)

set_target_properties(GreeterLib PROPERTIES 
  OUTPUT_NAME Greeter
  EXPORT_NAME Lib
)

target_include_directories(GreeterLib PUBLIC
  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

include(GNUInstallDirs)

target_include_directories(GreeterLib PUBLIC
  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

install(
  TARGETS GreeterLib
  EXPORT GreeterLibTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(DIRECTORY include/
  DESTINATION include
  FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)

install(EXPORT GreeterLibTargets
  FILE GreeterLibTargets.cmake
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib
  NAMESPACE Greeter::
)

include(CMakePackageConfigHelpers)

configure_package_config_file(
  "cmake/GreeterLibConfig.cmake.in"
  "GreeterLibConfig.cmake"
  INSTALL_DESTINATION
  "${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
)

write_basic_package_version_file(
  "GreeterLibConfigVersion.cmake"
  VERSION ${PROJECT_VERSION}
  COMPATIBILITY AnyNewerVersion
)

install(FILES
  "${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfig.cmake"
  "${CMAKE_CURRENT_BINARY_DIR}/GreeterLibConfigVersion.cmake"
  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GreeterLib"
)

The Installation Step

Now that we've defined how to install our library, let's actually do it. From our build directory, we first configure and build the project as usual:

cmake ..
cmake --build .

Finally, we use the cmake --install command. We'll use the --prefix argument to specify where we want our files to be created. A local /install directory in our project root is a good choice for testing. Given we're running the command in our /build directory, we first navigate up to the project root using ../:

cmake --install . --prefix "../install"

If you now inspect the /install directory, you'll see a clean, distributable package, as well as the generated .cmake files that make our library easy to integrate with CMake projects:

install/
├─ include/
│ └─ greeter/
│   └─ Greeter.h
└─ lib/
  ├─ libGreeter.a  (or Greeter.lib)
  └─ cmake/
    └─ GreeterLib/
      ├─ GreeterLibConfig.cmake
      ├─ GreeterLibConfigVersion.cmake
      ├─ GreeterLibTargets-noconfig.cmake
      └─ GreeterLibTargets.cmake

This is the "install-tree." It's everything another developer needs to use our library. We can package everything up and share it. They can add it to their project using a find_package() command in much the same way we used Boost in the previous lesson.

We'll walk through how we'd do that for our new package in the next lesson

Setting CMAKE_INSTALL_PREFIX

When developing and testing our library, we'll typically always want to install it in the same location. We can specify that location by setting a CMAKE_INSTALL_PREFIX cache variable:

cmake .. -DCMAKE_INSTALL_PREFIX="../install"

Now, we no longer need to constantly provide a --prefix argument when running the install step. It will always use the cached value, until we clear our cache:

cmake --install .

Summary

In this lesson, we've learned how to transform a simple library into a distributable, professional-grade CMake package. This process is the key to sharing your code with others in a way that is easy for them to consume.

  • Install and Export: Use a combination of install(TARGETS ... EXPORT ...) and install(EXPORT ...) to both copy your library's files and generate the IMPORTED target definitions.
  • Package Configuration Files: The CMakePackageConfigHelpers module provides the boilerplate for creating the Config.cmake and ConfigVersion.cmake files that find_package() needs.
  • The Install Step: cmake --install . --prefix <dir> executes your installation rules, creating a clean install-tree that separates your distributable files from the temporary build directory.
  • Generator Expressions: We used <BUILD_INTERFACE:...> and <INSTALL_INTERFACE:...> to provide different usage requirements depending on whether our library is being used from the build-tree or the install-tree.
Next Lesson
Lesson 30 of 33

Using Installed Packages

Learn how to consume a CMake package you've installed, completing the producer-consumer cycle. We'll cover using find_package() and how to point CMake to your library's location

Have a question about this lesson?
Answers are generated by AI models and may not be accurate