Navigating CMake Dependencies with CPS

March 31, 2025

Another Step Toward Standard C++ Dependency Management

Last fall, CMake took the first step toward a new world of expressing software package information in a new format, the Common Package Specification (CPS), which aims to take the expressiveness of CMake’s native export format (which is manifested in CMake script) and make it easily available to any tool. If you haven’t already, please refer to our previous blog for more background information.

That first step was the ability to export package information in CPS format. While that support remains experimental and incomplete, we believe there is enough substance to enable users to start trying it out… if only the resulting files could be used. Well, with the release of CMake 4.0, we are pleased to announce a major milestone in closing that gap—CMake can now import CPS! While this functionality is still under development (some features are not fully implemented), users can experiment with end-to-end usage, and enjoy an unexpected benefit as well.

Dependency Graphs Aren’t Flat

Anyone experienced in software development knows that a project’s dependency graph isn’t just a list of leaf nodes; spdlog depends on fmt, libarchive depends on libz, liblzma and others, Qt has a fairly large dependency graph, VTK depends on Qt, and so on. Historically, CMake has struggled to model this in an optimal manner, either requiring each subsequent consumer to list the complete dependency set or relying on shared libraries to hide such details (and trusting the immediate dependency to only exist on disk if its transitive dependencies also exist where they are expected to be).

A better option is for libraries to express their dependencies so that they can be recursively resolved. CMake has put some effort into this, but expressing transitive dependencies in the CMake script used for existing exports has some problems, and generating this information automatically (as opposed to requiring the library developer to write it out by hand) remains experimental.

Hope on the Horizon

CPS was developed with this issue in mind. The export support that was added last fall already makes an effort to record these transitive dependencies, and the recently added import support includes machinery for resolving them.

It isn’t perfect. There are idiosyncrasies in version matching (carried over from how existing CMake version-check scripts usually work), automatic dependency export doesn’t record version information or hints, and other corner cases will inevitably rear their ugly heads over time. However, unlike CMake script support for transitive dependencies, which is still awkward (and officially experimental on the export side), CPS has been designed with transitive dependencies in mind from the start, and the goal is to handle those dependencies in a way that Just Works™. Development is ongoing to improve CPS support in this and other respects, and we anticipate continuing progress both in the short- and long-term.

Kicking the Tires

I Can Haz CPS?

Importing CPS is somewhat easier than exporting, as it only requires opting in to support:

set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES
    "e82e467b-f997-4464-8ace-b00808fff261")

After opting in, simply call find_package as usual. Refer to the documentation for details on how CMake looks for CPS packages.

The eventual goal, of course, is for CPS support to be enabled by default.

Consuming the Imported Package

Just like importing a package described in CMake script, importing a CPS package will typically result in the creation of one or more imported targets, which can then be used via the target_link_libraries command.

One important caveat is that CPS codifies the canonical name of a component as <package>:<component>. (CMake translates the single-colon to double-colon on import to match its internal convention, and we recommend other tools use their own existing separators where relevant.) This means that imported targets generated from a CPS will always have the form a::b, with one exception—some packages may provide an INTERFACE target with the same name as the package which references that package’s default_components.

Transitive Dependencies

If a package description specifies transitive dependencies, CMake will attempt to resolve those dependencies before finalizing the import of the requested package. This is done using a special, internal “nested mode” version of find_package and can resolve dependencies via either CPS or CMake-script package descriptions. For example, if Canine depends on Mammal, find_package(Canine) might find canine.cps (like CMake-script package description files, CPS files are allowed to be lower-case of the package’s proper case) and resolve its dependency on Mammal via MammalConfig.cmake.

This comes with two important caveats. First, transitive dependencies supplied by a CMake-script package description must name their targets in conformance with the convention specified by CPS and must include any required optional components according to their local target names. (For a more technical discussion of using CMake-script packages to solve CPS package dependencies, see this documentation.) Second, because CMake does not currently have any mechanism for making transactional changes to its internal state, a missing transitive dependency may result in the partial import of transitive dependencies.

When everything works, however, importing a CPS package will result in all of its dependencies also being imported, with minimal or no additional work on the consumer’s end. At this time, this means that any transitive dependencies will also become available to the consumer. However, in the future, we may implement some form of “target visibility,” so we do not recommend relying on this behavior.

Show Me Some Code

We described export previously, and import primarily works “behind the scenes.” Besides flipping on the experimental switch, you might hardly notice anything “special” happening at all:

set(CMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES
    "e82e467b-f997-4464-8ace-b00808fff261")

find_package(foo 1.2 REQUIRED COMPONENTS extra)

add_executable(bar bar.cpp)
target_link_libraries(bar PRIVATE foo::core foo::extra)

…and that is a good thing! If the difference between CMake-script package description files and CPS package description files is invisible to the consumer, we’re doing something right.

Users desiring a complete, functional, end-to-end example for CPS export and import are encouraged to refer to the cmake/zdemo sample projects at https://github.com/cps-org/cps-examples. These provide an example (zwrap) of exporting a project in CPS format, which has transitive dependencies, and (ztest) of consuming the same.

Where To Next?

Implementing CPS remains an ongoing project. We encourage CMake users to try out the experimental export and import 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 C++ Ecosystem Evolution group has a mailing list and can also be contacted via the #ecosystem_evolution channel in the C++ Slack.

For professional CMake support, including customization and training courses, please contact Kitware.

2 comments to Navigating CMake Dependencies with CPS

  1. Sentence “For a more technical discussion of using CMake-script packages to solve CPS package dependencies, see this documentation” lack a link.

    Otherwise, a good article.

Leave a Reply