Bläddra i källkod

Dependency providers: Add find_package and FetchContent support

Fixes: #22619
Craig Scott 3 år sedan
förälder
incheckning
2aa83fa15b
50 ändrade filer med 957 tillägg och 38 borttagningar
  1. 263 0
      Help/command/cmake_language.rst
  2. 2 1
      Help/command/find_package.rst
  3. 9 0
      Help/release/dev/dependency-providers.rst
  4. 177 36
      Modules/FetchContent.cmake
  5. 1 0
      Source/CMakeLists.txt
  6. 93 0
      Source/cmCMakeLanguageCommand.cxx
  7. 38 0
      Source/cmDependencyProvider.h
  8. 48 1
      Source/cmFindPackageCommand.cxx
  9. 1 0
      Source/cmFindPackageCommand.h
  10. 5 0
      Source/cmGlobalGenerator.cxx
  11. 9 0
      Source/cmState.cxx
  12. 25 0
      Source/cmState.h
  13. 1 0
      Tests/RunCMake/CMakeLists.txt
  14. 1 0
      Tests/RunCMake/DependencyProviders/AfterProject-result.txt
  15. 6 0
      Tests/RunCMake/DependencyProviders/AfterProject-stderr.txt
  16. 1 0
      Tests/RunCMake/DependencyProviders/BeforeProject-result.txt
  17. 6 0
      Tests/RunCMake/DependencyProviders/BeforeProject-stderr.txt
  18. 7 0
      Tests/RunCMake/DependencyProviders/Bypass-stdout.txt
  19. 1 0
      Tests/RunCMake/DependencyProviders/Bypass.cmake
  20. 13 0
      Tests/RunCMake/DependencyProviders/CMakeLists.txt
  21. 2 0
      Tests/RunCMake/DependencyProviders/ConfigFiles/SomeDepConfig.cmake
  22. 7 0
      Tests/RunCMake/DependencyProviders/FetchContentSerial-stdout.txt
  23. 1 0
      Tests/RunCMake/DependencyProviders/FetchContentSerial.cmake
  24. 7 0
      Tests/RunCMake/DependencyProviders/FindPackage-stdout.txt
  25. 1 0
      Tests/RunCMake/DependencyProviders/FindPackage.cmake
  26. 1 0
      Tests/RunCMake/DependencyProviders/NoCommand-result.txt
  27. 3 0
      Tests/RunCMake/DependencyProviders/NoCommand-stderr.txt
  28. 3 0
      Tests/RunCMake/DependencyProviders/NoCommandOrMethods-stdout.txt
  29. 3 0
      Tests/RunCMake/DependencyProviders/NoCommandOrMethods.cmake
  30. 1 0
      Tests/RunCMake/DependencyProviders/NoMethods-result.txt
  31. 2 0
      Tests/RunCMake/DependencyProviders/NoMethods-stderr.txt
  32. 7 0
      Tests/RunCMake/DependencyProviders/PassThroughProvider-stdout.txt
  33. 1 0
      Tests/RunCMake/DependencyProviders/PassThroughProvider.cmake
  34. 1 0
      Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-result.txt
  35. 6 0
      Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-stderr.txt
  36. 1 0
      Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-result.txt
  37. 6 0
      Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-stderr.txt
  38. 7 0
      Tests/RunCMake/DependencyProviders/Recurse-stdout.txt
  39. 8 0
      Tests/RunCMake/DependencyProviders/Recurse.cmake
  40. 1 0
      Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-result.txt
  41. 11 0
      Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stderr.txt
  42. 5 0
      Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stdout.txt
  43. 1 0
      Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial.cmake
  44. 7 0
      Tests/RunCMake/DependencyProviders/RedirectFindPackage-stdout.txt
  45. 1 0
      Tests/RunCMake/DependencyProviders/RedirectFindPackage.cmake
  46. 73 0
      Tests/RunCMake/DependencyProviders/RunCMakeTest.cmake
  47. 1 0
      Tests/RunCMake/DependencyProviders/ToolchainFile-result.txt
  48. 6 0
      Tests/RunCMake/DependencyProviders/ToolchainFile-stderr.txt
  49. 64 0
      Tests/RunCMake/DependencyProviders/set_provider.cmake
  50. 12 0
      Tests/RunCMake/DependencyProviders/try_methods.cmake

+ 263 - 0
Help/command/cmake_language.rst

@@ -13,6 +13,7 @@ Synopsis
   cmake_language(`CALL`_ <command> [<arg>...])
   cmake_language(`CALL`_ <command> [<arg>...])
   cmake_language(`EVAL`_ CODE <code>...)
   cmake_language(`EVAL`_ CODE <code>...)
   cmake_language(`DEFER`_ <options>... CALL <command> [<arg>...])
   cmake_language(`DEFER`_ <options>... CALL <command> [<arg>...])
+  cmake_language(`SET_DEPENDENCY_PROVIDER`_ <command> SUPPORTED_METHODS <methods>...)
 
 
 Introduction
 Introduction
 ^^^^^^^^^^^^
 ^^^^^^^^^^^^
@@ -225,3 +226,265 @@ also prints::
   Immediate Message
   Immediate Message
   Deferred Message 1
   Deferred Message 1
   Deferred Message 2
   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
+  )

+ 2 - 1
Help/command/find_package.rst

@@ -12,7 +12,8 @@ find_package
    .. contents::
    .. contents::
 
 
 Find a package (usually provided by something external to the project),
 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
 Search Modes
 ^^^^^^^^^^^^
 ^^^^^^^^^^^^

+ 9 - 0
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`.

+ 177 - 36
Modules/FetchContent.cmake

@@ -193,6 +193,11 @@ Commands
       ``OVERRIDE_FIND_PACKAGE`` cannot be used when ``FIND_PACKAGE_ARGS`` is
       ``OVERRIDE_FIND_PACKAGE`` cannot be used when ``FIND_PACKAGE_ARGS`` is
       given.
       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``
     ``OVERRIDE_FIND_PACKAGE``
       When a ``FetchContent_Declare(<name> ...)`` call includes this option,
       When a ``FetchContent_Declare(<name> ...)`` call includes this option,
       subsequent calls to ``find_package(<name> ...)`` will ensure that
       subsequent calls to ``find_package(<name> ...)`` will ensure that
@@ -204,6 +209,13 @@ Commands
       satisfy the package requirements of the latter.  ``FIND_PACKAGE_ARGS``
       satisfy the package requirements of the latter.  ``FIND_PACKAGE_ARGS``
       cannot be used when ``OVERRIDE_FIND_PACKAGE`` is given.
       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
 .. command:: FetchContent_MakeAvailable
 
 
   .. versionadded:: 3.14
   .. versionadded:: 3.14
@@ -217,17 +229,35 @@ Commands
   :command:`FetchContent_Declare` for each dependency, and the first such call
   :command:`FetchContent_Declare` for each dependency, and the first such call
   will control how that dependency will be made available, as described below.
   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:
+
+  * .. 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 :command:`find_package` was unsuccessful or was not allowed to be called,
-  ``FetchContent_MakeAvailable()`` then uses the following logic to make the
-  dependency available:
+      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
   * If the dependency has already been populated earlier in this run, set
     the ``<lowercaseName>_POPULATED``, ``<lowercaseName>_SOURCE_DIR`` and
     the ``<lowercaseName>_POPULATED``, ``<lowercaseName>_SOURCE_DIR`` and
@@ -468,7 +498,7 @@ Commands
   When using saved content details, a call to
   When using saved content details, a call to
   :command:`FetchContent_MakeAvailable` or :command:`FetchContent_Populate`
   :command:`FetchContent_MakeAvailable` or :command:`FetchContent_Populate`
   records information in global properties which can be queried at any time.
   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
   the content and also whether or not the content population has been processed
   during the current configure run.
   during the current configure run.
 
 
@@ -488,6 +518,8 @@ Commands
   set the same variables as a call to
   set the same variables as a call to
   :command:`FetchContent_MakeAvailable(name) <FetchContent_MakeAvailable>` or
   :command:`FetchContent_MakeAvailable(name) <FetchContent_MakeAvailable>` or
   :command:`FetchContent_Populate(name) <FetchContent_Populate>`.
   :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
   This command is rarely needed when using
   :command:`FetchContent_MakeAvailable`.  It is more commonly used as part of
   :command:`FetchContent_MakeAvailable`.  It is more commonly used as part of
@@ -511,6 +543,33 @@ Commands
       add_subdirectory(${depname_SOURCE_DIR} ${depname_BINARY_DIR})
       add_subdirectory(${depname_SOURCE_DIR} ${depname_BINARY_DIR})
     endif()
     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
 Variables
 ^^^^^^^^^
 ^^^^^^^^^
 
 
@@ -588,7 +647,7 @@ A number of cache variables can influence the behavior where details from a
     behavior if ``FETCHCONTENT_TRY_FIND_PACKAGE_MODE`` is not set.
     behavior if ``FETCHCONTENT_TRY_FIND_PACKAGE_MODE`` is not set.
 
 
   ``ALWAYS``
   ``ALWAYS``
-    :command:`find_package` will be called by
+    :command:`find_package` can be called by
     :command:`FetchContent_MakeAvailable` regardless of whether the
     :command:`FetchContent_MakeAvailable` regardless of whether the
     :command:`FetchContent_Declare` call included a ``FIND_PACKAGE_ARGS``
     :command:`FetchContent_Declare` call included a ``FIND_PACKAGE_ARGS``
     keyword or not.  If no ``FIND_PACKAGE_ARGS`` keyword was given, the
     keyword or not.  If no ``FIND_PACKAGE_ARGS`` keyword was given, the
@@ -1099,14 +1158,26 @@ function(FetchContent_Declare contentName)
   endif()
   endif()
 
 
   set(options "")
   set(options "")
-  set(oneValueArgs SVN_REPOSITORY)
+  set(oneValueArgs
+    BINARY_DIR
+    SOURCE_DIR
+    SVN_REPOSITORY
+  )
   set(multiValueArgs "")
   set(multiValueArgs "")
 
 
   cmake_parse_arguments(PARSE_ARGV 1 ARG
   cmake_parse_arguments(PARSE_ARGV 1 ARG
     "${options}" "${oneValueArgs}" "${multiValueArgs}")
     "${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)
   if(ARG_SVN_REPOSITORY)
     # Add a hash of the svn repository URL to the source dir. This works
     # 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
     # around the problem where if the URL changes, the download would
@@ -1116,25 +1187,21 @@ function(FetchContent_Declare contentName)
     # problem on windows due to path length limits).
     # problem on windows due to path length limits).
     string(SHA1 urlSHA ${ARG_SVN_REPOSITORY})
     string(SHA1 urlSHA ${ARG_SVN_REPOSITORY})
     string(SUBSTRING ${urlSHA} 0 7 urlSHA)
     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()
   endif()
 
 
-  string(TOLOWER ${contentName} contentNameLower)
+  list(PREPEND ARG_UNPARSED_ARGUMENTS
+    SOURCE_DIR "${ARG_SOURCE_DIR}"
+    BINARY_DIR "${ARG_BINARY_DIR}"
+  )
 
 
   set(__argsQuoted)
   set(__argsQuoted)
   foreach(__item IN LISTS ARG_UNPARSED_ARGUMENTS)
   foreach(__item IN LISTS ARG_UNPARSED_ARGUMENTS)
     string(APPEND __argsQuoted " [==[${__item}]==]")
     string(APPEND __argsQuoted " [==[${__item}]==]")
   endforeach()
   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()
 endfunction()
@@ -1145,11 +1212,11 @@ endfunction()
 # The setter also records the source and binary dirs used.
 # 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
   cmake_parse_arguments(PARSE_ARGV 1 arg
     ""
     ""
@@ -1488,7 +1555,8 @@ function(FetchContent_Populate contentName)
   if(${contentNameLower}_POPULATED)
   if(${contentNameLower}_POPULATED)
     if("${${contentNameLower}_SOURCE_DIR}" STREQUAL "")
     if("${${contentNameLower}_SOURCE_DIR}" STREQUAL "")
       message(FATAL_ERROR
       message(FATAL_ERROR
-        "Content ${contentName} already populated by find_package()"
+        "Content ${contentName} already populated by find_package() or a "
+        "dependency provider"
       )
       )
     else()
     else()
       message(FATAL_ERROR
       message(FATAL_ERROR
@@ -1592,7 +1660,7 @@ function(FetchContent_Populate contentName)
     )
     )
   endif()
   endif()
 
 
-  __FetchContent_setPopulated(
+  FetchContent_SetPopulated(
     ${contentName}
     ${contentName}
     SOURCE_DIR "${${contentNameLower}_SOURCE_DIR}"
     SOURCE_DIR "${${contentNameLower}_SOURCE_DIR}"
     BINARY_DIR "${${contentNameLower}_BINARY_DIR}"
     BINARY_DIR "${${contentNameLower}_BINARY_DIR}"
@@ -1662,13 +1730,85 @@ endfunction()
 # calls will be available to the caller.
 # calls will be available to the caller.
 macro(FetchContent_MakeAvailable)
 macro(FetchContent_MakeAvailable)
 
 
+  get_property(__cmake_providerCommand GLOBAL PROPERTY
+    __FETCHCONTENT_MAKEAVAILABLE_SERIAL_PROVIDER
+  )
   foreach(__cmake_contentName IN ITEMS ${ARGV})
   foreach(__cmake_contentName IN ITEMS ${ARGV})
     string(TOLOWER ${__cmake_contentName} __cmake_contentNameLower)
     string(TOLOWER ${__cmake_contentName} __cmake_contentNameLower)
 
 
     # If user specified FETCHCONTENT_SOURCE_DIR_... for this dependency, that
     # 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)
     string(TOUPPER ${__cmake_contentName} __cmake_contentNameUpper)
     if("${FETCHCONTENT_SOURCE_DIR_${__cmake_contentNameUpper}}" STREQUAL "")
     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
       # Check if we've been asked to try find_package() first, even if we
       # have already populated this dependency. If we previously tried to
       # have already populated this dependency. If we previously tried to
       # use find_package() for this and it succeeded, those things might
       # use find_package() for this and it succeeded, those things might
@@ -1698,7 +1838,7 @@ macro(FetchContent_MakeAvailable)
         unset(__cmake_fpArgs)
         unset(__cmake_fpArgs)
 
 
         if(${__cmake_contentName}_FOUND)
         if(${__cmake_contentName}_FOUND)
-          __FetchContent_setPopulated(${__cmake_contentName})
+          FetchContent_SetPopulated(${__cmake_contentName})
           FetchContent_GetProperties(${__cmake_contentName})
           FetchContent_GetProperties(${__cmake_contentName})
           continue()
           continue()
         endif()
         endif()
@@ -1745,5 +1885,6 @@ macro(FetchContent_MakeAvailable)
   unset(__cmake_contentName)
   unset(__cmake_contentName)
   unset(__cmake_contentNameLower)
   unset(__cmake_contentNameLower)
   unset(__cmake_contentNameUpper)
   unset(__cmake_contentNameUpper)
+  unset(__cmake_providerCommand)
 
 
 endmacro()
 endmacro()

+ 1 - 0
Source/CMakeLists.txt

@@ -199,6 +199,7 @@ set(SRCS
   cmCustomCommandTypes.h
   cmCustomCommandTypes.h
   cmDefinitions.cxx
   cmDefinitions.cxx
   cmDefinitions.h
   cmDefinitions.h
+  cmDependencyProvider.h
   cmDepends.cxx
   cmDepends.cxx
   cmDepends.h
   cmDepends.h
   cmDependsC.cxx
   cmDependsC.cxx

+ 93 - 0
Source/cmCMakeLanguageCommand.cxx

@@ -13,11 +13,14 @@
 #include <cm/string_view>
 #include <cm/string_view>
 #include <cmext/string_view>
 #include <cmext/string_view>
 
 
+#include "cmArgumentParser.h"
+#include "cmDependencyProvider.h"
 #include "cmExecutionStatus.h"
 #include "cmExecutionStatus.h"
 #include "cmGlobalGenerator.h"
 #include "cmGlobalGenerator.h"
 #include "cmListFileCache.h"
 #include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMakefile.h"
 #include "cmRange.h"
 #include "cmRange.h"
+#include "cmState.h"
 #include "cmStringAlgorithms.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmSystemTools.h"
 
 
@@ -215,6 +218,91 @@ bool cmCMakeLanguageCommandEVAL(std::vector<cmListFileArgument> const& args,
   return makefile.ReadListFileAsString(
   return makefile.ReadListFileAsString(
     code, cmStrCat(context.FilePath, ":", context.Line, ":EVAL"));
     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,
 bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
@@ -246,6 +334,11 @@ bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
     return FatalError(status, "called with incorrect number of arguments");
     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;
   cm::optional<Defer> maybeDefer;
   if (expArgs[expArg] == "DEFER"_s) {
   if (expArgs[expArg] == "DEFER"_s) {
     ++expArg; // Consume "DEFER".
     ++expArg; // Consume "DEFER".

+ 38 - 0
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;
+};

+ 48 - 1
Source/cmFindPackageCommand.cxx

@@ -23,6 +23,7 @@
 #include "cmsys/String.h"
 #include "cmsys/String.h"
 
 
 #include "cmAlgorithms.h"
 #include "cmAlgorithms.h"
+#include "cmDependencyProvider.h"
 #include "cmListFileCache.h"
 #include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMakefile.h"
 #include "cmMessageType.h"
 #include "cmMessageType.h"
@@ -239,6 +240,7 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args)
   std::set<std::string> requiredComponents;
   std::set<std::string> requiredComponents;
   std::set<std::string> optionalComponents;
   std::set<std::string> optionalComponents;
   std::vector<std::pair<std::string, const char*>> componentVarDefs;
   std::vector<std::pair<std::string, const char*>> componentVarDefs;
+  bool bypassProvider = false;
 
 
   // Always search directly in a generated path.
   // Always search directly in a generated path.
   this->SearchPathSuffixes.emplace_back();
   this->SearchPathSuffixes.emplace_back();
@@ -269,6 +271,9 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args)
     if (args[i] == "QUIET") {
     if (args[i] == "QUIET") {
       this->Quiet = true;
       this->Quiet = true;
       doing = DoingNone;
       doing = DoingNone;
+    } else if (args[i] == "BYPASS_PROVIDER") {
+      bypassProvider = true;
+      doing = DoingNone;
     } else if (args[i] == "EXACT") {
     } else if (args[i] == "EXACT") {
       this->VersionExact = true;
       this->VersionExact = true;
       doing = DoingNone;
       doing = DoingNone;
@@ -409,7 +414,7 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args)
     return false;
     return false;
   }
   }
 
 
-  // Maybe choose one mode exclusively.
+  // Check and eliminate search modes not allowed by the args provided
   this->UseFindModules = configArgs.empty();
   this->UseFindModules = configArgs.empty();
   this->UseConfigFiles = moduleArgs.empty();
   this->UseConfigFiles = moduleArgs.empty();
   if (!this->UseFindModules && !this->UseConfigFiles) {
   if (!this->UseFindModules && !this->UseConfigFiles) {
@@ -544,6 +549,48 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args)
     return true;
     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.
     // Allocate a PACKAGE_ROOT_PATH for the current find_package call.
     this->Makefile->FindPackageRootPathStack.emplace_back();
     this->Makefile->FindPackageRootPathStack.emplace_back();

+ 1 - 0
Source/cmFindPackageCommand.h

@@ -9,6 +9,7 @@
 #include <map>
 #include <map>
 #include <set>
 #include <set>
 #include <string>
 #include <string>
+#include <utility>
 #include <vector>
 #include <vector>
 
 
 #include <cm/string_view>
 #include <cm/string_view>

+ 5 - 0
Source/cmGlobalGenerator.cxx

@@ -690,6 +690,7 @@ void cmGlobalGenerator::EnableLanguage(
     }
     }
 
 
     // One-time includes of user-provided project setup files
     // One-time includes of user-provided project setup files
+    mf->GetState()->SetInTopLevelIncludes(true);
     std::string includes =
     std::string includes =
       mf->GetSafeDefinition("CMAKE_PROJECT_TOP_LEVEL_INCLUDES");
       mf->GetSafeDefinition("CMAKE_PROJECT_TOP_LEVEL_INCLUDES");
     std::vector<std::string> includesList = cmExpandedList(includes);
     std::vector<std::string> includesList = cmExpandedList(includes);
@@ -700,22 +701,26 @@ void cmGlobalGenerator::EnableLanguage(
         cmSystemTools::Error(
         cmSystemTools::Error(
           "CMAKE_PROJECT_TOP_LEVEL_INCLUDES file does not exist: " +
           "CMAKE_PROJECT_TOP_LEVEL_INCLUDES file does not exist: " +
           setupFile);
           setupFile);
+        mf->GetState()->SetInTopLevelIncludes(false);
         return;
         return;
       }
       }
       if (cmSystemTools::FileIsDirectory(absSetupFile)) {
       if (cmSystemTools::FileIsDirectory(absSetupFile)) {
         cmSystemTools::Error(
         cmSystemTools::Error(
           "CMAKE_PROJECT_TOP_LEVEL_INCLUDES file is a directory: " +
           "CMAKE_PROJECT_TOP_LEVEL_INCLUDES file is a directory: " +
           setupFile);
           setupFile);
+        mf->GetState()->SetInTopLevelIncludes(false);
         return;
         return;
       }
       }
       if (!mf->ReadListFile(absSetupFile)) {
       if (!mf->ReadListFile(absSetupFile)) {
         cmSystemTools::Error(
         cmSystemTools::Error(
           "Failed reading CMAKE_PROJECT_TOP_LEVEL_INCLUDES file: " +
           "Failed reading CMAKE_PROJECT_TOP_LEVEL_INCLUDES file: " +
           setupFile);
           setupFile);
+        mf->GetState()->SetInTopLevelIncludes(false);
         return;
         return;
       }
       }
     }
     }
   }
   }
+  mf->GetState()->SetInTopLevelIncludes(false);
 
 
   // Check that the languages are supported by the generator and its
   // Check that the languages are supported by the generator and its
   // native build tool found above.
   // native build tool found above.

+ 9 - 0
Source/cmState.cxx

@@ -1072,3 +1072,12 @@ bool cmState::ParseCacheEntry(const std::string& entry, std::string& var,
 
 
   return flag;
   return flag;
 }
 }
+
+cmState::Command cmState::GetDependencyProviderCommand(
+  cmDependencyProvider::Method method) const
+{
+  return (this->DependencyProvider &&
+          this->DependencyProvider->SupportsMethod(method))
+    ? this->GetCommand(this->DependencyProvider->GetCommand())
+    : Command{};
+}

+ 25 - 0
Source/cmState.h

@@ -8,11 +8,16 @@
 #include <memory>
 #include <memory>
 #include <set>
 #include <set>
 #include <string>
 #include <string>
+#include <type_traits>
 #include <unordered_map>
 #include <unordered_map>
 #include <unordered_set>
 #include <unordered_set>
+#include <utility>
 #include <vector>
 #include <vector>
 
 
+#include <cm/optional>
+
 #include "cmDefinitions.h"
 #include "cmDefinitions.h"
+#include "cmDependencyProvider.h"
 #include "cmLinkedTree.h"
 #include "cmLinkedTree.h"
 #include "cmPolicies.h"
 #include "cmPolicies.h"
 #include "cmProperty.h"
 #include "cmProperty.h"
@@ -227,6 +232,24 @@ public:
 
 
   ProjectKind GetProjectKind() const;
   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:
 private:
   friend class cmake;
   friend class cmake;
   void AddCacheEntry(const std::string& key, const char* value,
   void AddCacheEntry(const std::string& key, const char* value,
@@ -288,4 +311,6 @@ private:
   bool NinjaMulti = false;
   bool NinjaMulti = false;
   Mode StateMode = Unknown;
   Mode StateMode = Unknown;
   ProjectKind StateProjectKind = ProjectKind::Normal;
   ProjectKind StateProjectKind = ProjectKind::Normal;
+  cm::optional<cmDependencyProvider> DependencyProvider;
+  bool ProcessingTopLevelIncludes = false;
 };
 };

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -461,6 +461,7 @@ add_RunCMake_test(message)
 add_RunCMake_test(option)
 add_RunCMake_test(option)
 add_RunCMake_test(project -DCMake_TEST_RESOURCES=${CMake_TEST_RESOURCES})
 add_RunCMake_test(project -DCMake_TEST_RESOURCES=${CMake_TEST_RESOURCES})
 add_RunCMake_test(project_injected)
 add_RunCMake_test(project_injected)
+add_RunCMake_test(DependencyProviders)
 add_RunCMake_test(return)
 add_RunCMake_test(return)
 add_RunCMake_test(separate_arguments)
 add_RunCMake_test(separate_arguments)
 add_RunCMake_test(set_property)
 add_RunCMake_test(set_property)

+ 1 - 0
Tests/RunCMake/DependencyProviders/AfterProject-result.txt

@@ -0,0 +1 @@
+1

+ 6 - 0
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\.

+ 1 - 0
Tests/RunCMake/DependencyProviders/BeforeProject-result.txt

@@ -0,0 +1 @@
+1

+ 6 - 0
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\.

+ 7 - 0
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

+ 1 - 0
Tests/RunCMake/DependencyProviders/Bypass.cmake

@@ -0,0 +1 @@
+find_package(SomeDep QUIET REQUIRED)

+ 13 - 0
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)

+ 2 - 0
Tests/RunCMake/DependencyProviders/ConfigFiles/SomeDepConfig.cmake

@@ -0,0 +1,2 @@
+message(STATUS "SomeDepConfig.cmake was used")
+set(SomeDep_FOUND TRUE)

+ 7 - 0
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

+ 1 - 0
Tests/RunCMake/DependencyProviders/FetchContentSerial.cmake

@@ -0,0 +1 @@
+include(try_methods.cmake)

+ 7 - 0
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

+ 1 - 0
Tests/RunCMake/DependencyProviders/FindPackage.cmake

@@ -0,0 +1 @@
+include(try_methods.cmake)

+ 1 - 0
Tests/RunCMake/DependencyProviders/NoCommand-result.txt

@@ -0,0 +1 @@
+1

+ 3 - 0
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

+ 3 - 0
Tests/RunCMake/DependencyProviders/NoCommandOrMethods-stdout.txt

@@ -0,0 +1,3 @@
+-- Before cmake_language
+-- After cmake_language
+-- AThing_FOUND = 0

+ 3 - 0
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}")

+ 1 - 0
Tests/RunCMake/DependencyProviders/NoMethods-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
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

+ 7 - 0
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

+ 1 - 0
Tests/RunCMake/DependencyProviders/PassThroughProvider.cmake

@@ -0,0 +1 @@
+include(try_methods.cmake)

+ 1 - 0
Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-result.txt

@@ -0,0 +1 @@
+1

+ 6 - 0
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\.

+ 1 - 0
Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-result.txt

@@ -0,0 +1 @@
+1

+ 6 - 0
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\.

+ 7 - 0
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

+ 8 - 0
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)

+ 1 - 0
Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-result.txt

@@ -0,0 +1 @@
+1

+ 11 - 0
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\.

+ 5 - 0
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

+ 1 - 0
Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial.cmake

@@ -0,0 +1 @@
+include(try_methods.cmake)

+ 7 - 0
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

+ 1 - 0
Tests/RunCMake/DependencyProviders/RedirectFindPackage.cmake

@@ -0,0 +1 @@
+include(try_methods.cmake)

+ 73 - 0
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"
+)

+ 1 - 0
Tests/RunCMake/DependencyProviders/ToolchainFile-result.txt

@@ -0,0 +1 @@
+1

+ 6 - 0
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\.

+ 64 - 0
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")

+ 12 - 0
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")