importing external projects using fetchContent or find_package

I am trying to wrap my head around the interplay between find_package and fetchContent.

Firstly: I need to support 2 use cases:

  • On my laptop I develop locally in WSL. So I have full internet available and I can use FetchContent to download stuff.
  • When building on our embedded target we use ssh to remotely copy the source files and run cmake on the embedded target. No internet connection.

As such, I would like to create a scenario (if possible) where locally I can use the fetchContent to download the dependencies, but on the embedded target I will have to provide them manually. The concept of FetchContent_Declare with “FIND_PACKAGE_ARGS” (as described in chapter 39.5.1 from Scotts book and in the online manual) seemed perfect.

But, I also want to optimize my workstation and build times. So preferably I download and build it only once and that is that. But strangely that seems not to work.

Information: using cmake 4.0.3, and WSL.

Question 1:
If I use this (which is just the online example)

cmake_minimum_required(VERSION 3.24)

# Use new FetchContent behavior if supported
set(CMAKE_POLICY_DEFAULT_CMP0168 NEW)

include(FetchContent)
FetchContent_Declare(
  Catch2
  GIT_REPOSITORY    https://github.com/catchorg/Catch2.git
  GIT_TAG           2b60af89e23d28eefc081bc930831ee9d45ea58b #v3.8.1
  FIND_PACKAGE_ARGS
)

FetchContent_MakeAvailable(Catch2)
find_package(Catch2)

Then indeed I see in my out/builds/<config>/_deps folder the download of the 3 catch2 projects:

  • catch2-build
  • catch2-src
  • catch2-subbuild

(No idea why 3 and where they come from. In no way do I recognize anything from the git repo you get when visiting github.) I also see config files appearing in the CMakeFiles/pkgRedirects folder, as expected.

Cmake reports that this took around 7s to do (can vary up to 10s). What I don’t understand is why it takes 7 seconds every time? If I just rerun cmake without cleaning, I would expect it to go much faster the second time since there is no need to download again.

Question 2:
How does this now connect to find_package?
I would expect that at some point the downloaded source files get compiled into the output that find_package needs. So that in subsequent runs it is using the find_package to link directly to the most optimized version of the dependencies. If so, where is that output being stored? How should I refer to this so that my second run gets faster?

Question 3:
I think I understand why it chooses a folder so deep, but the fact that it is a subfolder of the project and the config means that it is downloading catch2 for every project-config combination. As you can imagine we use testing in almost any projects, so probably I have a dozen copies of catch2 on my drive (obviously catch2 is just an example, but not the only dependency that is used a lot).
What is the good way to deal with this?
I was thinking of changing (in my CMakeUserPresets) the FETCHCONTENT_BASE_DIR to be outside my project and refer all my local builds to the same base dir. This could be like my local dependency repo.
But as I experiment with this, It seems that fetchcontent is not the good way to populate this repo as it does not compile. So the find_package is still unable to find the things fetchcontent downloaded.

Based on my internet usage, I see that it is not actually downloading anything anymore. So that still begs the question why it takes 7 seconds to make available a dependency that was already present on my computer.

Use case 1: local found

bash-5.2$ time cmake -S . -B build --log-level=TRACE --fresh --log-level=DEBUG
-- The CXX compiler identification is AppleClang 16.0.0.16000026
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Trying find_package(Catch2 ...) before FetchContent
-- Configuring done (0.6s)
-- Generating done (0.0s)
-- Build files have been written to: /Users/clausklein/Workspace/cpp/xxx/build

real    0m0.587s
user    0m0.280s
sys     0m0.273s
bash-5.2$ 

Use case 2: local not found

bash-5.2$ time cmake -S . -B build --log-level=TRACE --fresh --log-level=DEBUG
-- The CXX compiler identification is AppleClang 16.0.0.16000026
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Trying find_package(Catch2 ...) before FetchContent
-- Executing download step for catch2
'/Users/clausklein/.local/bin/cmake' '-E' 'rm' '-rf' '/Users/clausklein/Workspace/cpp/xxx/build/_deps/catch2-src'
'/usr/local/bin/git' 'clone' '--no-checkout' '--config' 'advice.detachedHead=false' 'https://github.com/catchorg/Catch2.git' 'catch2-src'
Cloning into 'catch2-src'...
'/usr/local/bin/git' 'checkout' 'v3.8.0' '--'
HEAD is now at 914aeecf v3.8.0
'/usr/local/bin/git' 'submodule' 'update' '--recursive' '--init'
'/Users/clausklein/.local/bin/cmake' '-E' 'copy' '/Users/clausklein/Workspace/cpp/xxx/build/CMakeFiles/fc-stamp/catch2/catch2-gitinfo.txt' '/Users/clausklein/Workspace/cpp/xxx/build/CMakeFiles/fc-stamp/catch2/catch2-gitclone-lastrun.txt'
-- Executing update step for catch2
-- Already at requested tag: v3.8.0
-- Executing patch step for catch2
-- Performing Test HAVE_FLAG__ffile_prefix_map__Users_clausklein_Workspace_cpp_xxx_build__deps_catch2_src__
-- Performing Test HAVE_FLAG__ffile_prefix_map__Users_clausklein_Workspace_cpp_xxx_build__deps_catch2_src__ - Success
-- Configuring done (8.7s)
-- Generating done (0.1s)
-- Build files have been written to: /Users/clausklein/Workspace/cpp/xxx/build

real    0m8.862s
user    0m4.844s
sys     0m1.624s
bash-5.2$ 
cmake_minimum_required(VERSION 3.24...4.1)

project(ProjectFetch CXX)

# Use new FetchContent behavior if supported
set(CMAKE_POLICY_DEFAULT_CMP0168 NEW)

include(FetchContent)
FetchContent_Declare(
  Catch2
  GIT_REPOSITORY    https://github.com/catchorg/Catch2.git
  GIT_TAG           v3.8.0
  FIND_PACKAGE_ARGS 3.8.0 EXACT
)

FetchContent_MakeAvailable(Catch2)
find_package(Catch2 REQUIRED)

First install the Catch2 packages to your build host.

Then build your project.

Thank you. I understand that:

  • also on your system it takes 8 seconds for cmake to understand that the download is already up to date.
  • fetchContent is more to be used as a backup. We prefer for developers to install the dependencies manually (ideally precompiled), but if they don’t the fetchContent in the project will offer a slow backup scenario to still get things working.
  • and correct me if I am wrong, but if devs install the dependency locally then either:
    • it is installed in standard location and cmake will find it automatically
    • It is installed in a non-standard location and they can provide (or better append to) ‘CMAKE_PREFIX_PATH’ to offer alternative search locations
    • Or they can provide e.g. Catch2_DIR directly, which should point to the folder containing …config.cmake/findCatch2.cmake file.
  • cmake is not the tool to download and prepare your dependencies locally.

Notes on Building CMake Projects and Dependency Management

It depends on what you expect.

If you want to build your project from source, all your build tools and dependencies must be properly set up.
The fastest and cleanest approach is to have everything already installed and ready to use via find_package().

However, keep in mind that everything must be built for the same binary ABI.

FetchContent can help fetch and build dependencies alongside your project. But this approach has limitations:

  • It doesn’t always work, as not all projects are prepared to be built as subprojects.
  • Dependencies fetched this way are built in your binary tree and are temporary—they are not installed system-wide.

To make dependencies persistent and reusable, consider using a package manager such as:

  • vcpkg – Microsoft’s cross-platform C++ library manager.
  • conan – A widely-used C++ package manager focused on flexibility and integration.
  • CPM.cmake – A CMake script for dependency management built on top of FetchContent.

Each of these tools can help manage dependencies more effectively, especially for larger projects or when working in teams.

1 Like