CMake’s Package Manager
Note
CMake is constantly adding new features. In recent releases, many of the new features are targeted at topics considered on this page. The information on this page was accurate circa CMake 3.11 (which was released in 2018 at about the time we started floating ideas about what would become CMaize).
Build systems and package managers often work together to build a project. For better or for worse, many build systems relying on traditional CMake attempt to be both the build system and the package manager. Making matters worse, most build systems assume that CMake is driving the build, not a package manager. What this means is that if a build system designer wants to enable package manager support, while still having their package be build-able by CMake, the package manager must be integrated into CMake.
CMaize was designed from the onset to support package managers under a CMake-based API. Key to this effort is establishing a package manager component (see Designing CMaize’s PackageManager Component). By default the package manager component uses what we call CMake’s built-in package manager. Admittedly, CMake is not usually discussed in this manner and the purpose of this page is to explain how CMaize maps CMake’s existing functions to a package manager.
Package Manager Responsibilities
Before discussing how CMake is mapped to a package manager, we briefly review the responsibilities of a package manager.
The primary responsibility of a package manager is, as the name states, to
maintain a set of packages. When a user wants to use a
package they need only request it from the the package manager and the package
manager does the rest. The package manager’s job is very easy if the user’s
request is very simple (e.g., “any version of Python”), and the package is
already available. The usefulness of a package manager can be measured by how
well it is able to handle complicated requests (e.g., Python version 3.12.0,
compiled with GCC version 9.5, -O2
, …) and obtain packages which are not
already installed. Ideally this process will be as efficient as possible, e.g.,
the package manager should avoid re-compiling already existing package
dependencies, if they satisfy the package specification.
In satisfying their primary responsibility, package managers must be able to:
- Understand package specifications.
Notably this includes knowing what other packages the package depends on.
- Inspect managed packages.
This can include not only the package’s specification, but also the integrity of the package (does it work?) and its authenticity (is it really what it says it is?).
- Compare and discern among managed packages.
In particular the package manager must be able to not only realize when two packages are entirely different (e.g., one is say a C++ compiler and the other is a Python interpreter), but also be able to discern among different specifications of the same package.
Comparisons are usually needed to know if a package could be used to satisfy a particular package specification.
- Obtain new packages.
New packages can come from manual addition (a non-ideal solution), downloading, or even other package managers.
- Facilitate use of managed packages.
If a package manager possesses a package, but the user can not use the package, the package is of no use. The package manager must provide mechanisms so that the package can actually be used.
This is particularly relevant for packages which were added to the package manager as dependencies of other packages. Often these packages are not located in places which are readily accessible to the user.
Many package managers also have a number of other features including removing and updating packages. These features are not part of the above list because, from the perspective of a build system, these features are not strictly necessary.
CMake as a Package Manager
Note
This section describes CMake pre version 3.24. Version 3.24 introduces dependency providers which invalidates some of this discussion.
The extent of CMake’s package management functions largely boils down to
find_package
and the FetchContent
module (for modern CMake; older CMake
build systems often relied on the ExternalProject
module instead of
FetchContent
). CMake’s native ability to understand package specifications
is largely limited to the package’s name, the version number, and a list of
components. For everything else, CMake defers to the package developer.
Inspecting packages happens under find_package
via one of two mechanisms.
The best practices mechanism is for packages to provide a config file (allowed
names are PackageNameConfig.cmake
or package_name-config.cmake
) and a
version file (PackageNameConfigVersion.cmake
or
package_name-config-version.cmake
). Alternatively, a find module may be
provided (FindXXX.cmake
where XXX
is the name passed to
find_package
). Regardless of which mechanism is used, it is the .cmake
files’ responsibility to make sure CMake has the package specification
information it needs (the version and available components; the package name is
used to locate the files). CMake considers the packages a match if the version
information and components provided by the files match what the user asked for.
To enforce checks on other parts of the package specification, the config file
author can ensure that XXX_FOUND
(XXX
again being the name provided to
find_package
) is set to false if the package associated with the files does
not satisfy the additional specifications. It is also the responsibility of
these files to provide the caller of find_package
with a target representing
the dependency.
Obtaining new packages is done via the functions in CMake’s FetchContent
module. However, the FetchContent
module really only targets packages which
can be used upon downloading, or packages which can be configured and built
with CMake. Since FetchContent
takes the union of all packages’ build
systems, it is worth noting that not all packages which use CMake are
FetchContent
compatible; in particular, packages which define targets with
the same name, overwrite global variables, or do not strictly follow the usual
CMake build process are NOT compatible with FetchContent
.
Summarily, with respect to the list in Package Manager Responsibilities:
- Understand package specifications.
CMake natively understands versions and components. Package maintainers need to register their package’s version and components with CMake in order to use CMake’s native support. Any additional package specification content must be manually checked.
- Inspect managed packages.
Inspecting packages is done under the hood of the
find_package
function. More specificallyfind_package
loops over a set of paths and looks for config files. If a config file matching the project’s name is found it then reads in the package specification.- Compare and discern among managed packages.
Once CMake has read in a config file it will compare the package specifications (version and components) to those the user requests.
- Obtain new packages.
CMake relies on the
FetchContent
module for obtaining new packages.- Facilitate use of managed packages.
It is the responsibility of the package maintainer to ensure the config file exports a target the user can use.