A Year Closer to Standard C++ Dependency Management

October 22, 2024

Looking Backward

In the beginning, there was chaos. Lacking any standards or even, in the early days, any conventions, hundreds of C and C++ packages developed their own bespoke approaches for making themselves available to consumers… if they heeded the problem at all.

One idea that gained traction was to ship a script or micro-application which, when queried, would spit out the necessary compile and link flags to build against a given library. This approach was heavily championed by GNOME and eventually resulted in the venerable pkg-config. While more-or-less adequate for autotools, flag soup is semantically lossy, and sub-optimal for describing related but separable consumables.

CMake, wading into this anarchy at about the same time, developed in a different direction, initially creating “find modules”, and later, “exported targets”. The latter in particular is a significant advancement in the state of package information exchange, although it retains some limitations… of which the biggest, by far, is that it’s implemented in CMake script. As a result, only CMake has been able to take advantage of the much richer information provided by exported targets.

In 2017, we began working on a proposal for a Common Package Specification (CPS), with the goal of changing this. CPS incorporates many of the lessons learned from CMake’s long history while also aiming to address some limitations of existing CMake exports. Most importantly, it does this in a purely declarative format that is intended to be easily consumed by any build tool. (Strictly speaking, CPS is agnostic about how it is expressed, but in practice we expect JSON to be the de facto standard “container”.) CPS was mentioned, briefly, in the WG21 paper “Let’s Talk About Package Specification” (P1313), but did not gain much traction at the time.

Recently, there has been renewed interest in improving the situation around dependency management for C-ABI (and especially C++) software. In 2023, Bret Brown and Bill Hoffman gave a talk about CPS and how it would fit in with the evolution of C++ dependency management. Coinciding with this, we unveiled a very preliminary demonstration of importing CPS packages into CMake. However, this functionality was crude, based on an old (no longer supported) version of CPS, and was limited to an experimental branch. Earlier this year (2024), Bill Hoffman gave another talk, this time focusing on build system target models.

In this blog, we will look (briefly) at the progress that has been made with CPS, and more importantly, what’s happening in CMake to bring us closer to making CPS the universal language for package information exchange.

What is CPS?

Common Package Specification is a specification for describing software artifacts that are intended to be consumed by other software. While the primary use-case is for C-ABI libraries, the long term goal of CPS is to be able to describe other, similar artifacts, such as executables (which are particularly of relevance when a package provides code generators), Java Archives, and so forth. Anyone familiar with CMake exported targets will already have a good understanding of how CPS functions, as there is a nearly one-to-one correspondence between CMake exported targets and CPS components. Indeed, the genesis of CPS was an explicit attempt to map existing CMake exported targets into a declarative language that does not rely on CMake script.

In addition to its primary goal of tool-agnosticism, CPS also provides better support for transitive dependencies (an area in which CMake is notably lacking) and improved version management, while continuing to support important CMake features such as multi-component packages, multiple component configurations, and interface libraries. Ultimately, the goal of CPS is both to address some of the shortcomings of describing packages in CMake script, to make the full expressiveness of CMake exported targets available to other build tools, and to simplify providing such descriptions for packages built by other tools that are consumed by CMake projects.

What’s Happening Now?

In the last year or so, we’ve been working on both the specification itself, which has undergone many revisions with the help of various members of the C++ ecosystem, as well as the opposite end of CMake support. We are proud to announce that CMake 3.31 includes support for CPS export. This remains experimental and is accordingly placed behind a CMake “experimental gate”, and still has some known limitations (see below). However, it represents a significant step toward a new, better, and more portable mechanism for describing packages that will make them easier to consume.

Usage

(This section will describe how to “kick the tires”. Feel free to skip to the next section if you aren’t looking to do that yet.)

As with any experimental feature, the first requirement is to opt in to the feature:

set(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO
    b80be207-778e-46ba-8080-b23bba22639e)

Next, you will install a CPS package description. We’ll assume you have already described the targets to be exported using install(TARGETS ...); this step is the same as for exporting targets to CMake’s native format. (Indeed, you can, with some limitations, export in both CMake format and CPS.) To generate and install the package description, use:

install(PACKAGE_INFO ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}-targets
    VERSION ${PROJECT_VERSION})

Note that we assume that you want to use ${PROJECT_NAME} and ${PROJECT_VERSION} as the package name and version, respectively. These can be replaced with whatever values are appropriate. (You can also omit the package version entirely.) We also assume that ${PROJECT_NAME}-targets is the name of your export set; again, substitute as appropriate.

For a basic package description, this is all you need. However, there are additional options that many users will find useful.

COMPAT_VERSION specifies the oldest version of a package that is forwards compatible with the current version. For example, a consumer requesting version 1.1 of a package might be able to use 1.2, but not 2.1; the package might declare its COMPAT_VERSION in the latter case as 2.0. This is conceptually equivalent to the COMPATIBILITY offered by write_basic_package_version_file, except that any version can be specified; compatibility is not restricted to matching a subset of the version number.

DEFAULT_TARGETS makes it easier for users to consume a package. In short, while all exported targets are automatically prefixed with the package name as their imported namespace, if DEFAULT_TARGETS is specified, an additional INTERFACE target which links the specified targets is created with the (unqualified) package name. This allows users of “simple” packages for which the same target(s) will typically be consumed to use those packages without having to know the target names.

APPENDIX allows a package description to be split into multiple files. This is particularly useful for projects that will be described as a single logical package, but distributed as multiple physical packages (for example, to allow users to only install those components they require). Use of appendices requires a “master” package description that contains common metadata (e.g. package version), which in turn may not be specified in an appendix. Something similar can be done with CMake-script exports, but requires manually writing the logic to find the “appendices”; with CPS this is built in.

Note also that there is no NAMESPACE option. CPS codifies the use of the package name to qualify external targets. This is one reason why we strongly recommend using the project name as the target namespace when exporting targets in CMake format.

Several options common to most install subcommands are also supported. Refer to the install command documentation for more information.

Finally, something else you may notice “missing” is any way to describe transitive dependencies. While we may yet need to add some mechanism for this to deal with corner cases, the goal is for CPS to handle these automatically. Generally, if what you’re exporting depends on something that was imported (or something in a different export set), CMake will handle expressing this in CPS without need for additional intervention.

Limitations

At this time, CPS export is only supported for installed packages. We have already received requests to support consuming packages from their build trees (as can be done using CMake’s native package information format via the export command) and hope to implement this in the future.

There is an additional issue which prevents exporting package information in both CMake’s native format and CPS if another export references the exported targets. Although we expect the number of users affected by this to be low, it is also on our roadmap to address. (This only affects users that export multiple packages from the same CMake build.)

Finally, C++ module support is not yet implemented in CPS. This is partly a function of the same issue which led to the creation of CPS; there does not yet exist a tool-agnostic mechanism for providing the necessary information. Again, this is an area of active work.

Parting Thoughts

Implementing CPS is an ongoing project. We encourage CMake users to try out the experimental export support, and we encourage users of all build systems to share any suggestions or concerns. Matters pertaining to the specification itself may be posted as issues or discussions to the CPS repository, while matters pertaining to CMake’s implementation of CPS should be posted to the CMake repository. For more general matters, or to request feedback before filing an issue, the somewhat-unofficial C++ Ecosystem Evolution group has a mailing list and can also be contacted via the #ecosystem_evolution channel in the C++ Slack.

Leave a Reply