diff options
author | Brad King <brad.king@kitware.com> | 2022-05-25 11:24:40 (GMT) |
---|---|---|
committer | Kitware Robot <kwrobot@kitware.com> | 2022-05-25 11:24:48 (GMT) |
commit | 5dcf505f63037e23094146730704b031c57c5d06 (patch) | |
tree | b9921b81b3741cad9d1ce13a5a5a0b69fed4d678 | |
parent | 7120221e2464f3ee0f4d511783a78d0d83e9cb03 (diff) | |
parent | 2aa83fa15b01941f0267e20a1a4e29793651fefd (diff) | |
download | CMake-5dcf505f63037e23094146730704b031c57c5d06.zip CMake-5dcf505f63037e23094146730704b031c57c5d06.tar.gz CMake-5dcf505f63037e23094146730704b031c57c5d06.tar.bz2 |
Merge topic 'dependency-providers'
2aa83fa15b Dependency providers: Add find_package and FetchContent support
8a28368feb FetchContent: Don't discard non-empty SOURCE_DIR and BINARY_DIR
8ce9bb8a0c FetchContent: Don't leak internal variables
74a6ddc339 cmFindPackageCommand: Handle Makefile variable definitions more robustly
Acked-by: Kitware Robot <kwrobot@kitware.com>
Acked-by: Gerhard Olsson <gerhard.nospam@gmail.com>
Merge-request: !7276
50 files changed, 996 insertions, 55 deletions
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 @@ Synopsis 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 @@ also prints:: 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 @@ find_package .. 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/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/Modules/FetchContent.cmake b/Modules/FetchContent.cmake index a342aa7..5ca296e 100644 --- a/Modules/FetchContent.cmake +++ b/Modules/FetchContent.cmake @@ -193,6 +193,11 @@ Commands ``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 @@ Commands 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 @@ Commands :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: + + * .. 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 the ``<lowercaseName>_POPULATED``, ``<lowercaseName>_SOURCE_DIR`` and @@ -468,7 +498,7 @@ Commands 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 @@ Commands 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 @@ Commands 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 @@ A number of cache variables can influence the behavior where details from a 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 @@ function(FetchContent_Declare contentName) 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 @@ function(FetchContent_Declare contentName) # 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 @@ endfunction() # 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 @@ function(__FetchContent_setPopulated contentName) 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 @@ function(FetchContent_Populate contentName) 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 @@ function(FetchContent_Populate contentName) ) endif() - __FetchContent_setPopulated( + FetchContent_SetPopulated( ${contentName} SOURCE_DIR "${${contentNameLower}_SOURCE_DIR}" BINARY_DIR "${${contentNameLower}_BINARY_DIR}" @@ -1654,22 +1730,98 @@ endfunction() # 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 @@ macro(FetchContent_MakeAvailable) __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 @@ macro(FetchContent_MakeAvailable) # 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 @@ set(SRCS cmCustomCommandTypes.h cmDefinitions.cxx cmDefinitions.h + cmDependencyProvider.h cmDepends.cxx cmDepends.h cmDependsC.cxx 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 @@ bool cmCMakeLanguageCommandEVAL(std::vector<cmListFileArgument> const& args, 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 @@ bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args, 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) } 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) } } - 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 @@ bool cmFindPackageCommand::InitialPass(std::vector<std::string> const& args) 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 @@ void cmFindPackageCommand::SetVersionVariables( 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 @@ void cmFindPackageCommand::AppendSuccessInformation() 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 @@ private: 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 @@ void cmGlobalGenerator::EnableLanguage( } // 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 @@ void cmGlobalGenerator::EnableLanguage( 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 @@ bool cmState::ParseCacheEntry(const std::string& entry, std::string& var, 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 @@ public: 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 @@ private: 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(message) 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") |