In this post, we will talk about dependency management in C++. In particular, we introduce FetchContent
, a CMake feature that should get more love!
We start with an overview of dependency management for C++ applications.
Git submodules
In the past, my default was to add each dependency as a git submodule. For dependencies with a modern CMake setup, this is enough to make it available to your project.
But what I always disliked about this approach, is that it requires each user to run
$ git submodule update --init
before building the code. I like to make things easy on people using my code. Thus I’m always on the lookout for a way to get rid of running any extra commands. Also, I have caught myself forgetting to run git submodule update --init
many times already.
Copying the code into your repository
Of course one can also copy the whole source code of the dependency into the repository of the project. Yet, in my view, this is inferior to submodules. Updating dependencies becomes complicated. And, depending on how big your dependencies are, this will bloat your repository.
Conan (and other package managers)
Another option is using a package manager like conan.
I found them an adequate solution for bigger projects. They’re also nice when you have dependencies that take very long to build (I’m looking at you OpenCV! ).
But for your small side project that needs two or three libraries from Github, it’s often too much effort.
My main critique point is that the package manager itself becomes a dependency. Everyone that wants to build your project has install the correct version. If you’re using CI, you have to set up everything on the build server as well, which can be a pain sometimes.
Can CMake help us here?
Younger languages like Rust and Go incorporated package management in the build system. This makes for a nicer developer experience as one does not have to choose which package manager to use. Also, packages are by default set up for the languages built-in package manager.
With this in mind, it’s natural to look towards CMake for a solution. I was excited to hear that CMake introduced a module called ExternalProject
in version 3.0. ExternalProject
wraps dependencies into a CMake target and allows managing foreign code from your CMakeLists.txt
.
To use it, one must add a target via ExternalProject_Add()
. CMake will then run the following steps for this target.
DOWNLOAD
Download the dependency. Here one can use a version control system or download from an URL.
UPDATE
Update the downloaded code if anything changed since the last CMake run.
CONFIGURE
Configure the project code.
BUILD
Build the dependencies code.
INSTALL
Install the built code into a specified directory.
TEST
(optional)
Run tests.
All the above commands are configurable.
ExternalProject
also allows for custom steps.
For more information, have a look at the documentation.
So, is this the solution?
After playing around with it for a little bit, I found that ExternalProject
is not what I was looking for.
The reason is that when using ExternalProject
, all its steps will run at build time.
This means that CMake downloads and builds your dependencies after the generation step.
So your dependencies will not be available yet when CMake configures your project.
Will we have to stick to submodules then?
No, we won’t!
With version 3.11 CMake introduced a new module: FetchContent
.
The module offers the same functionality as ExternalProject
but will download dependencies before the configure step.
That means we can use it to manage our C++ project dependencies from the CMakeLists.txt
file!
How to use FetchContent
In this repository, I prepared an example. It uses FetchContent
to include the libraries’ doctest and range-v3.
CMake will download and build the dependencies.
Very convenient!
I will explain how everything works below. Make sure you get the code to play around with it.
We begin by creating a regular CMake project.
The FetchContent
API got a makeover that makes usage easier in version 3.14.
Thus this is the least version we need.
Then we include the FetchContent
module.
We register each dependency with a call to FetchContent_Declare()
.
When making this call, you can customize how your dependency is loaded.
FetchContent
understands almost the same options as ExternalProject
.
But the options related to CONFIGURE
, BUILD
, INSTALL
, and TEST
are disabled.
We declare two targets, one for doctest
and one for range-v3
.
CMake downloads both libraries via their git repository.
The parameter GIT_TAG
specifies the commit in the dependencies history we use.
One can also use git branch names or tags here.
Yet, new commits can change what the branch is pointing to. This might affect the reproducibility of your project.
So the CMake documentation discourages using branch names or tags.
Next, we call FetchContent_MakeAvailable()
.
This call makes sure CMake downloads our dependencies and adds their directories.
Finally, we can add an executable and link to the included packages. CMake has taken over all the heavy lifting!
Using git repositories is the most convenient way of including dependencies with FetchContent
.
But, if the code you depend on is not a git repository, you can customize FetchContent
to work with other sources.
Have a look at the documentation of ExternalProject
.
It explains all parameters.
The whole CMakeLists.txt
file will look like this.
After setting everything up in CMake, we can use the packages in our source code. Below is a small test program that makes use of both included libraries.
The full example project is here.
What to watch out for
Here are some things to keep in mind when using FetchContent
.
Downloading requires an internet connection
Of course, you have to be online for first downloading your dependencies. Compared to using submodules, this requirement is now hidden. So you might forget about it when building your code. To mitigate this problem, there is a set of options.
-
FETCHCONTENT_FULLY_DISCONNECTED=ON
will skip theDOWNLOAD
andUPDATE
steps -
FETCHCONTENT_UPDATES_DISCONNECTED=ON
will skip theUPDATE
step
Output can become pretty verbose
FetchContent
will log all its steps.
That’s why console output can become hard to read.
To mute all output set FETCHCONTENT_QUIET
to ON
.
The library has to be installable
A problem that I ran into quite often is that the dependency I wanted to use was not installable. Every once in a while you will come across libraries that are missing the call to install()
in their CMakeLists.txt
.
In this case, FetchContent
does not know how to copy the built code into the install folder and will fail.
Consider adding the install()
calls and creating a PR.
FetchContent
works best with CMake based dependencies. I haven’t had a chance to test it with libraries that are not built with CMake. But I would expect that some extra configuration is necessary to make it work.
Conclusion
In this blog post, we learned about FetchContent
.
Now you know how to manage your dependencies from within your CMake setup.
We saw how to use FetchContent
to pull in dependencies for a small example project.
And we read about some things to watch out for when using it.
I like this way of managing dependencies very much. But of course, you can always mix different approaches and use what’s best for you!
Please let me know if you think I missed anything in this article!
Follow me on twitter (@bewagner_) for more of my thoughts on C++!