a five year old
describing the need for a “cookbook” to walk users through
effective packaging of
CMake projects. As with so many corners of
usage, the technical documentation extensively describes how everything works,
but gives no hints to which components of the extensive
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:
Source Tree: File system location of the project source code
Build Tree: File system location where the actual building and linking of code happens
Install Tree: File system location where the aforementioned packaging happens. Typically files will be moved out of the build tree into the install tree
Package: Collective name for the contents of the install tree. This includes artifacts from the build process as well as version information and other metadata files
Target: A named, distinct collection of build artifacts, headers, or other products of the codebase placed into the install tree
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
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:
When using directives from the
target_ family, such as
use a scope keyword to describe how the associated resources should be used.
PRIVATE resources will be used only by the associated target,
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
package-config-version.cmake:2 The version file allows
CMaketo discover version information without fully invoking or loading your package
package-config.cmake:3 The configuration file is responsible for loading dependencies and making targets available to the current build session
package.pc: The package config file is the format understood by the venerable
pkg-configprogram and serves as a pidgin used by build tools to talk to one another about dependencies. Package config is a bad format, but it’s the least common denominator. If a consumer of your library doesn’t use
CMakethey’ll need this file.
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
The 5½ Minute CMake
Best practice for packaging involves two helper scripts,
GNUInstallDirs provides the
CMAKE_INSTALL_ family of variables, which
provide standard path information for the install tree.
CMakePackageConfigHelpers provides the
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
Whatever name is used for the folder and file names, in this example “navidson”, is the package name. This is the name that will be used with
find_package()by dependencies to load your project. The project name is irrelevant here
The project version we noted above will be used for the purpose of determining compatibility. It can be overridden using the
Header-only libraries and other projects that don’t involve compilation should pass the
ARCH_INDEPENDENTparameter to avoid checking architecture compatibility
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
directive, then install the resulting generated file in the same way as the
We’re now ready for the meat, the
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
Alright, brace yourself for some overloaded terms. The first
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
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
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
house will be known as
“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:
Ad hoc include guards: First off,
CMakealready has an
include_guarddirective. Second off, they’re unnecessary, the export targets already protect against double inclusion in a far more comprehensive fashion than you will come up with.
@PACKAGE_INIT@: This is a substitution that comes from
CMakePackageConfigHelpersand as far as I can tell it’s obsolete in an export/
find_packagebased workflow. It doesn’t do anything except define macros you shouldn’t be using anyway.
set_and_check(): Macro provided by the above substitution. The generated export targets already do everything you would have previously used this macro for.
check_required_components(): Also from the above substitution. Superceded by
@PACKAGE_NAME@ and other such silliness: How often are you changing the name of your entire package? How hard is find+replace? I never say this, but YAGNI.
There are very few reasons for your project config file to consist of anything
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
include() of their export file.
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