Modern CMake Packaging: A Guide

Or, A Candle in the Dark

Pretty sure I just failed my Calc III final.
I hate calculus, but I love CMake. Let's talk about CMake.

There is a five year old CMake bug describing the need for a “cookbook” to walk users through effective packaging of CMake projects. As with so many corners of CMake usage, the technical documentation extensively describes how everything works, but gives no hints to which components of the extensive CMake ecosystem should be used. Inevitably, projects end up cobbling together code copied from other sources and gently massaged into a “works for me” state.

Due to other immense obstacles in the C/C++ package ecosystem, this has not been seen as the most pressing issue. However, with the rise of package managers like conan and especially vcpkg (which makes CMake central to its architecture), ensuring that C/C++ libraries have functional packaging routines has grown in importance.

Absent an official cookbook, widespread community consensus, a qualified expert, or well-liked town fool to provide guidance; I provide my dim, flickering advice for how to best take advantage of the CMake packaging facilities.

Package? Never heard of her.

Part of the problem is the term “packaging” doesn’t have a formal definition in the C/C++ ecosystem in the same way that it does for other languages. The quick definition is this: packaging is the process of generating and populating a directory structure that contains everything a third-party project will need to use your code. We’ll also need some other definitions:

Packaging Starts at Home

Before we get to packaging specific commands we need to briefly talk about how CMake targets interact with one another. Consider the library example from Figure 1.

Here we have a project called NavidsonRecord. We pause just long enough to note that the project directive is used to annotate the current version and that this will be important later.

The project consists of a single shared library named house which is itself composed of a single compilation unit named hallway.cpp, and some number of headers stored in the source tree under the include folder. One of these, labyrinth.hpp, is listed as a source file. While any files inside the BASE_DIRS will be visible for inclusion during the build process, only files listed under the FILES parameter will be installed.

Now let’s address the scope keywords: PUBLIC, PRIVATE, and INTERFACE.

When using directives from the target_ family, such as target_sources, we use a scope keyword to describe how the associated resources should be used. PRIVATE resources will be used only by the associated target, INTERFACE resources will be used only by dependents of the associated target, and PUBLIC resources will be used by both.

Keep the Goal in Mind

Let’s talk about what files we need to produce:1

These files, along with all files associated with the package’s targets, need to be placed inside the install tree. At that point our job is done. What eventually happens to the install tree depends on how the end-user is consuming our package. It’s entirely possible the “install tree” is the user’s program or /usr directory.

The 5½ Minute CMake

Best practice for packaging involves two helper scripts, CMakePackageConfigHelpers and GNUInstallDirs.

GNUInstallDirs provides the CMAKE_INSTALL_ family of variables, which provide standard path information for the install tree. CMakePackageConfigHelpers provides the write_basic_package_version_file macro, which we’ll use to generate the package-config-version.cmake file.

In Figure 2 we use these macros and variables to generate and install the version file for the NavidsonRecord project.

Quick notes:

Next let’s address the pkg-config file, for this we need to write an input template such as Figure 3.

In Figure 4 we fill in the @ variables using the configure_file directive, then install the resulting generated file in the same way as the version file.

Heading #5

We’re now ready for the meat, the install(TARGETS) and install(EXPORT) directives. These, like pkg-config, are complex commands with many different knobs and buttons. While we’ll explore a reasonable usage, the important take away is that you should be using install(EXPORT) instead of older styles of installing and exporting targets.

Enough foreplay, Figure 5 is the code we need to install our house target.

Alright, brace yourself for some overloaded terms. The first install(TARGETS) directive takes a list of one or more targets and associates them with what CMake calls an export. In Figure 5 the export is named navidsonTargets, which is a typical naming scheme.

An export is a different type than CMake variables or targets, but it works on a similar principle. In the same way files get associated with targets, the install(TARGETS) directive associates targets with an export. And just like how we can call target_sources repeatedly to add files to a given target, we can call install(TARGETS) repeatedly to add targets to a given export.

The install(EXPORT) directive generates a cmake file for a given export directly into the install tree with the name “[exportName].cmake”, so in this case “navidsonTargets.cmake”. This file does all the necessary legwork to setup our targets in the parent project. In the parent project our targets will have their names prefixed by the NAMESPACE, so house will be known as nvr::house.4

The End

“But wait!” you cry, “You said we needed three files, where is navidson-config.cmake?”

Good, you’re paying attention. You’ll find it in Figure 6, try not to be disappointed.

If our library had any dependencies we would add the code to find them here (as well as in our CMakeLists.txt file). In this example we don’t, so navidson-config.cmake is a single line of code, which includes the generated export file. The only thing left to do is make sure it gets installed alongside the version file and we’re done.

Effectively zero projects I’ve look at do this in the “modern” way so I’d like to briefly address silly things you shouldn’t be doing in the config file:

There are very few reasons for your project config file to consist of anything other than find_package() and include() directives. While there are some reasons to split out targets into separate exports (for example, if you have optional dependencies that enable/disable certain targets), most projects will get away with a set of zero or more calls to find_package() followed by a single include() of their export file.

Afterword

And that’s it. Example repo is available here. Example of using that example repo is here, to prove I’m not a crank. I’m going to go be sad about Calculus III. If I fail it next semester too I’ll write more about CMake.


  1. package is a placeholder name in these examples ↩︎

  2. The naming convention PackageConfigVersion.cmake is also supported, but I like lower case letters ↩︎

  3. Ditto ↩︎

  4. Convention is to use the package name as the namespace, so a good citizen would use navidson::house. See this discussion on Reddit ↩︎