summaryrefslogtreecommitdiff
path: root/Modules
diff options
context:
space:
mode:
authorChris Wright <chris@inkyspider.co.uk>2023-04-21 16:47:19 +1000
committerCraig Scott <craig.scott@crascit.com>2023-04-26 16:30:36 +0800
commit550f63447d4c7d2db6ccbeaf1f6378aa6f7af4ed (patch)
tree2f9dca56e1a6891f614c52c78d8884f71c7e84ed /Modules
parente256e35daa79732a200883cef398fcd0f8227a3d (diff)
downloadcmake-550f63447d4c7d2db6ccbeaf1f6378aa6f7af4ed.tar.gz
ExternalProject/FetchContent: Support relative remote URLs
Teach `ExternalProject_Add` and `FetchContent_Declare` to resolve relative remote URLs provided via `GIT_REPOSITORY`. Add policy CMP0150 to maintain compatibility. Fixes: #24211 Co-Authored-By: Craig Scott <craig.scott@crascit.com>
Diffstat (limited to 'Modules')
-rw-r--r--Modules/ExternalProject.cmake20
-rw-r--r--Modules/ExternalProject/shared_internal_commands.cmake182
-rw-r--r--Modules/FetchContent.cmake27
3 files changed, 229 insertions, 0 deletions
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