Merge topic 'LLVMFlang-compiler'

85749766df LLVMFlang: Add support for LLVM Flang

Acked-by: Kitware Robot <kwrobot@kitware.com>
Merge-request: !7246
diff --git a/Help/command/cmake_language.rst b/Help/command/cmake_language.rst
index 2859f6b..e49862f 100644
--- a/Help/command/cmake_language.rst
+++ b/Help/command/cmake_language.rst
@@ -13,6 +13,7 @@
   cmake_language(`CALL`_ <command> [<arg>...])
   cmake_language(`EVAL`_ CODE <code>...)
   cmake_language(`DEFER`_ <options>... CALL <command> [<arg>...])
+  cmake_language(`SET_DEPENDENCY_PROVIDER`_ <command> SUPPORTED_METHODS <methods>...)
 
 Introduction
 ^^^^^^^^^^^^
@@ -225,3 +226,265 @@
   Immediate Message
   Deferred Message 1
   Deferred Message 2
+
+
+.. _SET_DEPENDENCY_PROVIDER:
+.. _dependency_providers:
+
+Dependency Providers
+^^^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 3.24
+
+.. code-block:: cmake
+
+  cmake_language(SET_DEPENDENCY_PROVIDER <command>
+                 SUPPORTED_METHODS <methods>...)
+
+When a call is made to :command:`find_package` or
+:command:`FetchContent_MakeAvailable`, the call may be forwarded to a
+dependency provider which then has the opportunity to fulfill the request.
+If the request is for one of the ``<methods>`` specified when the provider
+was set, CMake calls the provider's ``<command>`` with a set of
+method-specific arguments.  If the provider does not fulfill the request,
+or if the provider doesn't support the request's method, or no provider
+is set, the built-in :command:`find_package` or
+:command:`FetchContent_MakeAvailable` implementation is used to fulfill
+the request in the usual way.
+
+One or more of the following values can be specified for the ``<methods>``
+when setting the provider:
+
+``FIND_PACKAGE``
+  The provider command accepts :command:`find_package` requests.
+
+``FETCHCONTENT_MAKEAVAILABLE_SERIAL``
+  The provider command accepts :command:`FetchContent_MakeAvailable`
+  requests.  It expects each dependency to be fed to the provider command
+  one at a time, not the whole list in one go.
+
+Only one provider can be set at any point in time.  If a provider is already
+set when ``cmake_language(SET_DEPENDENCY_PROVIDER)`` is called, the new
+provider replaces the previously set one.  The specified ``<command>`` must
+already exist when ``cmake_language(SET_DEPENDENCY_PROVIDER)`` is called.
+As a special case, providing an empty string for the ``<command>`` and no
+``<methods>`` will discard any previously set provider.
+
+The dependency provider can only be set while processing one of the files
+specified by the :variable:`CMAKE_PROJECT_TOP_LEVEL_INCLUDES` variable.
+Thus, dependency providers can only be set as part of the first call to
+:command:`project`.  Calling ``cmake_language(SET_DEPENDENCY_PROVIDER)``
+outside of that context will result in an error.
+
+.. note::
+  The choice of dependency provider should always be under the user's control.
+  As a convenience, a project may choose to provide a file that users can
+  list in their :variable:`CMAKE_PROJECT_TOP_LEVEL_INCLUDES` variable, but
+  the use of such a file should always be the user's choice.
+
+Provider commands
+"""""""""""""""""
+
+Providers define a single ``<command>`` to accept requests.  The name of
+the command should be specific to that provider, not something overly
+generic that another provider might also use.  This enables users to compose
+different providers in their own custom provider.  The recommended form is
+``xxx_provide_dependency()``, where ``xxx`` is the provider-specific part
+(e.g. ``vcpkg_provide_dependency()``, ``conan_provide_dependency()``,
+``ourcompany_provide_dependency()``, and so on).
+
+.. code-block:: cmake
+
+  xxx_provide_dependency(<method> [<method-specific-args>...])
+
+Because some methods expect certain variables to be set in the calling scope,
+the provider command should typically be implemented as a macro rather than a
+function.  This ensures it does not introduce a new variable scope.
+
+The arguments CMake passes to the dependency provider depend on the type of
+request.  The first argument is always the method, and it will only ever
+be one of the ``<methods>`` that was specified when setting the provider.
+
+``FIND_PACKAGE``
+  The ``<method-specific-args>`` will be everything passed to the
+  :command:`find_package` call that requested the dependency.  The first of
+  these ``<method-specific-args>`` will therefore always be the name of the
+  dependency.  Dependency names are case-sensitive for this method because
+  :command:`find_package` treats them case-sensitively too.
+
+  If the provider command fulfills the request, it must set the same variable
+  that :command:`find_package` expects to be set.  For a dependency named
+  ``depName``, the provider must set ``depName_FOUND`` to true if it fulfilled
+  the request.  If the provider returns without setting this variable, CMake
+  will assume the request was not fulfilled and will fall back to the
+  built-in implementation.
+
+  If the provider needs to call the built-in :command:`find_package`
+  implementation as part of its processing, it can do so by including the
+  ``BYPASS_PROVIDER`` keyword as one of the arguments.
+
+``FETCHCONTENT_MAKEAVAILABE_SERIAL``
+  The ``<method-specific-args>`` will be everything passed to the
+  :command:`FetchContent_Declare` call that corresponds to the requested
+  dependency, with the following exceptions:
+
+  * If ``SOURCE_DIR`` or ``BINARY_DIR`` were not part of the original
+    declared arguments, they will be added with their default values.
+  * If :variable:`FETCHCONTENT_TRY_FIND_PACKAGE_MODE` is set to ``NEVER``,
+    any ``FIND_PACKAGE_ARGS`` will be omitted.
+  * The ``OVERRIDE_FIND_PACKAGE`` keyword is always omitted.
+
+  The first of the ``<method-specific-args>`` will always be the name of the
+  dependency.  Dependency names are case-insensitive for this method because
+  :module:`FetchContent` also treats them case-insensitively.
+
+  If the provider fulfills the request, it should call
+  :command:`FetchContent_SetPopulated`, passing the name of the dependency as
+  the first argument.  The ``SOURCE_DIR`` and ``BINARY_DIR`` arguments to that
+  command should only be given if the provider makes the dependency's source
+  and build directories available in exactly the same way as the built-in
+  :command:`FetchContent_MakeAvailable` command.
+
+  If the provider returns without calling :command:`FetchContent_SetPopulated`
+  for the named dependency, CMake will assume the request was not fulfilled
+  and will fall back to the built-in implementation.
+
+  Note that empty arguments may be significant for this method (e.g. an empty
+  string following a ``GIT_SUBMODULES`` keyword).  Therefore, if forwarding
+  these arguments on to another command, extra care must be taken to avoid such
+  arguments being silently dropped.
+
+  If ``FETCHCONTENT_SOURCE_DIR_<uppercaseDepName>`` is set, then the
+  dependency provider will never see requests for the ``<depName>`` dependency
+  for this method. When the user sets such a variable, they are explicitly
+  overriding where to get that dependency from and are taking on the
+  responsibility that their overriding version meets any requirements for that
+  dependency and is compatible with whatever else in the project uses it.
+  Depending on the value of :variable:`FETCHCONTENT_TRY_FIND_PACKAGE_MODE`
+  and whether the ``OVERRIDE_FIND_PACKAGE`` option was given to
+  :command:`FetchContent_Declare`, having
+  ``FETCHCONTENT_SOURCE_DIR_<uppercaseDepName>`` set may also prevent the
+  dependency provider from seeing requests for a ``find_package(depName)``
+  call too.
+
+Provider Examples
+"""""""""""""""""
+
+This first example only intercepts :command:`find_package` calls.  The
+provider command runs an external tool which copies the relevant artifacts
+into a provider-specific directory, if that tool knows about the dependency.
+It then relies on the built-in implementation to then find those artifacts.
+:command:`FetchContent_MakeAvailable` calls would not go through the provider.
+
+.. code-block:: cmake
+  :caption: mycomp_provider.cmake
+
+  # Always ensure we have the policy settings this provider expects
+  cmake_minimum_required(VERSION 3.24)
+
+  set(MYCOMP_PROVIDER_INSTALL_DIR ${CMAKE_BINARY_DIR}/mycomp_packages
+    CACHE PATH "The directory this provider installs packages to"
+  )
+  # Tell the built-in implementation to look in our area first, unless
+  # the find_package() call uses NO_..._PATH options to exclude it
+  list(APPEND CMAKE_MODULE_PATH ${MYCOMP_PROVIDER_INSTALL_DIR}/cmake)
+  list(APPEND CMAKE_PREFIX_PATH ${MYCOMP_PROVIDER_INSTALL_DIR})
+
+  macro(mycomp_provide_dependency method package_name)
+    execute_process(
+      COMMAND some_tool ${package_name} --installdir ${MYCOMP_PROVIDER_INSTALL_DIR}
+      COMMAND_ERROR_IS_FATAL ANY
+    )
+  endmacro()
+
+  cmake_language(
+    SET_DEPENDENCY_PROVIDER mycomp_provide_dependency
+    SUPPORTED_METHODS FIND_PACKAGE
+  )
+
+The user would then typically use the above file like so::
+
+  cmake -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=/path/to/mycomp_provider.cmake ...
+
+The next example demonstrates a provider that accepts both methods, but
+only handles one specific dependency.  It enforces providing Google Test
+using :module:`FetchContent`, but leaves all other dependencies to be
+fulfilled by CMake's built-in implementation.  It accepts a few different
+names, which demonstrates one way of working around projects that hard-code
+an unusual or undesirable way of adding this particular dependency to the
+build.  The example also demonstrates how to use the :command:`list` command
+to preserve variables that may be overwritten by a call to
+:command:`FetchContent_MakeAvailable`.
+
+.. code-block:: cmake
+  :caption: mycomp_provider.cmake
+
+  cmake_minimum_required(VERSION 3.24)
+
+  # Because we declare this very early, it will take precedence over any
+  # details the project might declare later for the same thing
+  include(FetchContent)
+  FetchContent_Declare(
+    googletest
+    GIT_REPOSITORY https://github.com/google/googletest.git
+    GIT_TAG        e2239ee6043f73722e7aa812a459f54a28552929 # release-1.11.0
+  )
+
+  # Both FIND_PACKAGE and FETCHCONTENT_MAKEAVAILABLE_SERIAL methods provide
+  # the package or dependency name as the first method-specific argument.
+  macro(mycomp_provide_dependency method dep_name)
+    if("${dep_name}" MATCHES "^(gtest|googletest)$")
+      # Save our current command arguments in case we are called recursively
+      list(APPEND mycomp_provider_args ${method} ${dep_name})
+
+      # This will forward to the built-in FetchContent implementation,
+      # which detects a recursive call for the same thing and avoids calling
+      # the provider again if dep_name is the same as the current call.
+      FetchContent_MakeAvailable(googletest)
+
+      # Restore our command arguments
+      list(POP_BACK mycomp_provider_args dep_name method)
+
+      # Tell the caller we fulfilled the request
+      if("${method}" STREQUAL "FIND_PACKAGE")
+        # We need to set this if we got here from a find_package() call
+        # since we used a different method to fulfill the request.
+        # This example assumes projects only use the gtest targets,
+        # not any of the variables the FindGTest module may define.
+        set(${dep_name}_FOUND TRUE)
+      elseif(NOT "${dep_name}" STREQUAL "googletest")
+        # We used the same method, but were given a different name to the
+        # one we populated with. Tell the caller about the name it used.
+        FetchContent_SetPopulated(${dep_name}
+          SOURCE_DIR "${googletest_SOURCE_DIR}"
+          BINARY_DIR "${googletest_BINARY_DIR}"
+        )
+      endif()
+    endif()
+  endmacro()
+
+  cmake_language(
+    SET_DEPENDENCY_PROVIDER mycomp_provide_dependency
+    SUPPORTED_METHODS
+      FIND_PACKAGE
+      FETCHCONTENT_MAKEAVAILABLE_SERIAL
+  )
+
+The final example demonstrates how to modify arguments to a
+:command:`find_package` call.  It forces all such calls to have the
+``QUIET`` keyword.  It uses the ``BYPASS_PROVIDER`` keyword to prevent
+calling the provider command recursively for the same dependency.
+
+.. code-block:: cmake
+  :caption: mycomp_provider.cmake
+
+  cmake_minimum_required(VERSION 3.24)
+
+  macro(mycomp_provide_dependency method)
+    find_package(${ARGN} BYPASS_PROVIDER QUIET)
+  endmacro()
+
+  cmake_language(
+    SET_DEPENDENCY_PROVIDER mycomp_provide_dependency
+    SUPPORTED_METHODS FIND_PACKAGE
+  )
diff --git a/Help/command/find_package.rst b/Help/command/find_package.rst
index a7d7d00..a4dad21 100644
--- a/Help/command/find_package.rst
+++ b/Help/command/find_package.rst
@@ -12,7 +12,8 @@
    .. contents::
 
 Find a package (usually provided by something external to the project),
-and load its package-specific details.
+and load its package-specific details.  Calls to this command can also
+be intercepted by :ref:`dependency providers <dependency_providers>`.
 
 Search Modes
 ^^^^^^^^^^^^
diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst
index 22c14df..848a133 100644
--- a/Help/manual/cmake-generator-expressions.7.rst
+++ b/Help/manual/cmake-generator-expressions.7.rst
@@ -197,6 +197,8 @@
 
 .. genex:: $<HIP_COMPILER_ID:compiler_ids>
 
+  .. versionadded:: 3.21
+
   where ``compiler_ids`` is a comma-separated list.
   ``1`` if the CMake's compiler id of the HIP compiler matches any one
   of the entries in ``compiler_ids``, otherwise ``0``.
@@ -249,6 +251,8 @@
 
 .. genex:: $<HIP_COMPILER_VERSION:version>
 
+  .. versionadded:: 3.21
+
   ``1`` if the version of the HIP compiler matches ``version``, otherwise ``0``.
   See also the :variable:`CMAKE_<LANG>_COMPILER_VERSION` variable.
 
@@ -716,6 +720,8 @@
 
 .. genex:: $<HIP_COMPILER_ID>
 
+  .. versionadded:: 3.21
+
   The CMake's compiler id of the HIP compiler used.
   See also the :variable:`CMAKE_<LANG>_COMPILER_ID` variable.
 
@@ -762,6 +768,8 @@
 
 .. genex:: $<HIP_COMPILER_VERSION>
 
+  .. versionadded:: 3.21
+
   The version of the HIP compiler used.
   See also the :variable:`CMAKE_<LANG>_COMPILER_VERSION` variable.
 
diff --git a/Help/release/dev/dependency-providers.rst b/Help/release/dev/dependency-providers.rst
new file mode 100644
index 0000000..8b2cf06
--- /dev/null
+++ b/Help/release/dev/dependency-providers.rst
@@ -0,0 +1,9 @@
+dependency-providers
+--------------------
+
+* The :command:`cmake_language` command gained a new
+  ``SET_DEPENDENCY_PROVIDER`` sub-command.  When a dependency provider is set,
+  calls to :command:`find_package` and :command:`FetchContent_MakeAvailable`
+  can be redirected through a custom command, which can choose to fulfill the
+  request directly, modify how the request is processed, or leave it to be
+  fulfilled by the built-in implementation.  See :ref:`dependency_providers`.
diff --git a/Help/variable/CMAKE_COMPILER_IS_GNUCC.rst b/Help/variable/CMAKE_COMPILER_IS_GNUCC.rst
index 91cf848..4b799c0 100644
--- a/Help/variable/CMAKE_COMPILER_IS_GNUCC.rst
+++ b/Help/variable/CMAKE_COMPILER_IS_GNUCC.rst
@@ -1,7 +1,7 @@
 CMAKE_COMPILER_IS_GNUCC
 -----------------------
 
-.. versionadded:: 3.7
-
 True if the ``C`` compiler is GNU.
-Use :variable:`CMAKE_C_COMPILER_ID <CMAKE_<LANG>_COMPILER_ID>` instead.
+
+This variable is deprecated.  Use
+:variable:`CMAKE_C_COMPILER_ID <CMAKE_<LANG>_COMPILER_ID>` instead.
diff --git a/Help/variable/CMAKE_COMPILER_IS_GNUCXX.rst b/Help/variable/CMAKE_COMPILER_IS_GNUCXX.rst
index e67718a..29069d2 100644
--- a/Help/variable/CMAKE_COMPILER_IS_GNUCXX.rst
+++ b/Help/variable/CMAKE_COMPILER_IS_GNUCXX.rst
@@ -1,7 +1,7 @@
 CMAKE_COMPILER_IS_GNUCXX
 ------------------------
 
-.. versionadded:: 3.7
-
 True if the C++ (``CXX``) compiler is GNU.
-Use :variable:`CMAKE_CXX_COMPILER_ID <CMAKE_<LANG>_COMPILER_ID>` instead.
+
+This variable is deprecated.  Use
+:variable:`CMAKE_CXX_COMPILER_ID <CMAKE_<LANG>_COMPILER_ID>` instead.
diff --git a/Help/variable/CMAKE_COMPILER_IS_GNUG77.rst b/Help/variable/CMAKE_COMPILER_IS_GNUG77.rst
index f69c01a..05303dc 100644
--- a/Help/variable/CMAKE_COMPILER_IS_GNUG77.rst
+++ b/Help/variable/CMAKE_COMPILER_IS_GNUG77.rst
@@ -1,7 +1,7 @@
 CMAKE_COMPILER_IS_GNUG77
 ------------------------
 
-.. versionadded:: 3.7
-
 True if the ``Fortran`` compiler is GNU.
-Use :variable:`CMAKE_Fortran_COMPILER_ID <CMAKE_<LANG>_COMPILER_ID>` instead.
+
+This variable is deprecated.  Use
+:variable:`CMAKE_Fortran_COMPILER_ID <CMAKE_<LANG>_COMPILER_ID>` instead.
diff --git a/Modules/CPackComponent.cmake b/Modules/CPackComponent.cmake
index 1f8c38c..8ca9f28 100644
--- a/Modules/CPackComponent.cmake
+++ b/Modules/CPackComponent.cmake
@@ -327,32 +327,34 @@
 if(NOT CPackComponent_CMake_INCLUDED)
 set(CPackComponent_CMake_INCLUDED 1)
 
-# Macro that appends a SET command for the given variable name (var)
-# to the macro named strvar, but only if the variable named "var"
+# Function that appends a SET command for the given variable name (var)
+# to the string named strvar, but only if the variable named "var"
 # has been defined. The string will eventually be appended to a CPack
 # configuration file.
-macro(cpack_append_variable_set_command var strvar)
+function(cpack_append_variable_set_command var strvar)
   if (DEFINED ${var})
     string(APPEND ${strvar} "set(${var}")
     foreach(APPENDVAL ${${var}})
       string(APPEND ${strvar} " ${APPENDVAL}")
     endforeach()
     string(APPEND ${strvar} ")\n")
+    set(${strvar} "${${strvar}}" PARENT_SCOPE)
   endif ()
-endmacro()
+endfunction()
 
-# Macro that appends a SET command for the given variable name (var)
-# to the macro named strvar, but only if the variable named "var"
+# Function that appends a SET command for the given variable name (var)
+# to the string named strvar, but only if the variable named "var"
 # has been defined and is a string. The string will eventually be
 # appended to a CPack configuration file.
-macro(cpack_append_string_variable_set_command var strvar)
+function(cpack_append_string_variable_set_command var strvar)
   if (DEFINED ${var})
     list(LENGTH ${var} CPACK_APP_VALUE_LEN)
     if(${CPACK_APP_VALUE_LEN} EQUAL 1)
       string(APPEND ${strvar} "set(${var} \"${${var}}\")\n")
     endif()
+    set(${strvar} "${${strvar}}" PARENT_SCOPE)
   endif ()
-endmacro()
+endfunction()
 
 # Macro that appends a SET command for the given list variable name (var)
 # to the macro named strvar, but only if the variable named "var"
diff --git a/Modules/FetchContent.cmake b/Modules/FetchContent.cmake
index a342aa7..5ca296e 100644
--- a/Modules/FetchContent.cmake
+++ b/Modules/FetchContent.cmake
@@ -193,6 +193,11 @@
       ``OVERRIDE_FIND_PACKAGE`` cannot be used when ``FIND_PACKAGE_ARGS`` is
       given.
 
+      :ref:`dependency_providers` discusses another way that
+      :command:`FetchContent_MakeAvailable` calls can be redirected.
+      ``FIND_PACKAGE_ARGS`` is intended for project control, whereas
+      dependency providers allow users to override project behavior.
+
     ``OVERRIDE_FIND_PACKAGE``
       When a ``FetchContent_Declare(<name> ...)`` call includes this option,
       subsequent calls to ``find_package(<name> ...)`` will ensure that
@@ -204,6 +209,13 @@
       satisfy the package requirements of the latter.  ``FIND_PACKAGE_ARGS``
       cannot be used when ``OVERRIDE_FIND_PACKAGE`` is given.
 
+      If a :ref:`dependency provider <dependency_providers>` has been set
+      and the project calls :command:`find_package` for the ``<name>``
+      dependency, ``OVERRIDE_FIND_PACKAGE`` will not prevent the provider
+      from seeing that call.  Dependency providers always have the opportunity
+      to intercept any direct call to :command:`find_package`, except if that
+      call contains the ``BYPASS_PROVIDER`` option.
+
 .. command:: FetchContent_MakeAvailable
 
   .. versionadded:: 3.14
@@ -217,17 +229,35 @@
   :command:`FetchContent_Declare` for each dependency, and the first such call
   will control how that dependency will be made available, as described below.
 
-  .. versionadded:: 3.24
-    If permitted, :command:`find_package(<name> [<args>...]) <find_package>`
-    will be called, where ``<args>...`` may be provided by the
-    ``FIND_PACKAGE_ARGS`` option in :command:`FetchContent_Declare`.
-    The value of the :variable:`FETCHCONTENT_TRY_FIND_PACKAGE_MODE` variable
-    at the time :command:`FetchContent_Declare` was called determines whether
-    ``FetchContent_MakeAvailable()`` can call :command:`find_package`.
+  If ``<lowercaseName>_SOURCE_DIR`` is not set:
 
-  If :command:`find_package` was unsuccessful or was not allowed to be called,
-  ``FetchContent_MakeAvailable()`` then uses the following logic to make the
-  dependency available:
+  * .. versionadded:: 3.24
+
+      If a :ref:`dependency provider <dependency_providers>` is set, call the
+      provider's command with ``FETCHCONTENT_MAKEAVAILABLE_SERIAL`` as the
+      first argument, followed by the arguments of the first call to
+      :command:`FetchContent_Declare` for ``<name>``.  If ``SOURCE_DIR`` or
+      ``BINARY_DIR`` were not part of the original declared arguments, they
+      will be added with their default values.
+      If :variable:`FETCHCONTENT_TRY_FIND_PACKAGE_MODE` was set to ``NEVER``
+      when the details were declared, any ``FIND_PACKAGE_ARGS`` will be
+      omitted.  The ``OVERRIDE_FIND_PACKAGE`` keyword is also always omitted.
+      If the provider fulfilled the request, ``FetchContent_MakeAvailable()``
+      will consider that dependency handled, skip the remaining steps below
+      and move on to the next dependency in the list.
+
+  * .. versionadded:: 3.24
+
+      If permitted, :command:`find_package(<name> [<args>...]) <find_package>`
+      will be called, where ``<args>...`` may be provided by the
+      ``FIND_PACKAGE_ARGS`` option in :command:`FetchContent_Declare`.
+      The value of the :variable:`FETCHCONTENT_TRY_FIND_PACKAGE_MODE` variable
+      at the time :command:`FetchContent_Declare` was called determines whether
+      ``FetchContent_MakeAvailable()`` can call :command:`find_package`.
+
+  If the dependency was not satisfied by a provider or a
+  :command:`find_package` call, ``FetchContent_MakeAvailable()`` then uses
+  the following logic to make the dependency available:
 
   * If the dependency has already been populated earlier in this run, set
     the ``<lowercaseName>_POPULATED``, ``<lowercaseName>_SOURCE_DIR`` and
@@ -468,7 +498,7 @@
   When using saved content details, a call to
   :command:`FetchContent_MakeAvailable` or :command:`FetchContent_Populate`
   records information in global properties which can be queried at any time.
-  This information includes the source and binary directories associated with
+  This information may include the source and binary directories associated with
   the content and also whether or not the content population has been processed
   during the current configure run.
 
@@ -488,6 +518,8 @@
   set the same variables as a call to
   :command:`FetchContent_MakeAvailable(name) <FetchContent_MakeAvailable>` or
   :command:`FetchContent_Populate(name) <FetchContent_Populate>`.
+  Note that the ``SOURCE_DIR`` and ``BINARY_DIR`` values can be empty if the
+  call is fulfilled by a :ref:`dependency provider <dependency_providers>`.
 
   This command is rarely needed when using
   :command:`FetchContent_MakeAvailable`.  It is more commonly used as part of
@@ -511,6 +543,33 @@
       add_subdirectory(${depname_SOURCE_DIR} ${depname_BINARY_DIR})
     endif()
 
+.. command:: FetchContent_SetPopulated
+
+  .. versionadded:: 3.24
+
+  .. note::
+    This command should only be called by
+    :ref:`dependency providers <dependency_providers>`.  Calling it in any
+    other context is unsupported and future CMake versions may halt with a
+    fatal error in such cases.
+
+  .. code-block:: cmake
+
+    FetchContent_SetPopulated(
+      <name>
+      [SOURCE_DIR <srcDir>]
+      [BINARY_DIR <binDir>]
+    )
+
+  If a provider command fulfills a ``FETCHCONTENT_MAKEAVAILABLE_SERIAL``
+  request, it must call this function before returning.  The ``SOURCE_DIR``
+  and ``BINARY_DIR`` arguments can be used to specify the values that
+  :command:`FetchContent_GetProperties` should return for its corresponding
+  arguments.  Only provide ``SOURCE_DIR`` and ``BINARY_DIR`` if they have
+  the same meaning as if they had been populated by the built-in
+  :command:`FetchContent_MakeAvailable` implementation.
+
+
 Variables
 ^^^^^^^^^
 
@@ -588,7 +647,7 @@
     behavior if ``FETCHCONTENT_TRY_FIND_PACKAGE_MODE`` is not set.
 
   ``ALWAYS``
-    :command:`find_package` will be called by
+    :command:`find_package` can be called by
     :command:`FetchContent_MakeAvailable` regardless of whether the
     :command:`FetchContent_Declare` call included a ``FIND_PACKAGE_ARGS``
     keyword or not.  If no ``FIND_PACKAGE_ARGS`` keyword was given, the
@@ -1099,14 +1158,26 @@
   endif()
 
   set(options "")
-  set(oneValueArgs SVN_REPOSITORY)
+  set(oneValueArgs
+    BINARY_DIR
+    SOURCE_DIR
+    SVN_REPOSITORY
+  )
   set(multiValueArgs "")
 
   cmake_parse_arguments(PARSE_ARGV 1 ARG
     "${options}" "${oneValueArgs}" "${multiValueArgs}")
 
-  unset(srcDirSuffix)
-  unset(svnRepoArgs)
+  string(TOLOWER ${contentName} contentNameLower)
+
+  if(NOT ARG_BINARY_DIR)
+    set(ARG_BINARY_DIR "${FETCHCONTENT_BASE_DIR}/${contentNameLower}-build")
+  endif()
+
+  if(NOT ARG_SOURCE_DIR)
+    set(ARG_SOURCE_DIR "${FETCHCONTENT_BASE_DIR}/${contentNameLower}-src")
+  endif()
+
   if(ARG_SVN_REPOSITORY)
     # Add a hash of the svn repository URL to the source dir. This works
     # around the problem where if the URL changes, the download would
@@ -1116,25 +1187,21 @@
     # problem on windows due to path length limits).
     string(SHA1 urlSHA ${ARG_SVN_REPOSITORY})
     string(SUBSTRING ${urlSHA} 0 7 urlSHA)
-    set(srcDirSuffix "-${urlSHA}")
-    set(svnRepoArgs  SVN_REPOSITORY ${ARG_SVN_REPOSITORY})
+    string(APPEND ARG_SOURCE_DIR "-${urlSHA}")
+    list(PREPEND ARG_UNPARSED_ARGUMENTS SVN_REPOSITORY "${ARG_SVN_REPOSITORY}")
   endif()
 
-  string(TOLOWER ${contentName} contentNameLower)
+  list(PREPEND ARG_UNPARSED_ARGUMENTS
+    SOURCE_DIR "${ARG_SOURCE_DIR}"
+    BINARY_DIR "${ARG_BINARY_DIR}"
+  )
 
   set(__argsQuoted)
   foreach(__item IN LISTS ARG_UNPARSED_ARGUMENTS)
     string(APPEND __argsQuoted " [==[${__item}]==]")
   endforeach()
-  cmake_language(EVAL CODE "
-    __FetchContent_declareDetails(
-      ${contentNameLower}
-      SOURCE_DIR \"${FETCHCONTENT_BASE_DIR}/${contentNameLower}-src${srcDirSuffix}\"
-      BINARY_DIR \"${FETCHCONTENT_BASE_DIR}/${contentNameLower}-build\"
-      \${svnRepoArgs}
-      # List these last so they can override things we set above
-      ${__argsQuoted}
-    )"
+  cmake_language(EVAL CODE
+    "__FetchContent_declareDetails(${contentNameLower} ${__argsQuoted})"
   )
 
 endfunction()
@@ -1145,11 +1212,11 @@
 # The setter also records the source and binary dirs used.
 #=======================================================================
 
-# Internal use, projects must not call this directly. It is intended
-# for use by things like the FetchContent_Populate() function to
-# record when FetchContent_Populate() is called for a particular
-# content name.
-function(__FetchContent_setPopulated contentName)
+# Semi-internal use. Projects must not call this directly. Dependency
+# providers must call it if they satisfy a request made with the
+# FETCHCONTENT_MAKEAVAILABLE_SERIAL method (that is the only permitted
+# place to call it outside of the FetchContent module).
+function(FetchContent_SetPopulated contentName)
 
   cmake_parse_arguments(PARSE_ARGV 1 arg
     ""
@@ -1165,10 +1232,18 @@
 
   set(propertyName "${prefix}_sourceDir")
   define_property(GLOBAL PROPERTY ${propertyName})
+  if("${arg_SOURCE_DIR}" STREQUAL "")
+    # Don't discard a previously provided SOURCE_DIR
+    get_property(arg_SOURCE_DIR GLOBAL PROPERTY ${propertyName})
+  endif()
   set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}")
 
   set(propertyName "${prefix}_binaryDir")
   define_property(GLOBAL PROPERTY ${propertyName})
+  if("${arg_BINARY_DIR}" STREQUAL "")
+    # Don't discard a previously provided BINARY_DIR
+    get_property(arg_BINARY_DIR GLOBAL PROPERTY ${propertyName})
+  endif()
   set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}")
 
   set(propertyName "${prefix}_populated")
@@ -1480,7 +1555,8 @@
   if(${contentNameLower}_POPULATED)
     if("${${contentNameLower}_SOURCE_DIR}" STREQUAL "")
       message(FATAL_ERROR
-        "Content ${contentName} already populated by find_package()"
+        "Content ${contentName} already populated by find_package() or a "
+        "dependency provider"
       )
     else()
       message(FATAL_ERROR
@@ -1584,7 +1660,7 @@
     )
   endif()
 
-  __FetchContent_setPopulated(
+  FetchContent_SetPopulated(
     ${contentName}
     SOURCE_DIR "${${contentNameLower}_SOURCE_DIR}"
     BINARY_DIR "${${contentNameLower}_BINARY_DIR}"
@@ -1654,22 +1730,98 @@
 # calls will be available to the caller.
 macro(FetchContent_MakeAvailable)
 
+  get_property(__cmake_providerCommand GLOBAL PROPERTY
+    __FETCHCONTENT_MAKEAVAILABLE_SERIAL_PROVIDER
+  )
   foreach(__cmake_contentName IN ITEMS ${ARGV})
     string(TOLOWER ${__cmake_contentName} __cmake_contentNameLower)
 
     # If user specified FETCHCONTENT_SOURCE_DIR_... for this dependency, that
-    # overrides everything else and we shouldn't try to use find_package().
+    # overrides everything else and we shouldn't try to use find_package() or
+    # a dependency provider.
     string(TOUPPER ${__cmake_contentName} __cmake_contentNameUpper)
     if("${FETCHCONTENT_SOURCE_DIR_${__cmake_contentNameUpper}}" STREQUAL "")
+      # Dependency provider gets first opportunity, but prevent infinite
+      # recursion if we are called again for the same thing
+      if(NOT "${__cmake_providerCommand}" STREQUAL "" AND
+        NOT DEFINED __cmake_fcProvider_${__cmake_contentNameLower})
+        message(VERBOSE
+          "Trying FETCHCONTENT_MAKEAVAILABLE_SERIAL dependency provider for "
+          "${__cmake_contentName}"
+        )
+        # It's still valid if there are no saved details. The project may have
+        # been written to assume a dependency provider is always set and will
+        # provide dependencies without having any declared details for them.
+        __FetchContent_getSavedDetails(${__cmake_contentName} __cmake_contentDetails)
+        set(__cmake_providerArgs
+          "FETCHCONTENT_MAKEAVAILABLE_SERIAL"
+          "${__cmake_contentName}"
+        )
+        # Empty arguments must be preserved because of things like
+        # GIT_SUBMODULES (see CMP0097)
+        foreach(__cmake_item IN LISTS __cmake_contentDetails)
+          string(APPEND __cmake_providerArgs " [==[${__cmake_item}]==]")
+        endforeach()
+
+        # This property might be defined but empty. As long as it is defined,
+        # find_package() can be called.
+        get_property(__cmake_addfpargs GLOBAL PROPERTY
+          _FetchContent_${contentNameLower}_find_package_args
+          DEFINED
+        )
+        if(__cmake_addfpargs)
+          get_property(__cmake_fpargs GLOBAL PROPERTY
+            _FetchContent_${contentNameLower}_find_package_args
+          )
+          string(APPEND __cmake_providerArgs " FIND_PACKAGE_ARGS")
+          foreach(__cmake_item IN LISTS __cmake_fpargs)
+            string(APPEND __cmake_providerArgs " [==[${__cmake_item}]==]")
+          endforeach()
+        endif()
+
+        # Calling the provider could lead to FetchContent_MakeAvailable() being
+        # called for a nested dependency. That nested call may occur in the
+        # current variable scope. We have to save and restore the variables we
+        # need preserved.
+        list(APPEND __cmake_fcCurrentVarsStack
+          ${__cmake_contentName}
+          ${__cmake_contentNameLower}
+        )
+
+        set(__cmake_fcProvider_${__cmake_contentNameLower} YES)
+        cmake_language(EVAL CODE "${__cmake_providerCommand}(${__cmake_providerArgs})")
+        unset(__cmake_fcProvider_${__cmake_contentNameLower})
+
+        list(POP_BACK __cmake_fcCurrentVarsStack
+          __cmake_contentNameLower
+          __cmake_contentName
+        )
+
+        unset(__cmake_providerArgs)
+        unset(__cmake_addfpargs)
+        unset(__cmake_fpargs)
+        unset(__cmake_item)
+        unset(__cmake_contentDetails)
+
+        FetchContent_GetProperties(${__cmake_contentName})
+        if(${__cmake_contentNameLower}_POPULATED)
+          continue()
+        endif()
+      endif()
+
       # Check if we've been asked to try find_package() first, even if we
       # have already populated this dependency. If we previously tried to
       # use find_package() for this and it succeeded, those things might
       # no longer be in scope, so we have to do it again.
-      set(__cmake_fpArgsPropName "_FetchContent_${__cmake_contentNameLower}_find_package_args")
-      get_property(__cmake_haveFpArgs GLOBAL PROPERTY ${__cmake_fpArgsPropName} DEFINED)
+      get_property(__cmake_haveFpArgs GLOBAL PROPERTY
+        _FetchContent_${__cmake_contentNameLower}_find_package_args DEFINED
+      )
       if(__cmake_haveFpArgs)
+        unset(__cmake_haveFpArgs)
         message(VERBOSE "Trying find_package(${__cmake_contentName} ...) before FetchContent")
-        get_property(__cmake_fpArgs GLOBAL PROPERTY ${__cmake_fpArgsPropName})
+        get_property(__cmake_fpArgs GLOBAL PROPERTY
+          _FetchContent_${__cmake_contentNameLower}_find_package_args
+        )
 
         # This call could lead to FetchContent_MakeAvailable() being called for
         # a nested dependency and it may occur in the current variable scope.
@@ -1683,15 +1835,16 @@
           __cmake_contentNameLower
           __cmake_contentName
         )
+        unset(__cmake_fpArgs)
 
         if(${__cmake_contentName}_FOUND)
-          set(${__cmake_contentNameLower}_SOURCE_DIR "")
-          set(${__cmake_contentNameLower}_BINARY_DIR "")
-          set(${__cmake_contentNameLower}_POPULATED  TRUE)
-          __FetchContent_setPopulated(${__cmake_contentName})
+          FetchContent_SetPopulated(${__cmake_contentName})
+          FetchContent_GetProperties(${__cmake_contentName})
           continue()
         endif()
       endif()
+    else()
+      unset(__cmake_haveFpArgs)
     endif()
 
     FetchContent_GetProperties(${__cmake_contentName})
@@ -1731,5 +1884,7 @@
   # clear local variables to prevent leaking into the caller's scope
   unset(__cmake_contentName)
   unset(__cmake_contentNameLower)
+  unset(__cmake_contentNameUpper)
+  unset(__cmake_providerCommand)
 
 endmacro()
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 2deaaaa..95b07cb 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -199,6 +199,7 @@
   cmCustomCommandTypes.h
   cmDefinitions.cxx
   cmDefinitions.h
+  cmDependencyProvider.h
   cmDepends.cxx
   cmDepends.h
   cmDependsC.cxx
diff --git a/Source/CMakeVersion.cmake b/Source/CMakeVersion.cmake
index 3b258cd..b832307 100644
--- a/Source/CMakeVersion.cmake
+++ b/Source/CMakeVersion.cmake
@@ -1,7 +1,7 @@
 # CMake version number components.
 set(CMake_VERSION_MAJOR 3)
 set(CMake_VERSION_MINOR 23)
-set(CMake_VERSION_PATCH 20220524)
+set(CMake_VERSION_PATCH 20220526)
 #set(CMake_VERSION_RC 0)
 set(CMake_VERSION_IS_DIRTY 0)
 
diff --git a/Source/cmCMakeLanguageCommand.cxx b/Source/cmCMakeLanguageCommand.cxx
index 27d8cb8..7d05e88 100644
--- a/Source/cmCMakeLanguageCommand.cxx
+++ b/Source/cmCMakeLanguageCommand.cxx
@@ -13,11 +13,14 @@
 #include <cm/string_view>
 #include <cmext/string_view>
 
+#include "cmArgumentParser.h"
+#include "cmDependencyProvider.h"
 #include "cmExecutionStatus.h"
 #include "cmGlobalGenerator.h"
 #include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmRange.h"
+#include "cmState.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 
@@ -215,6 +218,91 @@
   return makefile.ReadListFileAsString(
     code, cmStrCat(context.FilePath, ":", context.Line, ":EVAL"));
 }
+
+bool cmCMakeLanguageCommandSET_DEPENDENCY_PROVIDER(
+  std::vector<std::string> const& args, cmExecutionStatus& status)
+{
+  cmState* state = status.GetMakefile().GetState();
+  if (!state->InTopLevelIncludes()) {
+    return FatalError(
+      status,
+      "Dependency providers can only be set as part of the first call to "
+      "project(). More specifically, cmake_language(SET_DEPENDENCY_PROVIDER) "
+      "can only be called while the first project() command processes files "
+      "listed in CMAKE_PROJECT_TOP_LEVEL_INCLUDES.");
+  }
+
+  struct SetProviderArgs
+  {
+    std::string Command;
+    std::vector<std::string> Methods;
+  };
+
+  auto const ArgsParser =
+    cmArgumentParser<SetProviderArgs>()
+      .Bind("SET_DEPENDENCY_PROVIDER"_s, &SetProviderArgs::Command)
+      .Bind("SUPPORTED_METHODS"_s, &SetProviderArgs::Methods);
+
+  std::vector<std::string> unparsed;
+  auto parsedArgs = ArgsParser.Parse(args, &unparsed);
+
+  if (!unparsed.empty()) {
+    return FatalError(
+      status, cmStrCat("Unrecognized keyword: \"", unparsed.front(), "\""));
+  }
+
+  // We store the command that FetchContent_MakeAvailable() can call in a
+  // global (but considered internal) property. If the provider doesn't
+  // support this method, we set this property to an empty string instead.
+  // This simplifies the logic in FetchContent_MakeAvailable() and doesn't
+  // require us to define a new internal command or sub-command.
+  std::string fcmasProperty = "__FETCHCONTENT_MAKEAVAILABLE_SERIAL_PROVIDER";
+
+  if (parsedArgs.Command.empty()) {
+    if (!parsedArgs.Methods.empty()) {
+      return FatalError(status,
+                        "Must specify a non-empty command name when provider "
+                        "methods are given");
+    }
+    state->ClearDependencyProvider();
+    state->SetGlobalProperty(fcmasProperty, "");
+    return true;
+  }
+
+  cmState::Command command = state->GetCommand(parsedArgs.Command);
+  if (!command) {
+    return FatalError(status,
+                      cmStrCat("Command \"", parsedArgs.Command,
+                               "\" is not a defined command"));
+  }
+
+  if (parsedArgs.Methods.empty()) {
+    return FatalError(status, "Must specify at least one provider method");
+  }
+
+  bool supportsFetchContentMakeAvailableSerial = false;
+  std::vector<cmDependencyProvider::Method> methods;
+  for (auto const& method : parsedArgs.Methods) {
+    if (method == "FIND_PACKAGE") {
+      methods.emplace_back(cmDependencyProvider::Method::FindPackage);
+    } else if (method == "FETCHCONTENT_MAKEAVAILABLE_SERIAL") {
+      supportsFetchContentMakeAvailableSerial = true;
+      methods.emplace_back(
+        cmDependencyProvider::Method::FetchContentMakeAvailableSerial);
+    } else {
+      return FatalError(
+        status,
+        cmStrCat("Unknown dependency provider method \"", method, "\""));
+    }
+  }
+
+  state->SetDependencyProvider({ parsedArgs.Command, methods });
+  state->SetGlobalProperty(
+    fcmasProperty,
+    supportsFetchContentMakeAvailableSerial ? parsedArgs.Command.c_str() : "");
+
+  return true;
+}
 }
 
 bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
@@ -246,6 +334,11 @@
     return FatalError(status, "called with incorrect number of arguments");
   }
 
+  if (expArgs[expArg] == "SET_DEPENDENCY_PROVIDER"_s) {
+    finishArgs();
+    return cmCMakeLanguageCommandSET_DEPENDENCY_PROVIDER(expArgs, status);
+  }
+
   cm::optional<Defer> maybeDefer;
   if (expArgs[expArg] == "DEFER"_s) {
     ++expArg; // Consume "DEFER".
diff --git a/Source/cmDependencyProvider.h b/Source/cmDependencyProvider.h
new file mode 100644
index 0000000..a6670b4
--- /dev/null
+++ b/Source/cmDependencyProvider.h
@@ -0,0 +1,38 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+class cmDependencyProvider
+{
+public:
+  enum class Method
+  {
+    FindPackage,
+    FetchContentMakeAvailableSerial,
+  };
+
+  cmDependencyProvider(std::string command, std::vector<Method> methods)
+    : Command(std::move(command))
+    , Methods(std::move(methods))
+  {
+  }
+
+  std::string const& GetCommand() const { return this->Command; }
+  std::vector<Method> const& GetMethods() const { return this->Methods; }
+  bool SupportsMethod(Method method) const
+  {
+    return std::find(this->Methods.begin(), this->Methods.end(), method) !=
+      this->Methods.end();
+  }
+
+private:
+  std::string Command;
+  std::vector<Method> Methods;
+};
diff --git a/Source/cmFindPackageCommand.cxx b/Source/cmFindPackageCommand.cxx
index 6586c69..e41d5e4 100644
--- a/Source/cmFindPackageCommand.cxx
+++ b/Source/cmFindPackageCommand.cxx
@@ -23,6 +23,7 @@
 #include "cmsys/String.h"
 
 #include "cmAlgorithms.h"
+#include "cmDependencyProvider.h"
 #include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMessageType.h"
@@ -238,6 +239,8 @@
   const char* components_sep = "";
   std::set<std::string> requiredComponents;
   std::set<std::string> optionalComponents;
+  std::vector<std::pair<std::string, const char*>> componentVarDefs;
+  bool bypassProvider = false;
 
   // Always search directly in a generated path.
   this->SearchPathSuffixes.emplace_back();
@@ -268,6 +271,9 @@
     if (args[i] == "QUIET") {
       this->Quiet = true;
       doing = DoingNone;
+    } else if (args[i] == "BYPASS_PROVIDER") {
+      bypassProvider = true;
+      doing = DoingNone;
     } else if (args[i] == "EXACT") {
       this->VersionExact = true;
       doing = DoingNone;
@@ -356,7 +362,7 @@
       }
 
       std::string req_var = this->Name + "_FIND_REQUIRED_" + args[i];
-      this->AddFindDefinition(req_var, isRequired);
+      componentVarDefs.emplace_back(req_var, isRequired);
 
       // Append to the list of required components.
       components += components_sep;
@@ -408,7 +414,7 @@
     return false;
   }
 
-  // Maybe choose one mode exclusively.
+  // Check and eliminate search modes not allowed by the args provided
   this->UseFindModules = configArgs.empty();
   this->UseConfigFiles = moduleArgs.empty();
   if (!this->UseFindModules && !this->UseConfigFiles) {
@@ -543,6 +549,48 @@
     return true;
   }
 
+  // Now choose what method(s) we will use to satisfy the request. Note that
+  // we still want all the above checking of arguments, etc. regardless of the
+  // method used. This will ensure ill-formed arguments are caught earlier,
+  // before things like dependency providers need to deal with them.
+
+  // A dependency provider (if set) gets first look before other methods.
+  // We do this before modifying the package root path stack because a
+  // provider might use methods that ignore that.
+  cmState* state = this->Makefile->GetState();
+  cmState::Command providerCommand = state->GetDependencyProviderCommand(
+    cmDependencyProvider::Method::FindPackage);
+  if (bypassProvider) {
+    if (this->DebugMode && providerCommand) {
+      this->DebugMessage(
+        "BYPASS_PROVIDER given, skipping dependency provider");
+    }
+  } else if (providerCommand) {
+    if (this->DebugMode) {
+      this->DebugMessage(cmStrCat("Trying dependency provider command: ",
+                                  state->GetDependencyProvider()->GetCommand(),
+                                  "()"));
+    }
+    std::vector<cmListFileArgument> listFileArgs(args.size() + 1);
+    listFileArgs[0] =
+      cmListFileArgument("FIND_PACKAGE", cmListFileArgument::Unquoted, 0);
+    std::transform(args.begin(), args.end(), listFileArgs.begin() + 1,
+                   [](const std::string& arg) {
+                     return cmListFileArgument(arg,
+                                               cmListFileArgument::Bracket, 0);
+                   });
+    if (!providerCommand(listFileArgs, this->Status)) {
+      return false;
+    }
+    if (this->Makefile->IsOn(cmStrCat(this->Name, "_FOUND"))) {
+      if (this->DebugMode) {
+        this->DebugMessage("Package was found by the dependency provider");
+      }
+      this->AppendSuccessInformation();
+      return true;
+    }
+  }
+
   {
     // Allocate a PACKAGE_ROOT_PATH for the current find_package call.
     this->Makefile->FindPackageRootPathStack.emplace_back();
@@ -573,7 +621,7 @@
     }
   }
 
-  this->SetModuleVariables(components);
+  this->SetModuleVariables(components, componentVarDefs);
 
   // See if we have been told to delegate to FetchContent or some other
   // redirected config package first. We have to check all names that
@@ -697,6 +745,12 @@
 
   this->AppendSuccessInformation();
 
+  // Restore original state of "_FIND_" variables set in SetModuleVariables()
+  this->RestoreFindDefinitions();
+
+  // Pop the package stack
+  this->Makefile->FindPackageRootPathStack.pop_back();
+
   if (!this->DebugBuffer.empty()) {
     this->DebugMessage(this->DebugBuffer);
   }
@@ -778,13 +832,18 @@
   addDefinition(prefix + "_COUNT", buf);
 }
 
-void cmFindPackageCommand::SetModuleVariables(const std::string& components)
+void cmFindPackageCommand::SetModuleVariables(
+  const std::string& components,
+  const std::vector<std::pair<std::string, const char*>>& componentVarDefs)
 {
   this->AddFindDefinition("CMAKE_FIND_PACKAGE_NAME", this->Name);
 
-  // Store the list of components.
+  // Store the list of components and associated variable definitions
   std::string components_var = this->Name + "_FIND_COMPONENTS";
   this->AddFindDefinition(components_var, components);
+  for (const auto& varDef : componentVarDefs) {
+    this->AddFindDefinition(varDef.first, varDef.second);
+  }
 
   if (this->Quiet) {
     // Tell the module that is about to be read that it should find
@@ -1388,12 +1447,6 @@
     this->Makefile->GetState()->SetGlobalProperty(requiredInfoPropName,
                                                   "REQUIRED");
   }
-
-  // Restore original state of "_FIND_" variables we set.
-  this->RestoreFindDefinitions();
-
-  // Pop the package stack
-  this->Makefile->FindPackageRootPathStack.pop_back();
 }
 
 inline std::size_t collectPathsForDebug(std::string& buffer,
diff --git a/Source/cmFindPackageCommand.h b/Source/cmFindPackageCommand.h
index 902fa32..80fd8f8 100644
--- a/Source/cmFindPackageCommand.h
+++ b/Source/cmFindPackageCommand.h
@@ -9,6 +9,7 @@
 #include <map>
 #include <set>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include <cm/string_view>
@@ -97,7 +98,9 @@
     const std::string& prefix, const std::string& version, unsigned int count,
     unsigned int major, unsigned int minor, unsigned int patch,
     unsigned int tweak);
-  void SetModuleVariables(const std::string& components);
+  void SetModuleVariables(
+    const std::string& components,
+    const std::vector<std::pair<std::string, const char*>>& componentVarDefs);
   bool FindModule(bool& found);
   void AddFindDefinition(const std::string& var, cm::string_view value);
   void RestoreFindDefinitions();
diff --git a/Source/cmGlobalGenerator.cxx b/Source/cmGlobalGenerator.cxx
index 4d636e4..9d61de9 100644
--- a/Source/cmGlobalGenerator.cxx
+++ b/Source/cmGlobalGenerator.cxx
@@ -690,6 +690,7 @@
     }
 
     // One-time includes of user-provided project setup files
+    mf->GetState()->SetInTopLevelIncludes(true);
     std::string includes =
       mf->GetSafeDefinition("CMAKE_PROJECT_TOP_LEVEL_INCLUDES");
     std::vector<std::string> includesList = cmExpandedList(includes);
@@ -700,22 +701,26 @@
         cmSystemTools::Error(
           "CMAKE_PROJECT_TOP_LEVEL_INCLUDES file does not exist: " +
           setupFile);
+        mf->GetState()->SetInTopLevelIncludes(false);
         return;
       }
       if (cmSystemTools::FileIsDirectory(absSetupFile)) {
         cmSystemTools::Error(
           "CMAKE_PROJECT_TOP_LEVEL_INCLUDES file is a directory: " +
           setupFile);
+        mf->GetState()->SetInTopLevelIncludes(false);
         return;
       }
       if (!mf->ReadListFile(absSetupFile)) {
         cmSystemTools::Error(
           "Failed reading CMAKE_PROJECT_TOP_LEVEL_INCLUDES file: " +
           setupFile);
+        mf->GetState()->SetInTopLevelIncludes(false);
         return;
       }
     }
   }
+  mf->GetState()->SetInTopLevelIncludes(false);
 
   // Check that the languages are supported by the generator and its
   // native build tool found above.
diff --git a/Source/cmState.cxx b/Source/cmState.cxx
index f1144e1..b753373 100644
--- a/Source/cmState.cxx
+++ b/Source/cmState.cxx
@@ -1072,3 +1072,12 @@
 
   return flag;
 }
+
+cmState::Command cmState::GetDependencyProviderCommand(
+  cmDependencyProvider::Method method) const
+{
+  return (this->DependencyProvider &&
+          this->DependencyProvider->SupportsMethod(method))
+    ? this->GetCommand(this->DependencyProvider->GetCommand())
+    : Command{};
+}
diff --git a/Source/cmState.h b/Source/cmState.h
index ee133fc..2d0c521 100644
--- a/Source/cmState.h
+++ b/Source/cmState.h
@@ -8,11 +8,16 @@
 #include <memory>
 #include <set>
 #include <string>
+#include <type_traits>
 #include <unordered_map>
 #include <unordered_set>
+#include <utility>
 #include <vector>
 
+#include <cm/optional>
+
 #include "cmDefinitions.h"
+#include "cmDependencyProvider.h"
 #include "cmLinkedTree.h"
 #include "cmPolicies.h"
 #include "cmProperty.h"
@@ -227,6 +232,24 @@
 
   ProjectKind GetProjectKind() const;
 
+  void ClearDependencyProvider() { this->DependencyProvider.reset(); }
+  void SetDependencyProvider(cmDependencyProvider provider)
+  {
+    this->DependencyProvider = std::move(provider);
+  }
+  cm::optional<cmDependencyProvider> const& GetDependencyProvider() const
+  {
+    return this->DependencyProvider;
+  }
+  Command GetDependencyProviderCommand(
+    cmDependencyProvider::Method method) const;
+
+  void SetInTopLevelIncludes(bool inTopLevelIncludes)
+  {
+    this->ProcessingTopLevelIncludes = inTopLevelIncludes;
+  }
+  bool InTopLevelIncludes() const { return this->ProcessingTopLevelIncludes; }
+
 private:
   friend class cmake;
   void AddCacheEntry(const std::string& key, const char* value,
@@ -288,4 +311,6 @@
   bool NinjaMulti = false;
   Mode StateMode = Unknown;
   ProjectKind StateProjectKind = ProjectKind::Normal;
+  cm::optional<cmDependencyProvider> DependencyProvider;
+  bool ProcessingTopLevelIncludes = false;
 };
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index 4fe6ac1..da91e64 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -461,6 +461,7 @@
 add_RunCMake_test(option)
 add_RunCMake_test(project -DCMake_TEST_RESOURCES=${CMake_TEST_RESOURCES})
 add_RunCMake_test(project_injected)
+add_RunCMake_test(DependencyProviders)
 add_RunCMake_test(return)
 add_RunCMake_test(separate_arguments)
 add_RunCMake_test(set_property)
diff --git a/Tests/RunCMake/DependencyProviders/AfterProject-result.txt b/Tests/RunCMake/DependencyProviders/AfterProject-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/AfterProject-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/AfterProject-stderr.txt b/Tests/RunCMake/DependencyProviders/AfterProject-stderr.txt
new file mode 100644
index 0000000..7bee23c
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/AfterProject-stderr.txt
@@ -0,0 +1,6 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Dependency providers can only be set as part of the first
+  call to project\(\)\.  More specifically,
+  cmake_language\(SET_DEPENDENCY_PROVIDER\) can only be called while the first
+  project\(\) command processes files listed in
+  CMAKE_PROJECT_TOP_LEVEL_INCLUDES\.
diff --git a/Tests/RunCMake/DependencyProviders/BeforeProject-result.txt b/Tests/RunCMake/DependencyProviders/BeforeProject-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/BeforeProject-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/BeforeProject-stderr.txt b/Tests/RunCMake/DependencyProviders/BeforeProject-stderr.txt
new file mode 100644
index 0000000..7bee23c
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/BeforeProject-stderr.txt
@@ -0,0 +1,6 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Dependency providers can only be set as part of the first
+  call to project\(\)\.  More specifically,
+  cmake_language\(SET_DEPENDENCY_PROVIDER\) can only be called while the first
+  project\(\) command processes files listed in
+  CMAKE_PROJECT_TOP_LEVEL_INCLUDES\.
diff --git a/Tests/RunCMake/DependencyProviders/Bypass-stdout.txt b/Tests/RunCMake/DependencyProviders/Bypass-stdout.txt
new file mode 100644
index 0000000..b0c7e6e
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/Bypass-stdout.txt
@@ -0,0 +1,7 @@
+-- Before cmake_language
+-- After cmake_language
+-- Forwarding find_package\(SomeDep\)
+-- Provider invoked for method FIND_PACKAGE with args: QUIET;REQUIRED
+-- SomeDepConfig\.cmake was used
+-- Leaving provider
+-- Configuring done
diff --git a/Tests/RunCMake/DependencyProviders/Bypass.cmake b/Tests/RunCMake/DependencyProviders/Bypass.cmake
new file mode 100644
index 0000000..883087e
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/Bypass.cmake
@@ -0,0 +1 @@
+find_package(SomeDep QUIET REQUIRED)
diff --git a/Tests/RunCMake/DependencyProviders/CMakeLists.txt b/Tests/RunCMake/DependencyProviders/CMakeLists.txt
new file mode 100644
index 0000000..3552604
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/CMakeLists.txt
@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 3.23...3.24)
+
+if(DEFINED include_before_project)
+  include("${include_before_project}")
+endif()
+
+project(${RunCMake_TEST} NONE)
+
+if(DEFINED include_after_project)
+  include("${include_after_project}")
+endif()
+
+include(${RunCMake_TEST}.cmake OPTIONAL)
diff --git a/Tests/RunCMake/DependencyProviders/ConfigFiles/SomeDepConfig.cmake b/Tests/RunCMake/DependencyProviders/ConfigFiles/SomeDepConfig.cmake
new file mode 100644
index 0000000..e04eefe
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ConfigFiles/SomeDepConfig.cmake
@@ -0,0 +1,2 @@
+message(STATUS "SomeDepConfig.cmake was used")
+set(SomeDep_FOUND TRUE)
diff --git a/Tests/RunCMake/DependencyProviders/FetchContentSerial-stdout.txt b/Tests/RunCMake/DependencyProviders/FetchContentSerial-stdout.txt
new file mode 100644
index 0000000..fa4a794
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/FetchContentSerial-stdout.txt
@@ -0,0 +1,7 @@
+-- Before cmake_language
+-- After cmake_language
+-- AThing_FOUND = 0
+-- Intercepted FetchContent_MakeAvailable\(SomeDep\)
+-- Provider invoked for method FETCHCONTENT_MAKEAVAILABLE_SERIAL with args: SOURCE_DIR;.*/Tests/RunCMake/DependencyProviders;BINARY_DIR;.*/Tests/RunCMake/DependencyProviders/FetchContentSerial-build/_deps/somedep-build;SOURCE_SUBDIR;DoesNotExist
+-- FetchContent_MakeAvailable\(\) succeeded
+-- Configuring done
diff --git a/Tests/RunCMake/DependencyProviders/FetchContentSerial.cmake b/Tests/RunCMake/DependencyProviders/FetchContentSerial.cmake
new file mode 100644
index 0000000..cbd3010
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/FetchContentSerial.cmake
@@ -0,0 +1 @@
+include(try_methods.cmake)
diff --git a/Tests/RunCMake/DependencyProviders/FindPackage-stdout.txt b/Tests/RunCMake/DependencyProviders/FindPackage-stdout.txt
new file mode 100644
index 0000000..19c88b9
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/FindPackage-stdout.txt
@@ -0,0 +1,7 @@
+-- Before cmake_language
+-- After cmake_language
+-- Intercepted find_package\(AThing\)
+-- Provider invoked for method FIND_PACKAGE with args: QUIET
+-- AThing_FOUND = TRUE
+-- FetchContent_MakeAvailable\(\) succeeded
+-- Configuring done
diff --git a/Tests/RunCMake/DependencyProviders/FindPackage.cmake b/Tests/RunCMake/DependencyProviders/FindPackage.cmake
new file mode 100644
index 0000000..cbd3010
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/FindPackage.cmake
@@ -0,0 +1 @@
+include(try_methods.cmake)
diff --git a/Tests/RunCMake/DependencyProviders/NoCommand-result.txt b/Tests/RunCMake/DependencyProviders/NoCommand-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/NoCommand-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/NoCommand-stderr.txt b/Tests/RunCMake/DependencyProviders/NoCommand-stderr.txt
new file mode 100644
index 0000000..a43222f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/NoCommand-stderr.txt
@@ -0,0 +1,3 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Must specify a non-empty command name when provider methods
+  are given
diff --git a/Tests/RunCMake/DependencyProviders/NoCommandOrMethods-stdout.txt b/Tests/RunCMake/DependencyProviders/NoCommandOrMethods-stdout.txt
new file mode 100644
index 0000000..c53435b
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/NoCommandOrMethods-stdout.txt
@@ -0,0 +1,3 @@
+-- Before cmake_language
+-- After cmake_language
+-- AThing_FOUND = 0
diff --git a/Tests/RunCMake/DependencyProviders/NoCommandOrMethods.cmake b/Tests/RunCMake/DependencyProviders/NoCommandOrMethods.cmake
new file mode 100644
index 0000000..bde0cf8
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/NoCommandOrMethods.cmake
@@ -0,0 +1,3 @@
+# Force the provider to be invoked
+find_package(AThing QUIET)
+message(STATUS "AThing_FOUND = ${AThing_FOUND}")
diff --git a/Tests/RunCMake/DependencyProviders/NoMethods-result.txt b/Tests/RunCMake/DependencyProviders/NoMethods-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/NoMethods-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/NoMethods-stderr.txt b/Tests/RunCMake/DependencyProviders/NoMethods-stderr.txt
new file mode 100644
index 0000000..6968851
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/NoMethods-stderr.txt
@@ -0,0 +1,2 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Must specify at least one provider method
diff --git a/Tests/RunCMake/DependencyProviders/PassThroughProvider-stdout.txt b/Tests/RunCMake/DependencyProviders/PassThroughProvider-stdout.txt
new file mode 100644
index 0000000..0c9303a
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/PassThroughProvider-stdout.txt
@@ -0,0 +1,7 @@
+-- Before cmake_language
+-- After cmake_language
+-- Null provider called
+-- Provider invoked for method FIND_PACKAGE with args: AThing;QUIET
+-- AThing_FOUND = 0
+-- Null provider called
+-- Provider invoked for method FETCHCONTENT_MAKEAVAILABLE_SERIAL with args: SomeDep;SOURCE_DIR;.*/Tests/RunCMake/DependencyProviders;BINARY_DIR;.*/Tests/RunCMake/DependencyProviders/PassThroughProvider-build/_deps/somedep-build;SOURCE_SUBDIR;DoesNotExist
diff --git a/Tests/RunCMake/DependencyProviders/PassThroughProvider.cmake b/Tests/RunCMake/DependencyProviders/PassThroughProvider.cmake
new file mode 100644
index 0000000..cbd3010
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/PassThroughProvider.cmake
@@ -0,0 +1 @@
+include(try_methods.cmake)
diff --git a/Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-result.txt b/Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-stderr.txt b/Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-stderr.txt
new file mode 100644
index 0000000..7bee23c
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-stderr.txt
@@ -0,0 +1,6 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Dependency providers can only be set as part of the first
+  call to project\(\)\.  More specifically,
+  cmake_language\(SET_DEPENDENCY_PROVIDER\) can only be called while the first
+  project\(\) command processes files listed in
+  CMAKE_PROJECT_TOP_LEVEL_INCLUDES\.
diff --git a/Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-result.txt b/Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-stderr.txt b/Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-stderr.txt
new file mode 100644
index 0000000..7bee23c
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-stderr.txt
@@ -0,0 +1,6 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Dependency providers can only be set as part of the first
+  call to project\(\)\.  More specifically,
+  cmake_language\(SET_DEPENDENCY_PROVIDER\) can only be called while the first
+  project\(\) command processes files listed in
+  CMAKE_PROJECT_TOP_LEVEL_INCLUDES\.
diff --git a/Tests/RunCMake/DependencyProviders/Recurse-stdout.txt b/Tests/RunCMake/DependencyProviders/Recurse-stdout.txt
new file mode 100644
index 0000000..2c2035a
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/Recurse-stdout.txt
@@ -0,0 +1,7 @@
+-- Before cmake_language
+-- After cmake_language
+-- Intercepted FetchContent_MakeAvailable\(SomeDep\)
+-- Provider invoked for method FETCHCONTENT_MAKEAVAILABLE_SERIAL with args: SOURCE_DIR;.*/Tests/RunCMake/DependencyProviders/Recurse-build/_deps/somedep-src;BINARY_DIR;.*/Tests/RunCMake/DependencyProviders/Recurse-build/_deps/somedep-build;DOWNLOAD_COMMAND;.*/cmake(\.exe)?;-E;echo;Download command called
+.*Download command called
+.*-- Should now be handled
+-- Configuring done
diff --git a/Tests/RunCMake/DependencyProviders/Recurse.cmake b/Tests/RunCMake/DependencyProviders/Recurse.cmake
new file mode 100644
index 0000000..3a79d9c
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/Recurse.cmake
@@ -0,0 +1,8 @@
+include(FetchContent)
+
+set(FETCHCONTENT_QUIET NO)
+
+FetchContent_Declare(SomeDep
+  DOWNLOAD_COMMAND ${CMAKE_COMMAND} -E echo "Download command called"
+)
+FetchContent_MakeAvailable(SomeDep)
diff --git a/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-result.txt b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stderr.txt b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stderr.txt
new file mode 100644
index 0000000..047a64b
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stderr.txt
@@ -0,0 +1,11 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(find_package\):
+  Could not find a package configuration file provided by "SomeDep" with any
+  of the following names:
+
+    SomeDepConfig\.cmake
+    somedep-config\.cmake
+
+  Add the installation prefix of "SomeDep" to CMAKE_PREFIX_PATH or set
+  "SomeDep_DIR" to a directory containing one of the above files\.  If
+  "SomeDep" provides a separate development package or SDK, be sure it has
+  been installed\.
diff --git a/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stdout.txt b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stdout.txt
new file mode 100644
index 0000000..a293324
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stdout.txt
@@ -0,0 +1,5 @@
+-- Before cmake_language
+-- After cmake_language
+-- AThing_FOUND = 0
+-- Redirecting FetchContent_MakeAvailable\(SomeDep\) to find_package\(\)
+-- Provider invoked for method FETCHCONTENT_MAKEAVAILABLE_SERIAL with args: SOURCE_DIR;.*/Tests/RunCMake/DependencyProviders;BINARY_DIR;.*/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-build/_deps/somedep-build;SOURCE_SUBDIR;DoesNotExist
diff --git a/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial.cmake b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial.cmake
new file mode 100644
index 0000000..cbd3010
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial.cmake
@@ -0,0 +1 @@
+include(try_methods.cmake)
diff --git a/Tests/RunCMake/DependencyProviders/RedirectFindPackage-stdout.txt b/Tests/RunCMake/DependencyProviders/RedirectFindPackage-stdout.txt
new file mode 100644
index 0000000..23e751d
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RedirectFindPackage-stdout.txt
@@ -0,0 +1,7 @@
+-- Before cmake_language
+-- After cmake_language
+-- Redirecting find_package\(AThing\) to FetchContent_MakeAvailable\(\)
+-- Provider invoked for method FIND_PACKAGE with args: QUIET
+-- AThing_FOUND = TRUE
+-- FetchContent_MakeAvailable\(\) succeeded
+-- Configuring done
diff --git a/Tests/RunCMake/DependencyProviders/RedirectFindPackage.cmake b/Tests/RunCMake/DependencyProviders/RedirectFindPackage.cmake
new file mode 100644
index 0000000..cbd3010
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RedirectFindPackage.cmake
@@ -0,0 +1 @@
+include(try_methods.cmake)
diff --git a/Tests/RunCMake/DependencyProviders/RunCMakeTest.cmake b/Tests/RunCMake/DependencyProviders/RunCMakeTest.cmake
new file mode 100644
index 0000000..42893d2
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/RunCMakeTest.cmake
@@ -0,0 +1,73 @@
+include(RunCMake)
+
+run_cmake_with_options(BeforeProject
+  -D "include_before_project=set_provider.cmake"
+  -D "provider_command=null_provider"
+  -D "provider_methods=find_package"
+)
+run_cmake_with_options(AfterProject
+  -D "include_after_project=set_provider.cmake"
+  -D "provider_command=null_provider"
+  -D "provider_methods=find_package"
+)
+run_cmake_with_options(ProjectIncludeBefore
+  -D "CMAKE_PROJECT_INCLUDE_BEFORE=set_provider.cmake"
+  -D "provider_command=null_provider"
+  -D "provider_methods=find_package"
+)
+run_cmake_with_options(ProjectIncludeAfter
+  -D "CMAKE_PROJECT_INCLUDE=set_provider.cmake"
+  -D "provider_command=null_provider"
+  -D "provider_methods=find_package"
+)
+run_cmake_with_options(ToolchainFile
+  -D "CMAKE_TOOLCHAIN_FILE=set_provider.cmake"
+  -D "provider_command=null_provider"
+  -D "provider_methods=find_package"
+)
+run_cmake_with_options(NoCommand
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_methods=find_package"
+)
+run_cmake_with_options(NoMethods
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=null_provider"
+)
+run_cmake_with_options(NoCommandOrMethods
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+)
+run_cmake_with_options(PassThroughProvider
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=null_provider"
+  -D "provider_methods=FIND_PACKAGE\\;FETCHCONTENT_MAKEAVAILABLE_SERIAL"
+)
+run_cmake_with_options(FindPackage
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=find_package_provider"
+  -D "provider_methods=FIND_PACKAGE"
+)
+run_cmake_with_options(RedirectFindPackage
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=redirect_find_package_provider"
+  -D "provider_methods=FIND_PACKAGE"
+)
+run_cmake_with_options(FetchContentSerial
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=FetchContentSerial_provider"
+  -D "provider_methods=FETCHCONTENT_MAKEAVAILABLE_SERIAL"
+)
+run_cmake_with_options(RedirectFetchContentSerial
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=redirect_FetchContentSerial_provider"
+  -D "provider_methods=FETCHCONTENT_MAKEAVAILABLE_SERIAL"
+)
+run_cmake_with_options(Bypass
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=forward_find_package"
+  -D "provider_methods=FIND_PACKAGE"
+)
+run_cmake_with_options(Recurse
+  -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=set_provider.cmake"
+  -D "provider_command=recurse_FetchContent"
+  -D "provider_methods=FETCHCONTENT_MAKEAVAILABLE_SERIAL"
+)
diff --git a/Tests/RunCMake/DependencyProviders/ToolchainFile-result.txt b/Tests/RunCMake/DependencyProviders/ToolchainFile-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ToolchainFile-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/DependencyProviders/ToolchainFile-stderr.txt b/Tests/RunCMake/DependencyProviders/ToolchainFile-stderr.txt
new file mode 100644
index 0000000..7bee23c
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/ToolchainFile-stderr.txt
@@ -0,0 +1,6 @@
+CMake Error at set_provider\.cmake:[0-9]+ \(cmake_language\):
+  cmake_language Dependency providers can only be set as part of the first
+  call to project\(\)\.  More specifically,
+  cmake_language\(SET_DEPENDENCY_PROVIDER\) can only be called while the first
+  project\(\) command processes files listed in
+  CMAKE_PROJECT_TOP_LEVEL_INCLUDES\.
diff --git a/Tests/RunCMake/DependencyProviders/set_provider.cmake b/Tests/RunCMake/DependencyProviders/set_provider.cmake
new file mode 100644
index 0000000..6e82b8f
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/set_provider.cmake
@@ -0,0 +1,64 @@
+include(FetchContent)
+
+macro(null_provider method)
+  message(STATUS "Null provider called")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+endmacro()
+
+macro(find_package_provider method package_name)
+  message(STATUS "Intercepted find_package(${package_name})")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+  set(${package_name}_FOUND TRUE)
+endmacro()
+
+macro(FetchContentSerial_provider method dep_name)
+  message(STATUS "Intercepted FetchContent_MakeAvailable(${dep_name})")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+  FetchContent_SetPopulated(${dep_name})
+endmacro()
+
+macro(redirect_find_package_provider method package_name)
+  message(STATUS "Redirecting find_package(${package_name}) to FetchContent_MakeAvailable()")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+  FetchContent_Declare(${package_name}
+    SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}
+    SOURCE_SUBDIR DoesNotExist
+  )
+  FetchContent_MakeAvailable(${package_name})
+  set(${package_name}_FOUND TRUE)
+endmacro()
+
+macro(redirect_FetchContentSerial_provider method dep_name)
+  message(STATUS "Redirecting FetchContent_MakeAvailable(${dep_name}) to find_package()")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+  find_package(${dep_name} NO_DEFAULT_PATH
+    PATHS ${CMAKE_CURRENT_LIST_DIR}/Finders
+    REQUIRED
+  )
+  FetchContent_SetPopulated(${dep_name})
+endmacro()
+
+macro(forward_find_package method package_name)
+  message(STATUS "Forwarding find_package(${package_name})")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+  find_package(${package_name}
+    BYPASS_PROVIDER
+    PATHS ${CMAKE_CURRENT_LIST_DIR}/ConfigFiles
+    ${ARGN}
+  )
+  message(STATUS "Leaving provider")
+endmacro()
+
+macro(recurse_FetchContent method dep_name)
+  message(STATUS "Intercepted FetchContent_MakeAvailable(${dep_name})")
+  message(STATUS "Provider invoked for method ${method} with args: ${ARGN}")
+  FetchContent_MakeAvailable(${dep_name})
+  message(STATUS "Should now be handled")
+endmacro()
+
+message(STATUS "Before cmake_language")
+cmake_language(
+  SET_DEPENDENCY_PROVIDER ${provider_command}
+  SUPPORTED_METHODS ${provider_methods}
+)
+message(STATUS "After cmake_language")
diff --git a/Tests/RunCMake/DependencyProviders/try_methods.cmake b/Tests/RunCMake/DependencyProviders/try_methods.cmake
new file mode 100644
index 0000000..652c32d
--- /dev/null
+++ b/Tests/RunCMake/DependencyProviders/try_methods.cmake
@@ -0,0 +1,12 @@
+# Force the provider to be invoked for each method
+find_package(AThing QUIET)
+message(STATUS "AThing_FOUND = ${AThing_FOUND}")
+
+# These declared details should always succeed when used
+include(FetchContent)
+FetchContent_Declare(SomeDep
+  SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}
+  SOURCE_SUBDIR DoesNotExist
+)
+FetchContent_MakeAvailable(SomeDep)
+message(STATUS "FetchContent_MakeAvailable() succeeded")