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:
-
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
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
-
package-config-version.cmake:2 The version file allows
CMake
to 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-config
program 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 useCMake
they’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
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:
-
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
VERSION
parameter -
Header-only libraries and other projects that don’t involve compilation should pass the
ARCH_INDEPENDENT
parameter 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 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:
-
Ad hoc include guards: First off,
CMake
already has aninclude_guard
directive. 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
CMakePackageConfigHelpers
and as far as I can tell it’s obsolete in an export/find_package
based 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
find_package(REQUIRED)
. -
@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
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
.
-
package is a placeholder name in these examples ↩︎
-
The naming convention PackageConfigVersion.cmake is also supported, but I like lower case letters ↩︎
-
Ditto ↩︎
-
Convention is to use the package name as the namespace, so a good citizen would use
navidson::house
. See this discussion on Reddit ↩︎