Preserve source dir when using FetchContent or ExternalProject

Is there a way to prevent FetchContent / ExternalProject from deleting existing source dir when downloading a git repository?

I use FetchContent_Declare and FetchContent_MakeAvailable to download a repository into <project_root>/extern/repo that is a dependency of my project (using pybind11 as an example here):

set(ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}")
set(EXTERN_DIR "${ROOT_DIR}/extern")

FetchContent_Declare(
    pybind11
    GIT_REPOSITORY https://github.com/pybind/pybind11.git
    GIT_TAG master
    SOURCE_DIR "${EXTERN_DIR}/pybind11")
FetchContent_MakeAvailable(pybind11)

I want to be able to safely make changes in this clone without fear of my changes being deleted with no warning.

Currently, cmake behaves as follows:

mkdir build
cd build
cmake ..  # *deletes* the existing extern/pybind11 folder, then clones
cmake ..  # second run, only updates (does not delete)

So even though second run is fine, the initial run is problematic, because I might delete the build folder and rebuild again, thus accidentally delete the clones.

One solution is to wrap the FetchContent_MakeAvailable, to first check if the clone exists, and if so, to use add_subdirectory directly. This should work, but is cumbersome.
I found this patch on the cmake mail archive, but the change doesn’t seem to be merged into cmake.

Is there any better solution than wrapping FetchContent? Would it make sense to post an issue on gitlab regarding the patch?

I strongly recommend you don’t modify the sources that FetchContent is managing. As you’ve seen, you can easily lose changes. Instead, make use of the FETCHCONTENT_SOURCE_DIR_... variables to point it at a local clone that you create and manage yourself. This feature is specifically intended for your scenario, where you want to make changes and be in control of the source tree rather than leaving it to FetchContent to manage. You set these variables on the cmake command line or directly editing the cache in the CMake GUI or similar tool, not in the project directly (because it is meant to be a user override, not a project setting). Search for FETCHCONTENT_SOURCE_DIR in the FetchContent documentation for more info.

I would also strongly recommend you don’t try to get FetchContent or ExternalProject to place your files in your source tree. The things they download/create should be considered build artifacts. Your source tree should ideally remain untouched by a build. Personally, I almost never override the default location for where FetchContent or ExternalProject put things. I use the FETCHCONTENT_SOURCE_DIR_... feature for those cases where I want to work on a dependency locally, and for everything else I will generally let things get put wherever those modules put them. If I need to know where they put things, they provide variables or properties that I can query to retrieve the location rather than me trying to force things to be somewhere.

Nice, I wasn’t aware of the FETCHCONTENT_SOURCE_DIR_... variable.

Still, I want to keep the subproject clone within the project folder to keep the whole thing self-contained and to make sure my changes to the subproject clone don’t affect other projects that use it (effectively, I want more or less the behavior of meson’s subprojects).

So, I think I will go with a compromise. By default, I will let FetchContent clone the repos in the build folder. However, if the user sets some kind of a “development mode” flag, I will put the clone in the "$ROOT_DIR/extern/" folder and do the if (EXISTS ...) + add_subdirectory guard as I described in the original post. Cumbersome, but should do the job.

Thanks a lot for your help!