Prevent FetchContent_MakeAvailable to execute CMakeLists.txt

After seeing the deprecation warnings for FetchContent_Populate I replaced it with FetchContent_MakeAvailable but now my project is failing.

I use to do this (in a nutshell)

# ...
FetchContent_Declare(re-logging ...)
FetchContent_Populate(re-logging)
include("${re-logging_ROOT_DIR}/re-logging.cmake")

The issue I am facing with replacing FetchContent_Populate with FetchContent_MakeAvailable is that FetchContent_MakeAvailable seems to automatically “execute” CMakeLists.txt included in re-logging but I don’t want this to happen. FetchContent_Populate did not do that.

I want to load re-logging.cmake instead.

Is there a way to do this?

Use the SOURCE_SUBDIR option and set it to a path that doesn’t exist. Issue 26220 asks for a dedicated keyword to say “don’t add the subdirectory, only populate it”.

Thank you for the workaround.

We went away from CMake after this decision. To replace a working function with such a workaround is simply unprofessional.

So populating content without adding it to the build does not work?

I though there is a DOWNLOAD_ONLY option as CPM.cmake has?

FetchContent_Populate works like a charm but is deprecated now. And thats the problem.

Many open source projects continued to use FetchContent_Populate() long after FetchContent_MakeAvailable() replaced it as the recommended way to populate content. That prevented those projects from being handled by dependency providers. The deprecation is a clear signal that projects relying on FetchContent_Populate() need to migrate. Everything you could do with FetchContent_Populate() before, you can do with FetchContent_MakeAvailable() now. My earlier comment explains how to use SOURCE_SUBDIR to prevent calling add_subdirectory() for those who were using FetchContent_Populate() to only download things but not add them to the project. That comment also linked to an issue that discusses that use case in more detail.

Is the documentation wrong?
Quote from https://cmake.org/cmake/help/latest/module/FetchContent.html#populating-content-without-adding-it-to-the-build

Projects don't always need to add the populated content to the build.
 Sometimes the project just wants to make the downloaded content 
available at a predictable location. 
...

It’s not wrong, it just doesn’t cover the case specifically being discussed here. That example assumes there is no CMakeLists.txt in the top level of the toolchains project. If I find time, I’ll update that example to make things clearer.

I also just added a comment to the linked issue to give more clarity on my thinking around the requested feature.

Maybe I am missing something, but I have another project that was using FetchContent_Declare and FetchContent_Populate.

Old code:

FetchContent_Declare(jamba
      GIT_REPOSITORY    ${JAMBA_GIT_REPO}
      GIT_TAG           ${JAMBA_GIT_TAG}
      GIT_CONFIG        advice.detachedHead=false
      GIT_SHALLOW       true
      SOURCE_DIR        "${CMAKE_BINARY_DIR}/jamba"
      BINARY_DIR        "${CMAKE_BINARY_DIR}/jamba-build"
      CONFIGURE_COMMAND ""
      BUILD_COMMAND     ""
      INSTALL_COMMAND   ""
      TEST_COMMAND      ""
      )

FetchContent_GetProperties(jamba)

if(NOT jamba_POPULATED)

  if(FETCHCONTENT_SOURCE_DIR_JAMBA)
    message(STATUS "Using jamba from local ${FETCHCONTENT_SOURCE_DIR_JAMBA}")
  else()
    message(STATUS "Fetching jamba ${JAMBA_GIT_REPO}/tree/${JAMBA_GIT_TAG}")
  endif()

  FetchContent_Populate(jamba)

New code, does not use FetchContent_Declare and instead uses the other form of FetchContent_Populate

if(JAMBA_ROOT_DIR)
  message(STATUS "Using jamba from local ${JAMBA_ROOT_DIR}")
  FetchContent_Populate(jamba
      QUIET
      SOURCE_DIR        "${JAMBA_ROOT_DIR}"
      BINARY_DIR        "${CMAKE_BINARY_DIR}/jamba-build"
  )
else()
  if(JAMBA_DOWNLOAD_URL STREQUAL "" OR JAMBA_DOWNLOAD_URL_HASH STREQUAL "")
    message(STATUS "Fetching jamba from ${JAMBA_GIT_REPO}/tree/${JAMBA_GIT_TAG}")
    FetchContent_Populate(jamba
        QUIET
        GIT_REPOSITORY    ${JAMBA_GIT_REPO}
        GIT_TAG           ${JAMBA_GIT_TAG}
        GIT_CONFIG        advice.detachedHead=false
        GIT_SHALLOW       true
        SOURCE_DIR        "${CMAKE_BINARY_DIR}/jamba"
        BINARY_DIR        "${CMAKE_BINARY_DIR}/jamba-build"
    )
  else()
    message(STATUS "Fetching jamba from ${JAMBA_DOWNLOAD_URL}")
    FetchContent_Populate(jamba
        QUIET
        URL                        "${JAMBA_DOWNLOAD_URL}"
        URL_HASH                   "${JAMBA_DOWNLOAD_URL_HASH}"
        DOWNLOAD_EXTRACT_TIMESTAMP true
        SOURCE_DIR                 "${CMAKE_BINARY_DIR}/jamba"
        BINARY_DIR                 "${CMAKE_BINARY_DIR}/jamba-build"
    )
  endif()
endif()

and I am simply not calling FetchContent_MakeAvailable.

Unless I am wrong it is not FetchContent_Populate that is deprecated, it is FetchContent_Populate(<name>) with a single argument: per the documentation

FetchContent_Populate(<name>)
Changed in version 3.30: This form is deprecated. Policy CMP0169 provides backward compatibility for projects that still need to use this form, but projects should be updated to use FetchContent_MakeAvailable() instead.

Can you please confirm? I am not sure why the “workaround” that was suggested was to use a hack when it seems to me that the proper and not hacky way is simply to not use FetchContent_Declare and use the other form of FetchContent_Populate which is not deprecated.

Switching to the multi-argument form of FetchContent_Populate() is not the right choice. Using that form in project code is almost always wrong. That multi-argument form exists almost exclusively for use in CMake script mode. Using the multi-argument form in projects still prevents dependency providers from being used, and it bypasses all the logic that allows the developer to use their own local override with CMake variables of the form FETCHCONTENT_SOURCE_DIR_<uppercaseDepName>.

The right change is to keep the call to FetchContent_Declare(), and replace your call to FetchContent_Populate(jamba) with FetchContent_MakeAvailable(jamba). If you want to prevent FetchContent_MakeAvailable(jamba) from adding the jamba source directory to the build, add a SOURCE_SUBDIR argument to your FetchContent_Declare() call and set it to a path that does not exist in the jamba sources.

You can decide whether you want to still keep the message() output, but personally I’d remove it. A good rule of thumb is to only log output if something goes wrong. When you have a lot of output, it trains developers to ignore all output and then they frequently miss things that are actually important. If you follow this advice, your project could be just this:

FetchContent_Declare(jamba
      GIT_REPOSITORY    ${JAMBA_GIT_REPO}
      GIT_TAG           ${JAMBA_GIT_TAG}
      GIT_CONFIG        advice.detachedHead=false
      GIT_SHALLOW       true
      SOURCE_DIR        "${CMAKE_BINARY_DIR}/jamba"
      BINARY_DIR        "${CMAKE_BINARY_DIR}/jamba-build"
      SOURCE_SUBDIR     pathThatDoesNotExist
)
FetchContent_MakeAvailable(jamba)

All the CONFIGURE_COMMAND, BUILD_COMMAND, INSTALL_COMMAND and TEST_COMMAND keywords are useless and can be removed. They have no meaning for FetchContent and the values you were setting is what FetchContent enforces internally anyway.

There’s also not much point specifying BINARY_DIR if you’re going to prevent FetchContent_MakeAvailable() from adding the dependency to the project. The binary directory won’t be used in that case.

Craig, thank you for the detailed explanation. I am going to try your suggestion with the invalid SOURCE_SUBDIR. That being said, I have a few questions:

  • what do you mean by: “Using the multi-argument form in projects still prevents dependency providers from being used”?
  • when you say, “the logic that allows developer to … FETCHCONTENT_SOURCE_DIR_<uppercaseDepName>.” I believe this is what I am offering to the developer: if they specify JAMBA_ROOT_DIR, then it does not download it, and simply use the one that is local.

At the end of the day, the logic that I want, is to be able to fetch the content, period. Which is what the module FetchContent is supposed to do. I think the MakeAvailable part is not what the name FetchContent implies, but somehow you have to use FetchContent_MakeAvailable in order for the content fetching to happen.

As an analogy, when I “download” something with my browser, I am not expecting the browser to “execute” it…

I also wanted to add a little bit of background about this project (Jamba).

Jamba is a framework not a library. As such you “fetch” it and then you use it. And by using it, you include a file. Something like:

# Step 1: fetch Jamba (either via GIT TAG or DOWNLOAD URL or use local version)
# => ${JAMBA_ROOT_DIR} now contains the location of the framework

# Step 2: include the framework, which sets up a bunch of variables and cmake functions
include(${JAMBA_ROOT_DIR}/jamba.cmake)

# Step 3: use the framework
jamba_add_vst_plugin(....)

I feel like the module FetchContent should allow to do what its name suggest: fetching content and stop there. I understand that using SOURCE_SUBDIR with an invalid path will do just that when then invoking FetchContent_MakeAvailable but this is really twisting things into a pretzel to achieve what should be (and was without warning) possible.

FetchContent_Populate() has two forms, as you’ve already discovered. The form that takes a single argument (a dependency name) requires an earlier call to FetchContent_Declare() to define how to populate that dependency. This one-argument form is what FetchContent_MakeAvailable() replaces (and extends with additional capabilities). Dependency providers can intercept calls to FetchContent_MakeAvailable() and populate the dependency in whatever way they like, using the information given in FetchContent_Declare(), or ignoring that information and choosing to populate it some other way (e.g. the cmake-conan dependency provider takes the information from your conanfile.py or other equivalent Conan-specific files).

The other form of FetchContent_Populate() takes more than one argument. It does not expect or use any information from an earlier call to FetchContent_Declare(), it is its own standalone call. It was only ever intended for use by standalone CMake scripts, executed in CMake script mode (i.e. cmake -P scriptName). It should not be used in project code. Such calls cannot be intercepted by dependency providers, and this is a pain point for open source projects that ignore this advice and use it anyway. It forces consumers who want to get their dependencies a different way to have to modify that project. Dependency providers give that flexible capability without having to modify projects, but it requires projects to follow the recommended way of doing things.

Yes, but it is using a different variable to the official one FetchContent provides and that developers would already know how to use. And your implementation reimplements functionality you can already get for free just by using FetchContent_MakeAvailable() instead.

There’s a lot of history here. When FetchContent was first added to CMake, FetchContent_MakeAvailable() wasn’t part of the picture. It didn’t take long for folks to complain there were too many calls involved in populating content when following the canonical pattern. FetchContent_MakeAvailable() was added to simplify things down to the minimum two calls (FetchContent_Declare() being the other call). A few CMake releases later, FetchContent_MakeAvailable() was made more flexible by allowing it to download content that had no CMakeLists.txt file in their top level. That essentially enhanced the command’s behavior to become “add it to the project if you can, but it’s not an error if you can’t”. Later on, dependency providers were added, and those take advantage of this consolidation by intercepting FetchContent_MakeAvailable(). This still allows projects to specify how things should happen if there is no dependency provider (the FetchContent_Declare() call defines that).

The name FetchContent_MakeAvailable() was chosen carefully. It doesn’t say anything about how the dependency is “made available”. I was thinking forward to likely features that would be added later, and those did end up being added, as described above. The phrase “make available” can mean adding it to the project (the default behavior), or it could mean only downloading it (which is what happens if the content doesn’t have a CMakeLists.txt at the top level, or if a SOURCE_SUBDIR points it somewhere else that doesn’t exist).

Naming is hard. Sometimes names are constrained by what names are already in use. That has been the case for FetchContent. I can only encourage folks to consider the considerable gain in flexibility for their consumers by adopting FetchContent_MakeAvailable(), even if it isn’t the perfect name for your particular use case (there never will be a perfect name for something that covers the broad set of use cases that FetchContent_MakeAvailable() does).

See the issue I linked in my earlier comment. The new keyword proposed there would be pretty clear.

Thank you for providing the very detailed explanation including history.

I have now changed my code to use FetchContent_Declare + SOURCE_SUBDIR / FetchContent_MakeAvailable.

Due to how my framework is being used, I would rather keep the simpler way to customize it from a user point of view even if it means I am re-implementing some of the features that FetchContent_MakeAvailable offers (note that my project was created prior to FetchContent_MakeAvailable existing so did not have a choice at the time).

As long as this is the official way to achieve what I need (and I do see that it is documented on the website) and it is not a hacky workaround that will be removed in an upcoming release, then I am fine with the changes.