Tutorial: Preparing libraries for CMake FetchContent
If you’re working on an executable project in C++, as opposed to a C++ library, using a package manager to get your dependencies might be overkill:
If all you need is to get the source code of a library, include in your CMake project, and have it compiled from source with the rest of your project, CMake’s FetchContent
module can do it for you.
If you’re a library writer, there are ways you can structure your CMake project to improve the experience for end users that use FetchContent
:
hide developer targets like tests, provide a zip archive that contains only the source files relevant downstream, and use GitHub actions to create it automatically.
Let’s see how.
Basic FetchContent
usage
FetchContent is a CMake module that makes downloading or “fetching” dependencies really trivial.
All you need is to let CMake know where the sources are with a call to FetchContent_Declare()
and then include them as a subproject with FetchContent_MakeAvailable()
.
This will automatically download the project and make the targets available so you can link against them and have them built as necessary.
FetchContent can clone git repositories,
include(FetchContent) # once in the project to include the module
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG 703bd9caab50b139428cea1aaff9974ebee5742e # release-1.10.0)
FetchContent_MakeAvailable(googletest)
# Link against googletest's CMake targets now.
individual files,
FetchContent_Declare(doctest URL https://raw.githubusercontent.com/doctest/doctest/v2.4.9/doctest/doctest.h)
FetchContent_MakeAvailable(doctest)
# Add ${doctest_SOURCE_DIR} to the project's include paths
or zipped folders.
FetchContent_Declare(lexy URL https://lexy.foonathan.net/download/lexy-src.zip)
FetchContent_MakeAvailable(lexy)
# Link against lexy's targets now.
Very simple and straightforward, refer to CMake’s documentation for more details. Let’s look at the library side of things for the remainder of the post.
Designing projects for FetchContent
If a project is used via FetchContent
, CMake will automatically call add_subdirectory()
.
This makes all targets of the project available in the parent, so you can link against them and use them.
However, this includes targets that are not useful for downstream consumers like unit tests, documentation builders, and so on. Crucially, this includes the dependencies of those targets – when using a library, I don’t want CMake to download that libraries testing framework! It is therefore a good idea to prevent that by only exposing those helper targets when not used as a subdirectory.
In the library’s root CMakeLists.txt
, it can be detected by comparing CMAKE_CURRENT_SOURCE_DIR
with CMAKE_SOURCE_DIR
: they’re only the same if it is the real root of the source tree.
As such, we only define test targets, when this is not the case:
project(my_project LANGUAGES CXX)
# define build options useful for all use
…
# define the library targets
add_subdirectory(src)
if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
# We're in the root, define additional targets for developers.
option(MY_PROJECT_BUILD_EXAMPLES "whether or not examples should be built" ON)
option(MY_PROJECT_BUILD_TESTS "whether or not tests should be built" ON)
if(MY_PROJECT_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
if(MY_PROJECT_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
…
endif()
By bifurcating the CMakeLists.txt
in that way, we can even use different CMake versions for downstream consumers and library developers.
For example, lexy requires version 3.8 to consume it, but 3.18 to develop it.
This is done by calling cmake_minimum_required(VERSION 3.18)
inside the if()
block.
What to download?
FetchContent_Declare
can download the project from many different sources, but not all sources take the same time.
At least from GitHub, cloning the git repository takes a lot longer than downloading and extracting the zipped sources:
# slow
FetchContent_Declare(lexy GIT_REPOSITORY https://github.com/foonathan/lexy)
FetchContent_MakeAvailable(lexy)
# fast
FetchContent_Declare(lexy URL https://github.com/foonathan/lexy/archive/refs/heads/main.zip)
FetchContent_MakeAvailable(lexy)
However, downloading all sources can be too much. In the case of lexy, for example, it includes many tests, examples, and benchmarks – none of which are necessary to actually consume the project as a downstream user. This is especially true, because lexy disables most functionality when used as a subproject as explained above.
So instead, for lexy, you’re meant to download a prepackaged zip file that only contains the necessary files:
the header files, source files of the library, and top-level CMakeLists.txt
.
That way, you don’t waste bandwidth or disk space on unnecessary stuff
# really fast
FetchContent_Declare(lexy URL https://lexy.foonathan.net/download/lexy-src.zip)
FetchContent_MakeAvailable(lexy)
If you’re maintaining a library meant for use with FetchContent
, I highly recommend you do that as well – especially, because the process can be completely automated.
Automatically creating and publishing packaged source files
For that, we first need to define a custom CMake target that will create the package:
set(package_files include/ src/ CMakeLists.txt LICENSE)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-src.zip
COMMAND ${CMAKE_COMMAND} -E tar c ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-src.zip --format=zip -- ${package_files}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${package_files})
add_custom_target(${PROJECT_NAME}_package DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-src.zip)
This is done in three steps.
- We define a list of all files and folders that need to be included in the package.
This always needs to include the root
CMakeLists.txt
and the include and source files of the library. - We define a custom command to create the
zip
file: it needs to invokecmake -E tar
to create an archive. It has a dependency on the list of package files, so that CMake knows it needs to rebuild the zip archive when those files change. - We define a custom target. In order to build it (which itself does nothing), we’ve instructed CMake that we need the
zip
file. So building the target will execute the custom command and create the archive.
Of course, this targets is only defined if the project is not used as a subdirectory!
With that done, we just need a GitHub action that is triggered when we create a new release and adds the packaged source files as an artifact:
name: Release
permissions:
contents: write
on:
release:
types: [published]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create build environment
run: cmake -E make_directory build
- name: Configure
working-directory: build/
run: cmake $GITHUB_WORKSPACE
- name: Package source code
working-directory: build/
run: cmake --build . --target my_project_package
- name: Add packaged source code to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/my_project-src.zip
tag: ${{ github.ref }}
Now we just need to create a new release in GitHub’s UI, wait for everything to finish execute, and automatically have a packaged source file that people can download via FetchContent
.
Conclusion
FetchContent
is a really convenient way of managing dependencies.
But you as a library authors can do a couple of things to make it even easier for the end user:
- Only define minimal targets when the project is included as a subdirectory.
- Provide minimal zipped archive of sources that users can download instead of the entire repository.
- Use GitHub actions to automatically create the archive for each release.
If you want to check the techniques out in more detail, lexy uses them.