From 74a6ddc339ba36637949e40cad216d41adb552a7 Mon Sep 17 00:00:00 2001 From: Craig Scott Date: Sun, 15 May 2022 19:02:27 +1000 Subject: cmFindPackageCommand: Handle Makefile variable definitions more robustly During argument parsing in InitialPass(), Makefile variables were being added for components. Most other such variables were set in the call to SetModuleVariables(), which happens much later. Both sets of variables were then restored to their previous values as part of a call to AppendSuccessInformation(), but that is not an obvious nor robust place to undo those variable changes. InitialPass() also pushes a new item to the package root stack, but the corresponding pop was in AppendSuccessInformation(). Again, this puts a symmetric operation in an asymmetric place. Refactor the code slightly such that Makefile variables are set in one clear location, then restored later in the same function. Also move the package root stack pop into the same function as the push. AppendSuccessInformation() now has one clear responsibility and doesn't perform any unrelated cleanup on behalf of InitialPass(). --- Source/cmFindPackageCommand.cxx | 26 ++++++++++++++++---------- Source/cmFindPackageCommand.h | 4 +++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Source/cmFindPackageCommand.cxx b/Source/cmFindPackageCommand.cxx index 6586c69..fa2eb1c 100644 --- a/Source/cmFindPackageCommand.cxx +++ b/Source/cmFindPackageCommand.cxx @@ -238,6 +238,7 @@ bool cmFindPackageCommand::InitialPass(std::vector const& args) const char* components_sep = ""; std::set requiredComponents; std::set optionalComponents; + std::vector> componentVarDefs; // Always search directly in a generated path. this->SearchPathSuffixes.emplace_back(); @@ -356,7 +357,7 @@ bool cmFindPackageCommand::InitialPass(std::vector 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; @@ -573,7 +574,7 @@ bool cmFindPackageCommand::InitialPass(std::vector 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 +698,12 @@ bool cmFindPackageCommand::InitialPass(std::vector 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 +785,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>& 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 +1400,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..1767b74 100644 --- a/Source/cmFindPackageCommand.h +++ b/Source/cmFindPackageCommand.h @@ -97,7 +97,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>& componentVarDefs); bool FindModule(bool& found); void AddFindDefinition(const std::string& var, cm::string_view value); void RestoreFindDefinitions(); -- cgit v0.12 From 8ce9bb8a0c6e98ff58b7b497c4b8376c2759abfc Mon Sep 17 00:00:00 2001 From: Craig Scott Date: Wed, 18 May 2022 23:25:42 +1000 Subject: FetchContent: Don't leak internal variables --- Modules/FetchContent.cmake | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Modules/FetchContent.cmake b/Modules/FetchContent.cmake index b92c679..f142a5c 100644 --- a/Modules/FetchContent.cmake +++ b/Modules/FetchContent.cmake @@ -1665,11 +1665,15 @@ macro(FetchContent_MakeAvailable) # 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,6 +1687,7 @@ macro(FetchContent_MakeAvailable) __cmake_contentNameLower __cmake_contentName ) + unset(__cmake_fpArgs) if(${__cmake_contentName}_FOUND) set(${__cmake_contentNameLower}_SOURCE_DIR "") @@ -1692,6 +1697,8 @@ macro(FetchContent_MakeAvailable) continue() endif() endif() + else() + unset(__cmake_haveFpArgs) endif() FetchContent_GetProperties(${__cmake_contentName}) @@ -1731,5 +1738,6 @@ macro(FetchContent_MakeAvailable) # clear local variables to prevent leaking into the caller's scope unset(__cmake_contentName) unset(__cmake_contentNameLower) + unset(__cmake_contentNameUpper) endmacro() -- cgit v0.12 From 8a28368feb938f301604c24c0294e2a25749cc77 Mon Sep 17 00:00:00 2001 From: Craig Scott Date: Wed, 18 May 2022 23:28:10 +1000 Subject: FetchContent: Don't discard non-empty SOURCE_DIR and BINARY_DIR --- Modules/FetchContent.cmake | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Modules/FetchContent.cmake b/Modules/FetchContent.cmake index f142a5c..f74454c 100644 --- a/Modules/FetchContent.cmake +++ b/Modules/FetchContent.cmake @@ -1165,10 +1165,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") @@ -1690,10 +1698,8 @@ macro(FetchContent_MakeAvailable) 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_GetProperties(${__cmake_contentName}) continue() endif() endif() -- cgit v0.12 From 2aa83fa15b01941f0267e20a1a4e29793651fefd Mon Sep 17 00:00:00 2001 From: Craig Scott Date: Wed, 18 May 2022 23:29:21 +1000 Subject: Dependency providers: Add find_package and FetchContent support Fixes: #22619 --- Help/command/cmake_language.rst | 263 +++++++++++++++++++++ Help/command/find_package.rst | 3 +- Help/release/dev/dependency-providers.rst | 9 + Modules/FetchContent.cmake | 213 ++++++++++++++--- Source/CMakeLists.txt | 1 + Source/cmCMakeLanguageCommand.cxx | 93 ++++++++ Source/cmDependencyProvider.h | 38 +++ Source/cmFindPackageCommand.cxx | 49 +++- Source/cmFindPackageCommand.h | 1 + Source/cmGlobalGenerator.cxx | 5 + Source/cmState.cxx | 9 + Source/cmState.h | 25 ++ Tests/RunCMake/CMakeLists.txt | 1 + .../DependencyProviders/AfterProject-result.txt | 1 + .../DependencyProviders/AfterProject-stderr.txt | 6 + .../DependencyProviders/BeforeProject-result.txt | 1 + .../DependencyProviders/BeforeProject-stderr.txt | 6 + .../RunCMake/DependencyProviders/Bypass-stdout.txt | 7 + Tests/RunCMake/DependencyProviders/Bypass.cmake | 1 + Tests/RunCMake/DependencyProviders/CMakeLists.txt | 13 + .../ConfigFiles/SomeDepConfig.cmake | 2 + .../FetchContentSerial-stdout.txt | 7 + .../DependencyProviders/FetchContentSerial.cmake | 1 + .../DependencyProviders/FindPackage-stdout.txt | 7 + .../RunCMake/DependencyProviders/FindPackage.cmake | 1 + .../DependencyProviders/NoCommand-result.txt | 1 + .../DependencyProviders/NoCommand-stderr.txt | 3 + .../NoCommandOrMethods-stdout.txt | 3 + .../DependencyProviders/NoCommandOrMethods.cmake | 3 + .../DependencyProviders/NoMethods-result.txt | 1 + .../DependencyProviders/NoMethods-stderr.txt | 2 + .../PassThroughProvider-stdout.txt | 7 + .../DependencyProviders/PassThroughProvider.cmake | 1 + .../ProjectIncludeAfter-result.txt | 1 + .../ProjectIncludeAfter-stderr.txt | 6 + .../ProjectIncludeBefore-result.txt | 1 + .../ProjectIncludeBefore-stderr.txt | 6 + .../DependencyProviders/Recurse-stdout.txt | 7 + Tests/RunCMake/DependencyProviders/Recurse.cmake | 8 + .../RedirectFetchContentSerial-result.txt | 1 + .../RedirectFetchContentSerial-stderr.txt | 11 + .../RedirectFetchContentSerial-stdout.txt | 5 + .../RedirectFetchContentSerial.cmake | 1 + .../RedirectFindPackage-stdout.txt | 7 + .../DependencyProviders/RedirectFindPackage.cmake | 1 + .../DependencyProviders/RunCMakeTest.cmake | 73 ++++++ .../DependencyProviders/ToolchainFile-result.txt | 1 + .../DependencyProviders/ToolchainFile-stderr.txt | 6 + .../DependencyProviders/set_provider.cmake | 64 +++++ .../RunCMake/DependencyProviders/try_methods.cmake | 12 + 50 files changed, 957 insertions(+), 38 deletions(-) create mode 100644 Help/release/dev/dependency-providers.rst create mode 100644 Source/cmDependencyProvider.h create mode 100644 Tests/RunCMake/DependencyProviders/AfterProject-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/AfterProject-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/BeforeProject-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/BeforeProject-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/Bypass-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/Bypass.cmake create mode 100644 Tests/RunCMake/DependencyProviders/CMakeLists.txt create mode 100644 Tests/RunCMake/DependencyProviders/ConfigFiles/SomeDepConfig.cmake create mode 100644 Tests/RunCMake/DependencyProviders/FetchContentSerial-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/FetchContentSerial.cmake create mode 100644 Tests/RunCMake/DependencyProviders/FindPackage-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/FindPackage.cmake create mode 100644 Tests/RunCMake/DependencyProviders/NoCommand-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/NoCommand-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/NoCommandOrMethods-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/NoCommandOrMethods.cmake create mode 100644 Tests/RunCMake/DependencyProviders/NoMethods-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/NoMethods-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/PassThroughProvider-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/PassThroughProvider.cmake create mode 100644 Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/ProjectIncludeAfter-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/ProjectIncludeBefore-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/Recurse-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/Recurse.cmake create mode 100644 Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/RedirectFetchContentSerial.cmake create mode 100644 Tests/RunCMake/DependencyProviders/RedirectFindPackage-stdout.txt create mode 100644 Tests/RunCMake/DependencyProviders/RedirectFindPackage.cmake create mode 100644 Tests/RunCMake/DependencyProviders/RunCMakeTest.cmake create mode 100644 Tests/RunCMake/DependencyProviders/ToolchainFile-result.txt create mode 100644 Tests/RunCMake/DependencyProviders/ToolchainFile-stderr.txt create mode 100644 Tests/RunCMake/DependencyProviders/set_provider.cmake create mode 100644 Tests/RunCMake/DependencyProviders/try_methods.cmake 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`_ [...]) cmake_language(`EVAL`_ CODE ...) cmake_language(`DEFER`_ ... CALL [...]) + cmake_language(`SET_DEPENDENCY_PROVIDER`_ SUPPORTED_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 + SUPPORTED_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 ```` specified when the provider +was set, CMake calls the provider's ```` 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 ```` +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 ```` must +already exist when ``cmake_language(SET_DEPENDENCY_PROVIDER)`` is called. +As a special case, providing an empty string for the ```` and no +```` 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 ```` 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( [...]) + +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 ```` that was specified when setting the provider. + +``FIND_PACKAGE`` + The ```` will be everything passed to the + :command:`find_package` call that requested the dependency. The first of + these ```` 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 ```` 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 ```` 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_`` is set, then the + dependency provider will never see requests for the ```` 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_`` 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 8ce6529..8567a00 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 `. 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 f74454c..e1cc843 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( ...)`` call includes this option, subsequent calls to ``find_package( ...)`` 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 ` has been set + and the project calls :command:`find_package` for the ```` + 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( [...]) ` - will be called, where ``...`` 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 ``_SOURCE_DIR`` is not set: + + * .. versionadded:: 3.24 + + If a :ref:`dependency provider ` 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 ````. 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( [...]) ` + will be called, where ``...`` 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 ``_POPULATED``, ``_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) ` or :command:`FetchContent_Populate(name) `. + Note that the ``SOURCE_DIR`` and ``BINARY_DIR`` values can be empty if the + call is fulfilled by a :ref:`dependency provider `. 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 `. 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( + + [SOURCE_DIR ] + [BINARY_DIR ] + ) + + 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 "" @@ -1488,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 @@ -1592,7 +1660,7 @@ function(FetchContent_Populate contentName) ) endif() - __FetchContent_setPopulated( + FetchContent_SetPopulated( ${contentName} SOURCE_DIR "${${contentNameLower}_SOURCE_DIR}" BINARY_DIR "${${contentNameLower}_BINARY_DIR}" @@ -1662,13 +1730,85 @@ 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 @@ -1698,7 +1838,7 @@ macro(FetchContent_MakeAvailable) unset(__cmake_fpArgs) if(${__cmake_contentName}_FOUND) - __FetchContent_setPopulated(${__cmake_contentName}) + FetchContent_SetPopulated(${__cmake_contentName}) FetchContent_GetProperties(${__cmake_contentName}) continue() endif() @@ -1745,5 +1885,6 @@ macro(FetchContent_MakeAvailable) 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 #include +#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 const& args, return makefile.ReadListFileAsString( code, cmStrCat(context.FilePath, ":", context.Line, ":EVAL")); } + +bool cmCMakeLanguageCommandSET_DEPENDENCY_PROVIDER( + std::vector 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 Methods; + }; + + auto const ArgsParser = + cmArgumentParser() + .Bind("SET_DEPENDENCY_PROVIDER"_s, &SetProviderArgs::Command) + .Bind("SUPPORTED_METHODS"_s, &SetProviderArgs::Methods); + + std::vector 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 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 const& args, @@ -246,6 +334,11 @@ bool cmCMakeLanguageCommand(std::vector 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 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 +#include +#include +#include + +class cmDependencyProvider +{ +public: + enum class Method + { + FindPackage, + FetchContentMakeAvailableSerial, + }; + + cmDependencyProvider(std::string command, std::vector methods) + : Command(std::move(command)) + , Methods(std::move(methods)) + { + } + + std::string const& GetCommand() const { return this->Command; } + std::vector 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 Methods; +}; diff --git a/Source/cmFindPackageCommand.cxx b/Source/cmFindPackageCommand.cxx index fa2eb1c..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" @@ -239,6 +240,7 @@ bool cmFindPackageCommand::InitialPass(std::vector const& args) std::set requiredComponents; std::set optionalComponents; std::vector> componentVarDefs; + bool bypassProvider = false; // Always search directly in a generated path. this->SearchPathSuffixes.emplace_back(); @@ -269,6 +271,9 @@ bool cmFindPackageCommand::InitialPass(std::vector 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; @@ -409,7 +414,7 @@ bool cmFindPackageCommand::InitialPass(std::vector 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) { @@ -544,6 +549,48 @@ bool cmFindPackageCommand::InitialPass(std::vector 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 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(); diff --git a/Source/cmFindPackageCommand.h b/Source/cmFindPackageCommand.h index 1767b74..80fd8f8 100644 --- a/Source/cmFindPackageCommand.h +++ b/Source/cmFindPackageCommand.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include diff --git a/Source/cmGlobalGenerator.cxx b/Source/cmGlobalGenerator.cxx index 3831546..12ae983 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 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 #include #include +#include #include #include +#include #include +#include + #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 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 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") -- cgit v0.12