summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Scott <craig.scott@crascit.com>2023-04-27 13:12:03 +0000
committerKitware Robot <kwrobot@kitware.com>2023-04-27 09:12:26 -0400
commit0ca98beb5747de97040227695732f660b7b91fc0 (patch)
treef97d62a34b926e8464bf1d60c8f969b059202211
parent2fbcc81440343bdb1bca4e164fdefec98eaa1196 (diff)
parent550f63447d4c7d2db6ccbeaf1f6378aa6f7af4ed (diff)
downloadcmake-0ca98beb5747de97040227695732f660b7b91fc0.tar.gz
Merge topic 'ExternalProject-relative-git-urls'
550f63447d ExternalProject/FetchContent: Support relative remote URLs Acked-by: Kitware Robot <kwrobot@kitware.com> Merge-request: !7988
-rw-r--r--Help/manual/cmake-policies.7.rst1
-rw-r--r--Help/policy/CMP0150.rst39
-rw-r--r--Help/release/dev/ExternalProject-FetchContent-relative-git-remotes.rst7
-rw-r--r--Modules/ExternalProject.cmake20
-rw-r--r--Modules/ExternalProject/shared_internal_commands.cmake182
-rw-r--r--Modules/FetchContent.cmake27
-rw-r--r--Source/cmPolicies.h7
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-NEW-build-stdout.txt7
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-NEW-resolve.cmake107
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-NEW-stdout.txt4
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-NEW.cmake45
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-OLD-build-stdout.txt3
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-OLD-common.cmake21
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-OLD-stdout.txt3
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-OLD.cmake2
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-WARN-build-stdout.txt3
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-WARN-stderr.txt25
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-WARN-stdout.txt3
-rw-r--r--Tests/RunCMake/CMP0150/CMP0150-WARN.cmake2
-rw-r--r--Tests/RunCMake/CMP0150/CMakeLists.txt27
-rw-r--r--Tests/RunCMake/CMP0150/CMakeLists.txt.in23
-rw-r--r--Tests/RunCMake/CMP0150/RunCMakeTest.cmake17
-rw-r--r--Tests/RunCMake/CMakeLists.txt1
23 files changed, 575 insertions, 1 deletions
diff --git a/Help/manual/cmake-policies.7.rst b/Help/manual/cmake-policies.7.rst
index 8c7189a72b..ff573906b2 100644
--- a/Help/manual/cmake-policies.7.rst
+++ b/Help/manual/cmake-policies.7.rst
@@ -57,6 +57,7 @@ Policies Introduced by CMake 3.27
.. toctree::
:maxdepth: 1
+ CMP0150: ExternalProject_Add and FetchContent_Declare treat relative git repository paths as being relative to parent project's remote. </policy/CMP0150>
CMP0149: Visual Studio generators select latest Windows SDK by default. </policy/CMP0149>
CMP0148: The FindPythonInterp and FindPythonLibs modules are removed. </policy/CMP0148>
CMP0147: Visual Studio generators build custom commands in parallel. </policy/CMP0147>
diff --git a/Help/policy/CMP0150.rst b/Help/policy/CMP0150.rst
new file mode 100644
index 0000000000..fe646d98f4
--- /dev/null
+++ b/Help/policy/CMP0150.rst
@@ -0,0 +1,39 @@
+CMP0150
+-------
+
+.. versionadded:: 3.27
+
+:command:`ExternalProject_Add` and :command:`FetchContent_Declare` commands
+treat relative ``GIT_REPOSITORY`` paths as being relative to the parent
+project's remote.
+
+Earlier versions of these commands always treated relative paths in
+``GIT_REPOSITORY`` as local paths, but the base directory it was treated
+as relative to was both undocumented and unintuitive. The ``OLD`` behavior
+for this policy is to interpret relative paths used for ``GIT_REPOSITORY``
+as local paths relative to the following:
+
+* The parent directory of ``SOURCE_DIR`` for :command:`ExternalProject_Add`.
+* ``FETCHCONTENT_BASE_DIR`` for :command:`FetchContent_Declare`.
+
+The ``NEW`` behavior is to determine the remote from the parent project and
+interpret the path relative to that remote. The value of
+:variable:`CMAKE_CURRENT_SOURCE_DIR` when :command:`ExternalProject_Add` or
+:command:`FetchContent_Declare` is called determines the parent project.
+The remote is selected according to the following (the first match is used):
+
+* If the parent project is checked out on a branch with an upstream remote
+ defined, use that remote.
+* If only one remote is defined, use that remote.
+* If multiple remotes are defined and one of them is named ``origin``, use
+ ``origin``'s remote but also issue a warning.
+
+If an appropriate remote cannot be determined from the above, a fatal error
+will be raised.
+
+This policy was introduced in CMake version 3.27. CMake version |release|
+warns when a relative path is encountered and the policy is not set,
+falling back to using ``OLD`` behavior. Use the :command:`cmake_policy`
+command to set it to ``OLD`` or ``NEW`` explicitly.
+
+.. include:: DEPRECATED.txt
diff --git a/Help/release/dev/ExternalProject-FetchContent-relative-git-remotes.rst b/Help/release/dev/ExternalProject-FetchContent-relative-git-remotes.rst
new file mode 100644
index 0000000000..d467620a39
--- /dev/null
+++ b/Help/release/dev/ExternalProject-FetchContent-relative-git-remotes.rst
@@ -0,0 +1,7 @@
+ExternalProject-FetchContent-Relative-git-remotes
+-------------------------------------------------
+
+* The :module:`ExternalProject` and :module:`FetchContent` modules
+ now resolve relative `GIT_REPOSITORY` paths as relative to the
+ parent project's remote, not as a relative local file system path.
+ See :policy:`CMP0150`.
diff --git a/Modules/ExternalProject.cmake b/Modules/ExternalProject.cmake
index 9a6cbd6bc4..e2cc497a65 100644
--- a/Modules/ExternalProject.cmake
+++ b/Modules/ExternalProject.cmake
@@ -278,6 +278,13 @@ External Project Definition
URL of the git repository. Any URL understood by the ``git`` command
may be used.
+ .. versionchanged:: 3.27
+ A relative URL will be resolved based on the parent project's
+ remote, subject to :policy:`CMP0150`. See the policy documentation
+ for how the remote is selected, including conditions where the
+ remote selection can fail. Local filesystem remotes should
+ always use absolute paths.
+
``GIT_TAG <tag>``
Git branch name, tag or commit hash. Note that branch names and tags
should generally be specified as remote names (i.e. ``origin/myBranch``
@@ -1188,6 +1195,8 @@ The custom step could then be triggered from the main build like so::
#]=======================================================================]
+include(${CMAKE_CURRENT_LIST_DIR}/ExternalProject/shared_internal_commands.cmake)
+
cmake_policy(PUSH)
cmake_policy(SET CMP0054 NEW) # if() quoted variables not dereferenced
cmake_policy(SET CMP0057 NEW) # if() supports IN_LIST
@@ -4159,6 +4168,17 @@ function(ExternalProject_Add name)
set_property(TARGET ${name} PROPERTY EXCLUDE_FROM_ALL TRUE)
endif()
+ get_property(repo TARGET ${name} PROPERTY _EP_GIT_REPOSITORY)
+ if(NOT repo STREQUAL "")
+ cmake_policy(GET CMP0150 cmp0150
+ PARENT_SCOPE # undocumented, do not use outside of CMake
+ )
+ get_property(source_dir TARGET ${name} PROPERTY _EP_SOURCE_DIR)
+ get_filename_component(work_dir "${source_dir}" PATH)
+ _ep_resolve_git_remote(resolved_git_repository "${repo}" "${cmp0150}" "${work_dir}")
+ set_property(TARGET ${name} PROPERTY _EP_GIT_REPOSITORY ${resolved_git_repository})
+ endif()
+
# The 'complete' step depends on all other steps and creates a
# 'done' mark. A dependent external project's 'configure' step
# depends on the 'done' mark so that it rebuilds when this project
diff --git a/Modules/ExternalProject/shared_internal_commands.cmake b/Modules/ExternalProject/shared_internal_commands.cmake
new file mode 100644
index 0000000000..ca3cd9fec7
--- /dev/null
+++ b/Modules/ExternalProject/shared_internal_commands.cmake
@@ -0,0 +1,182 @@
+cmake_policy(VERSION 3.25)
+
+# Determine the remote URL of the project containing the working_directory.
+# This will leave output_variable unset if the URL can't be determined.
+function(_ep_get_git_remote_url output_variable working_directory)
+ set("${output_variable}" "" PARENT_SCOPE)
+
+ find_package(Git QUIET REQUIRED)
+
+ execute_process(
+ COMMAND ${GIT_EXECUTABLE} symbolic-ref --short HEAD
+ WORKING_DIRECTORY "${working_directory}"
+ OUTPUT_VARIABLE git_symbolic_ref
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+
+ if(NOT git_symbolic_ref STREQUAL "")
+ # We are potentially on a branch. See if that branch is associated with
+ # an upstream remote (might be just a local one or not a branch at all).
+ execute_process(
+ COMMAND ${GIT_EXECUTABLE} config branch.${git_symbolic_ref}.remote
+ WORKING_DIRECTORY "${working_directory}"
+ OUTPUT_VARIABLE git_remote_name
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+ endif()
+
+ if(NOT git_remote_name)
+ # Can't select a remote based on a branch. If there's only one remote,
+ # or we have multiple remotes but one is called "origin", choose that.
+ execute_process(
+ COMMAND ${GIT_EXECUTABLE} remote
+ WORKING_DIRECTORY "${working_directory}"
+ OUTPUT_VARIABLE git_remote_list
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+ string(REPLACE "\n" ";" git_remote_list "${git_remote_list}")
+ list(LENGTH git_remote_list git_remote_list_length)
+
+ if(git_remote_list_length EQUAL 0)
+ message(FATAL_ERROR "Git remote not found in parent project.")
+ elseif(git_remote_list_length EQUAL 1)
+ list(GET git_remote_list 0 git_remote_name)
+ else()
+ set(base_warning_msg "Multiple git remotes found for parent project")
+ if("origin" IN_LIST git_remote_list)
+ message(WARNING "${base_warning_msg}, defaulting to origin.")
+ set(git_remote_name "origin")
+ else()
+ message(FATAL_ERROR "${base_warning_msg}, none of which are origin.")
+ endif()
+ endif()
+ endif()
+
+ if(GIT_VERSION VERSION_LESS 1.7.5)
+ set(_git_remote_url_cmd_args config remote.${git_remote_name}.url)
+ elseif(GIT_VERSION VERSION_LESS 2.7)
+ set(_git_remote_url_cmd_args ls-remote --get-url ${git_remote_name})
+ else()
+ set(_git_remote_url_cmd_args remote get-url ${git_remote_name})
+ endif()
+
+ execute_process(
+ COMMAND ${GIT_EXECUTABLE} ${_git_remote_url_cmd_args}
+ WORKING_DIRECTORY "${working_directory}"
+ OUTPUT_VARIABLE git_remote_url
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ COMMAND_ERROR_IS_FATAL LAST
+ ENCODING UTF-8 # Needed to handle non-ascii characters in local paths
+ )
+
+ set("${output_variable}" "${git_remote_url}" PARENT_SCOPE)
+endfunction()
+
+function(_ep_is_relative_git_remote output_variable remote_url)
+ if(remote_url MATCHES "^\\.\\./")
+ set("${output_variable}" TRUE PARENT_SCOPE)
+ else()
+ set("${output_variable}" FALSE PARENT_SCOPE)
+ endif()
+endfunction()
+
+# Return an absolute remote URL given an existing remote URL and relative path.
+# The output_variable will be set to an empty string if an absolute URL
+# could not be computed (no error message is output).
+function(_ep_resolve_relative_git_remote
+ output_variable
+ parent_remote_url
+ relative_remote_url
+)
+ set("${output_variable}" "" PARENT_SCOPE)
+
+ if(parent_remote_url STREQUAL "")
+ return()
+ endif()
+
+ string(REGEX MATCH
+ "^(([A-Za-z0-9][A-Za-z0-9+.-]*)://)?(([^/@]+)@)?(\\[[A-Za-z0-9:]+\\]|[^/:]+)?([/:]/?)(.+(\\.git)?/?)$"
+ git_remote_url_components
+ "${parent_remote_url}"
+ )
+
+ set(protocol "${CMAKE_MATCH_1}")
+ set(auth "${CMAKE_MATCH_3}")
+ set(host "${CMAKE_MATCH_5}")
+ set(separator "${CMAKE_MATCH_6}")
+ set(path "${CMAKE_MATCH_7}")
+
+ string(REPLACE "/" ";" remote_path_components "${path}")
+ string(REPLACE "/" ";" relative_path_components "${relative_remote_url}")
+
+ foreach(relative_path_component IN LISTS relative_path_components)
+ if(NOT relative_path_component STREQUAL "..")
+ break()
+ endif()
+
+ list(LENGTH remote_path_components remote_path_component_count)
+
+ if(remote_path_component_count LESS 1)
+ return()
+ endif()
+
+ list(POP_BACK remote_path_components)
+ list(POP_FRONT relative_path_components)
+ endforeach()
+
+ list(APPEND final_path_components ${remote_path_components} ${relative_path_components})
+ list(JOIN final_path_components "/" path)
+
+ set("${output_variable}" "${protocol}${auth}${host}${separator}${path}" PARENT_SCOPE)
+endfunction()
+
+# The output_variable will be set to the original git_repository if it
+# could not be resolved (no error message is output). The original value is
+# also returned if it doesn't need to be resolved.
+function(_ep_resolve_git_remote
+ output_variable
+ git_repository
+ cmp0150
+ cmp0150_old_base_dir
+)
+ if(git_repository STREQUAL "")
+ set("${output_variable}" "" PARENT_SCOPE)
+ return()
+ endif()
+
+ _ep_is_relative_git_remote(_git_repository_is_relative "${git_repository}")
+
+ if(NOT _git_repository_is_relative)
+ set("${output_variable}" "${git_repository}" PARENT_SCOPE)
+ return()
+ endif()
+
+ if(cmp0150 STREQUAL "NEW")
+ _ep_get_git_remote_url(_parent_git_remote_url "${CMAKE_CURRENT_SOURCE_DIR}")
+ _ep_resolve_relative_git_remote(_resolved_git_remote_url "${_parent_git_remote_url}" "${git_repository}")
+
+ if(_resolved_git_remote_url STREQUAL "")
+ message(FATAL_ERROR
+ "Failed to resolve relative git remote URL:\n"
+ " Relative URL: ${git_repository}\n"
+ " Parent URL: ${_parent_git_remote_url}"
+ )
+ endif()
+ set("${output_variable}" "${_resolved_git_remote_url}" PARENT_SCOPE)
+ return()
+ elseif(cmp0150 STREQUAL "")
+ cmake_policy(GET_WARNING CMP0150 _cmp0150_warning)
+ message(AUTHOR_WARNING
+ "${_cmp0150_warning}\n"
+ "A relative GIT_REPOSITORY path was detected. "
+ "This will be interpreted as a local path to where the project is being cloned. "
+ "Set GIT_REPOSITORY to an absolute path or set policy CMP0150 to NEW to avoid "
+ "this warning."
+ )
+ endif()
+
+ set("${output_variable}" "${cmp0150_old_base_dir}/${git_repository}" PARENT_SCOPE)
+endfunction()
diff --git a/Modules/FetchContent.cmake b/Modules/FetchContent.cmake
index dd5f617acd..74ac8aab2a 100644
--- a/Modules/FetchContent.cmake
+++ b/Modules/FetchContent.cmake
@@ -1076,6 +1076,8 @@ current working directory.
#]=======================================================================]
+include(${CMAKE_CURRENT_LIST_DIR}/ExternalProject/shared_internal_commands.cmake)
+
#=======================================================================
# Recording and retrieving content details for later population
#=======================================================================
@@ -1223,6 +1225,7 @@ function(FetchContent_Declare contentName)
# cannot check for multi-value arguments with this method. We will have to
# handle the URL keyword differently.
set(oneValueArgs
+ GIT_REPOSITORY
SVN_REPOSITORY
DOWNLOAD_NO_EXTRACT
DOWNLOAD_EXTRACT_TIMESTAMP
@@ -1242,6 +1245,30 @@ function(FetchContent_Declare contentName)
set(ARG_SOURCE_DIR "${FETCHCONTENT_BASE_DIR}/${contentNameLower}-src")
endif()
+ if(ARG_GIT_REPOSITORY)
+ # We resolve the GIT_REPOSITORY here so that we get the right parent in the
+ # remote selection logic. In the sub-build, ExternalProject_Add() would see
+ # the private sub-build directory as the parent project, but the parent
+ # project should be the one that called FetchContent_Declare(). We resolve
+ # a relative repo here so that the sub-build's ExternalProject_Add() only
+ # ever sees a non-relative repo.
+ # Since these checks may be non-trivial on some platforms (notably Windows),
+ # don't perform them if we won't be using these details. This also allows
+ # projects to override calls with relative URLs when they have checked out
+ # the parent project in an unexpected way, such as from a mirror or fork.
+ set(savedDetailsPropertyName "_FetchContent_${contentNameLower}_savedDetails")
+ get_property(alreadyDefined GLOBAL PROPERTY ${savedDetailsPropertyName} DEFINED)
+ if(NOT alreadyDefined)
+ cmake_policy(GET CMP0150 cmp0150
+ PARENT_SCOPE # undocumented, do not use outside of CMake
+ )
+ _ep_resolve_git_remote(_resolved_git_repository
+ "${ARG_GIT_REPOSITORY}" "${cmp0150}" "${FETCHCONTENT_BASE_DIR}"
+ )
+ set(ARG_GIT_REPOSITORY "${_resolved_git_repository}")
+ endif()
+ 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
diff --git a/Source/cmPolicies.h b/Source/cmPolicies.h
index fe883826aa..23e50a9715 100644
--- a/Source/cmPolicies.h
+++ b/Source/cmPolicies.h
@@ -450,7 +450,12 @@ class cmMakefile;
27, 0, cmPolicies::WARN) \
SELECT(POLICY, CMP0149, \
"Visual Studio generators select latest Windows SDK by default.", 3, \
- 27, 0, cmPolicies::WARN)
+ 27, 0, cmPolicies::WARN) \
+ SELECT(POLICY, CMP0150, \
+ "ExternalProject_Add and FetchContent_Declare commands " \
+ "treat relative GIT_REPOSITORY paths as being relative " \
+ "to the parent project's remote.", \
+ 3, 27, 0, cmPolicies::WARN)
#define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
#define CM_FOR_EACH_POLICY_ID(POLICY) \
diff --git a/Tests/RunCMake/CMP0150/CMP0150-NEW-build-stdout.txt b/Tests/RunCMake/CMP0150/CMP0150-NEW-build-stdout.txt
new file mode 100644
index 0000000000..9e71b731c7
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-NEW-build-stdout.txt
@@ -0,0 +1,7 @@
+.*-- Configured bottom project
+.*ExternalProject for ep-Y
+.*-- Configured bottom project
+[^\n]*-- Completed configuring project middle
+.*-- Configured bottom project
+.*ExternalProject for ep-X
+.*Non-ep top project
diff --git a/Tests/RunCMake/CMP0150/CMP0150-NEW-resolve.cmake b/Tests/RunCMake/CMP0150/CMP0150-NEW-resolve.cmake
new file mode 100644
index 0000000000..f43f3d5f7b
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-NEW-resolve.cmake
@@ -0,0 +1,107 @@
+include(ExternalProject/shared_internal_commands)
+
+function(test_resolve parentUrl relativeUrl expectedResult)
+ _ep_resolve_relative_git_remote(result "${parentUrl}" "${relativeUrl}")
+ if(NOT result STREQUAL expectedResult)
+ message(SEND_ERROR "URL resolved to unexpected result:\n"
+ " Expected: ${expectedResult}\n"
+ " Actual : ${result}"
+ )
+ endif()
+endfunction()
+
+test_resolve(
+ "https://example.com/group/parent"
+ "../other"
+ "https://example.com/group/other"
+)
+test_resolve(
+ "https://example.com/group/parent"
+ "../../alt/other"
+ "https://example.com/alt/other"
+)
+
+test_resolve(
+ "git@example.com:group/parent"
+ "../other"
+ "git@example.com:group/other"
+)
+test_resolve(
+ "git@example.com:group/parent"
+ "../../alt/other"
+ "git@example.com:alt/other"
+)
+test_resolve(
+ "git@example.com:/group/parent"
+ "../other"
+ "git@example.com:/group/other"
+)
+test_resolve(
+ "git@example.com:/group/parent"
+ "../../alt/other"
+ "git@example.com:/alt/other"
+)
+test_resolve(
+ "git+ssh://git@example.com:group/parent"
+ "../other"
+ "git+ssh://git@example.com:group/other"
+)
+test_resolve(
+ "ssh://git@example.com:1234/group/parent"
+ "../../alt/other"
+ "ssh://git@example.com:1234/alt/other"
+)
+
+test_resolve(
+ "file:///group/parent"
+ "../other"
+ "file:///group/other"
+)
+test_resolve(
+ "file:///group/parent"
+ "../../alt/other"
+ "file:///alt/other"
+)
+test_resolve(
+ "file:///~/group/parent"
+ "../../other"
+ "file:///~/other"
+)
+test_resolve(
+ "/group/parent"
+ "../other"
+ "/group/other"
+)
+test_resolve(
+ "/group/parent"
+ "../../alt/other"
+ "/alt/other"
+)
+test_resolve(
+ "C:/group/parent"
+ "../other"
+ "C:/group/other"
+)
+test_resolve(
+ "C:/group/parent"
+ "../../alt/other"
+ "C:/alt/other"
+)
+
+test_resolve(
+ "x-Test+v1.0://example.com/group/parent"
+ "../other"
+ "x-Test+v1.0://example.com/group/other"
+)
+
+# IPv6 literals
+test_resolve(
+ "http://[::1]/group/parent"
+ "../../alt/other"
+ "http://[::1]/alt/other"
+)
+test_resolve(
+ "git@[::1]:group/parent"
+ "../../alt/other"
+ "git@[::1]:alt/other"
+)
diff --git a/Tests/RunCMake/CMP0150/CMP0150-NEW-stdout.txt b/Tests/RunCMake/CMP0150/CMP0150-NEW-stdout.txt
new file mode 100644
index 0000000000..0f25fabf40
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-NEW-stdout.txt
@@ -0,0 +1,4 @@
+-- Configured bottom project
+-- Completed configuring project middle
+-- Completed configuring project top
+-- Configuring done
diff --git a/Tests/RunCMake/CMP0150/CMP0150-NEW.cmake b/Tests/RunCMake/CMP0150/CMP0150-NEW.cmake
new file mode 100644
index 0000000000..c1c5607af7
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-NEW.cmake
@@ -0,0 +1,45 @@
+set(policyCommand "cmake_policy(SET CMP0150 NEW)")
+
+# Need to keep paths and file names short to avoid hitting limits on Windows.
+# Directory names "a" through to "g" are used here according to the following:
+# a = Top project
+# b/c = Middle project
+# d = Bottom project
+# e/f = Cloned Top project
+# g = Build directory for cloned Top project
+#
+# Dependency names map as follows:
+# X = middle dependency
+# Y = bottom dependency
+
+set(projName top)
+set(depName X)
+set(epRelativeGitRepo ../b/c)
+set(fcRelativeGitRepo ../b/c)
+configure_file(CMakeLists.txt.in a/CMakeLists.txt @ONLY)
+initGitRepo("${CMAKE_CURRENT_BINARY_DIR}/a")
+
+set(projName middle)
+set(depName Y)
+set(epRelativeGitRepo ../../d)
+set(fcRelativeGitRepo ../../d)
+configure_file(CMakeLists.txt.in b/c/CMakeLists.txt @ONLY)
+initGitRepo("${CMAKE_CURRENT_BINARY_DIR}/b/c")
+
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/d")
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/d/CMakeLists.txt" [[
+cmake_minimum_required(VERSION 3.26)
+project(bottom LANGUAGES NONE)
+message(STATUS "Configured bottom project")
+]])
+initGitRepo("${CMAKE_CURRENT_BINARY_DIR}/d")
+
+set(clonedTopDir "${CMAKE_CURRENT_BINARY_DIR}/e/f")
+file(MAKE_DIRECTORY "${clonedTopDir}")
+execGitCommand(${CMAKE_CURRENT_BINARY_DIR} clone --quiet "file://${CMAKE_CURRENT_BINARY_DIR}/a" "${clonedTopDir}")
+add_subdirectory("${clonedTopDir}" "${CMAKE_CURRENT_BINARY_DIR}/g")
+
+# Ensure build order is predictable
+add_custom_target(non-ep-top ALL COMMAND ${CMAKE_COMMAND} -E echo "Non-ep top project")
+add_dependencies(non-ep-top ep-X)
+add_dependencies(ep-X ep-Y)
diff --git a/Tests/RunCMake/CMP0150/CMP0150-OLD-build-stdout.txt b/Tests/RunCMake/CMP0150/CMP0150-OLD-build-stdout.txt
new file mode 100644
index 0000000000..0150af769b
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-OLD-build-stdout.txt
@@ -0,0 +1,3 @@
+.*-- Configured bottom project
+.*ExternalProject for ep-bottom
+.*Non-ep top project
diff --git a/Tests/RunCMake/CMP0150/CMP0150-OLD-common.cmake b/Tests/RunCMake/CMP0150/CMP0150-OLD-common.cmake
new file mode 100644
index 0000000000..69748f7325
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-OLD-common.cmake
@@ -0,0 +1,21 @@
+# There's no point testing more than one level for OLD, since the behavior only
+# depends on the current build, not anything about the parent git repo, etc.
+set(projName top)
+set(depName bottom)
+set(epRelativeGitRepo ../../../Bottom)
+set(fcRelativeGitRepo ../Bottom)
+configure_file(CMakeLists.txt.in Top/CMakeLists.txt @ONLY)
+
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Bottom")
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/Bottom/CMakeLists.txt" [[
+cmake_minimum_required(VERSION 3.26)
+project(bottom LANGUAGES NONE)
+message(STATUS "Configured bottom project")
+]])
+initGitRepo("${CMAKE_CURRENT_BINARY_DIR}/Bottom")
+
+add_subdirectory("${CMAKE_CURRENT_BINARY_DIR}/Top" "${CMAKE_CURRENT_BINARY_DIR}/Top-build")
+
+# Ensure build order is predictable
+add_custom_target(non-ep-top ALL COMMAND ${CMAKE_COMMAND} -E echo "Non-ep top project")
+add_dependencies(non-ep-top ep-bottom)
diff --git a/Tests/RunCMake/CMP0150/CMP0150-OLD-stdout.txt b/Tests/RunCMake/CMP0150/CMP0150-OLD-stdout.txt
new file mode 100644
index 0000000000..680f9c1d3d
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-OLD-stdout.txt
@@ -0,0 +1,3 @@
+-- Configured bottom project
+-- Completed configuring project top
+-- Configuring done
diff --git a/Tests/RunCMake/CMP0150/CMP0150-OLD.cmake b/Tests/RunCMake/CMP0150/CMP0150-OLD.cmake
new file mode 100644
index 0000000000..6b58e70c83
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-OLD.cmake
@@ -0,0 +1,2 @@
+set(policyCommand "cmake_policy(SET CMP0150 OLD)")
+include(CMP0150-OLD-common.cmake)
diff --git a/Tests/RunCMake/CMP0150/CMP0150-WARN-build-stdout.txt b/Tests/RunCMake/CMP0150/CMP0150-WARN-build-stdout.txt
new file mode 100644
index 0000000000..0150af769b
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-WARN-build-stdout.txt
@@ -0,0 +1,3 @@
+.*-- Configured bottom project
+.*ExternalProject for ep-bottom
+.*Non-ep top project
diff --git a/Tests/RunCMake/CMP0150/CMP0150-WARN-stderr.txt b/Tests/RunCMake/CMP0150/CMP0150-WARN-stderr.txt
new file mode 100644
index 0000000000..74c932aaa2
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-WARN-stderr.txt
@@ -0,0 +1,25 @@
+CMake Warning \(dev\) at .*/Modules/ExternalProject/shared_internal_commands\.cmake:[0-9]+ \(message\):
+ Policy CMP0150 is not set: ExternalProject_Add and FetchContent_Declare
+ commands treat relative GIT_REPOSITORY paths as being relative to the
+ parent project's remote\. Run "cmake --help-policy CMP0150" for policy
+ details\. Use the cmake_policy command to set the policy and suppress this
+ warning\.
+
+ A relative GIT_REPOSITORY path was detected\. This will be interpreted as a
+ local path to where the project is being cloned\. Set GIT_REPOSITORY to an
+ absolute path or set policy CMP0150 to NEW to avoid this warning\.
+Call Stack \(most recent call first\):
+ .*/Modules/ExternalProject\.cmake:[0-9]+ \(_ep_resolve_git_remote\)
+.*
+CMake Warning \(dev\) at .*/Modules/ExternalProject/shared_internal_commands\.cmake:[0-9]+ \(message\):
+ Policy CMP0150 is not set: ExternalProject_Add and FetchContent_Declare
+ commands treat relative GIT_REPOSITORY paths as being relative to the
+ parent project's remote\. Run "cmake --help-policy CMP0150" for policy
+ details\. Use the cmake_policy command to set the policy and suppress this
+ warning\.
+
+ A relative GIT_REPOSITORY path was detected\. This will be interpreted as a
+ local path to where the project is being cloned\. Set GIT_REPOSITORY to an
+ absolute path or set policy CMP0150 to NEW to avoid this warning\.
+Call Stack \(most recent call first\):
+ .*/Modules/FetchContent\.cmake:[0-9]+ \(_ep_resolve_git_remote\)
diff --git a/Tests/RunCMake/CMP0150/CMP0150-WARN-stdout.txt b/Tests/RunCMake/CMP0150/CMP0150-WARN-stdout.txt
new file mode 100644
index 0000000000..680f9c1d3d
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-WARN-stdout.txt
@@ -0,0 +1,3 @@
+-- Configured bottom project
+-- Completed configuring project top
+-- Configuring done
diff --git a/Tests/RunCMake/CMP0150/CMP0150-WARN.cmake b/Tests/RunCMake/CMP0150/CMP0150-WARN.cmake
new file mode 100644
index 0000000000..20fd45d4e6
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMP0150-WARN.cmake
@@ -0,0 +1,2 @@
+set(policyCommand "")
+include(CMP0150-OLD-common.cmake)
diff --git a/Tests/RunCMake/CMP0150/CMakeLists.txt b/Tests/RunCMake/CMP0150/CMakeLists.txt
new file mode 100644
index 0000000000..371dccc7b2
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMakeLists.txt
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.25)
+project(${RunCMake_TEST} NONE)
+
+find_package(Git REQUIRED)
+
+function(execGitCommand workDir)
+ execute_process(
+ WORKING_DIRECTORY "${workDir}"
+ COMMAND "${GIT_EXECUTABLE}" ${ARGN}
+ COMMAND_ECHO STDOUT
+ COMMAND_ERROR_IS_FATAL ANY
+ )
+endfunction()
+
+function(initGitRepo workDir)
+ # init.defaultBranch only works with git 2.28 or later, so we must use the
+ # historical default branch name "master". Force the old default in case test
+ # sites have overridden the default to something else.
+ execGitCommand("${workDir}" -c init.defaultBranch=master init)
+ execGitCommand("${workDir}" config user.email "testauthor@cmake.org")
+ execGitCommand("${workDir}" config user.name testauthor)
+ execGitCommand("${workDir}" config core.autocrlf false)
+ execGitCommand("${workDir}" add CMakeLists.txt)
+ execGitCommand("${workDir}" commit -m "Initial commit")
+endfunction()
+
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/CMP0150/CMakeLists.txt.in b/Tests/RunCMake/CMP0150/CMakeLists.txt.in
new file mode 100644
index 0000000000..db6cfc76e0
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/CMakeLists.txt.in
@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 3.25)
+project(@projName@ LANGUAGES NONE)
+
+@policyCommand@
+
+include(ExternalProject)
+ExternalProject_Add(ep-@depName@
+ GIT_REPOSITORY @epRelativeGitRepo@
+ GIT_TAG master
+ GIT_CONFIG init.defaultBranch=master
+ TEST_COMMAND ""
+ INSTALL_COMMAND "${CMAKE_COMMAND}" -E echo "ExternalProject for ep-@depName@"
+)
+
+include(FetchContent)
+FetchContent_Declare(@depName@
+ GIT_REPOSITORY @fcRelativeGitRepo@
+ GIT_TAG master
+ GIT_CONFIG init.defaultBranch=master
+)
+FetchContent_MakeAvailable(@depName@)
+
+message(STATUS "Completed configuring project @projName@")
diff --git a/Tests/RunCMake/CMP0150/RunCMakeTest.cmake b/Tests/RunCMake/CMP0150/RunCMakeTest.cmake
new file mode 100644
index 0000000000..940c33e65b
--- /dev/null
+++ b/Tests/RunCMake/CMP0150/RunCMakeTest.cmake
@@ -0,0 +1,17 @@
+include(RunCMake)
+
+function(test_CMP0150 val)
+ set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${val}-build)
+ run_cmake(CMP0150-${val})
+ set(RunCMake_TEST_NO_CLEAN TRUE)
+ # Some git versions write clone messages to stderr. These would cause the
+ # test to fail, so we need to merge them into stdout.
+ set(RunCMake_TEST_OUTPUT_MERGE TRUE)
+ run_cmake_command(CMP0150-${val}-build ${CMAKE_COMMAND} --build .)
+endfunction()
+
+test_CMP0150(WARN)
+test_CMP0150(OLD)
+test_CMP0150(NEW)
+
+run_cmake_script(CMP0150-NEW-resolve)
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index 90feeb645c..32ccef0725 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -160,6 +160,7 @@ endif()
add_RunCMake_test(CMP0132)
add_RunCMake_test(CMP0135)
add_RunCMake_test(CMP0139)
+add_RunCMake_test(CMP0150)
# The test for Policy 65 requires the use of the
# CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS variable, which both the VS and Xcode