Get dependencies right with CMake

TL;DR

Forget find_package(), Use FetchContent_Declare with set(FETCHCONTENT_TRY_FIND_PACKAGE_MODE ALWAYS) instead.

Dependencies in C and C++

Dealing with dependencies in C++ is seen as a tedious task and here I’m going to share two simple rules to set you on the right track with CMake. This topic costs software publisher hundreds of thousands of dollars each release - you call them DevOps engineering - but I don’t think this is how is meant to be.

Regardless of your opinion about how easy or hard is to deal with dependencies, there are two types of dependencies in C/C++: pre-built dependencies and source dependencies. I have seen small and large organizations making no distinction between these two categories and that’s a mistake. Even worse is when you see them embracing package managers like Conan to fix their circular dependencies - Conan is great, but it is not the answer to spaghetti build systems. Let’s dive into these types from a CMake perspective.

Dependencies in CMake

Again, there are two main types of dependencies in CMake projects: pre-built packages and source dependencies. Pre-built packages can be located using a command that searches well-known locations and paths provided by the project or user. This command supports config files provided by the package itself or separate module files. Source dependencies can be downloaded and incorporated into the project’s build process using a dedicated CMake module. This module supports various download methods and allows the project to link against the dependency’s targets.

CMake offers two distinguished methods to deal with pre-built packages and source dependencies: the find_package command and the FetchContent module.

Pre-built packages

The find_package command searches for pre-built packages in well-known locations and paths provided by the project or user. It supports both config mode, which relies on files provided by the package itself, and module mode, which uses separate Find module files. For example, find_package(Boost 1.79 COMPONENTS date_time) searches for the Boost library version 1.79 with the date_time component. Users can set cache variables like CMAKE_PREFIX_PATH to influence where packages are found.

Caching CMAKE_PREFIX_PATH is extremely powerful and it offers to complex builds. If you care about ABI compatibility, you can craft a careful solution relying solely on CMAKE_PREFIX_PATH and removing package managers altogether!

The find_package command also supports package components and optional packages, allowing for flexibility in dependency management. When a package is found, CMake provides result variables and imported targets for the project to use, simplifying the process of linking to the dependency.

Source dependencies

The FetchContent module, on the other hand, allows dependencies to be built from source as part of the main project. Projects declare dependencies using FetchContent_Declare and make them available with FetchContent_MakeAvailable. For instance, a project can declare a dependency on Google Test with:

FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG master)

Various download methods are supported, including archives and repositories. When a dependency is added with FetchContent, the project can link to the dependency’s targets just like any other target within the project.

The superpower of FetchContent

Not many developers know that, since CMake 3.24, FetchContent is all you need: its key feature is the ability to integrate with the find_package command using the FIND_PACKAGE_ARGS option or the FETCHCONTENT_TRY_FIND_PACKAGE_MODE variable. When declaring a dependency with FetchContent_Declare, the FIND_PACKAGE_ARGS option can be used to specify arguments that will be passed to the find_package command. This allows the project to first attempt to find a pre-built version of the dependency using find_package before falling back to building it from source.

This is how I import Highway in my project:

FetchContent_Declare(
  ghwy
  GIT_REPOSITORY "https://github.com/google/highway.git"
  GIT_TAG master
  GIT_PROGRESS OFF
  GIT_SHALLOW ON
  FIND_PACKAGE_ARGS NAMES hwy)

In this case, CMake will first try to find the “hwy” package using find_package(hwy). If a pre-built version of the package is found, it will be used. Otherwise, CMake will proceed to download and build the dependency from the specified Git repository.

The FETCHCONTENT_TRY_FIND_PACKAGE_MODE variable provides high-level control over the behavior of FetchContent when integrated with find_package. It can be set to one of three values:

  • NEVER: Disables all redirection to find_package, forcing dependencies to always be built from source.
  • ALWAYS: Tries find_package even if FIND_PACKAGE_ARGS is not specified. This should be used with caution.
  • DEFAULT (the default value): Follows the behavior specified by FIND_PACKAGE_ARGS.

Conclusion

There are two conclusions intended for different audience:

  1. You are a developer. Use FetchContent_Declare with the FIND_PACKAGE_ARGS NAMES option by default. If you’re life is more complex than that, blame your DevOps Manager and refer them to this documentation by CMake.

  2. You are a DevOps Manager. Hire another DevOps engineer.

Published At
Tagged with
Creative Commons Attribution-ShareAlike (CC BY-SA)