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.
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 fromGreeterApp
. - Variable Scopes Change: When this CMakeLists.txt is processed, variables like
PROJECT_NAME
andPROJECT_SOURCE_DIR
will now refer toGreeterLib
and thegreeter/
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" namedGreeterLibTargets
. 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 theCMAKE_INSTALL_LIBDIR
recommended byGNUInstallDirs
(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 theIMPORTED
target definition. If we omit this, CMake will call the file[ExportName].cmake
by default. This matches theGreeterLibTargets.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 generatedGreeterLibTargets.cmake
file to be placed within our install tree. By convention, the CMake files go within the/lib/cmake/[PackageName]
directory.NAMESPACE Greeter::
: This prependsGreeter::
to the target name within the generated file. In an we discussed the technique and rationale for using a namespacedALIAS
for our library. Those techniques apply to the build tree, and thisNAMESPACE
keyword is how we achieve the same effect within the install tree. Our library will be calledGreeter::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), itsGreeterLibConfig.cmake
file would need to callfind_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 ourproject()
command, which generates aPROJECT_VERSION
variable that we can reuse here.COMPATIBILITY AnyNewerVersion
: This tellsfind_package()
that this version of the package is fully backwards compatible. For example, if the consumer requestedfind_library(GreeterLib VERSION 1.2)
and version2.0
withCOMPATIBILITY 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 ...)
andinstall(EXPORT ...)
to both copy your library's files and generate theIMPORTED
target definitions. - Package Configuration Files: The
CMakePackageConfigHelpers
module provides the boilerplate for creating theConfig.cmake
andConfigVersion.cmake
files thatfind_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.
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