From b2496bf14c90f6c362d20b84bf406d51c795778b Mon Sep 17 00:00:00 2001 From: Craig Scott Date: Fri, 17 May 2024 14:23:06 +1000 Subject: FetchContent: Populate directly without a sub-build Fixes: #21703 --- Help/manual/cmake-policies.7.rst | 1 + Help/policy/CMP0168.rst | 64 +++++ Help/release/dev/fetchcontent-direct.rst | 9 + Modules/ExternalProject/download.cmake.in | 22 +- Modules/ExternalProject/extractfile.cmake.in | 14 +- Modules/ExternalProject/gitclone.cmake.in | 18 +- Modules/ExternalProject/gitupdate.cmake.in | 27 +- Modules/ExternalProject/hgclone.cmake.in | 15 +- .../ExternalProject/shared_internal_commands.cmake | 306 ++++++++++++++++++++- Modules/ExternalProject/stepscript.cmake.in | 9 + Modules/ExternalProject/verify.cmake.in | 6 +- Modules/FetchContent.cmake | 223 ++++++++++++++- Source/cmPolicies.h | 6 +- Tests/RunCMake/FetchContent/CMakeLists.txt | 6 + Tests/RunCMake/FetchContent/DownloadFile.cmake | 4 + .../FetchContent/DownloadTwice-direct-result.txt | 1 + .../FetchContent/DownloadTwice-direct-stderr.txt | 2 + .../MakeAvailableUndeclared-direct-result.txt | 1 + .../MakeAvailableUndeclared-direct-stderr.txt | 2 + .../ManualSourceDirectoryMissing-direct-result.txt | 1 + .../ManualSourceDirectoryMissing-direct-stderr.txt | 2 + .../ManualSourceDirectoryMissing.cmake | 1 + ...ManualSourceDirectoryRelative-direct-stderr.txt | 3 + Tests/RunCMake/FetchContent/RunCMakeTest.cmake | 112 +++++--- Tests/RunCMake/FetchContent/ScriptMode.cmake | 2 + 25 files changed, 761 insertions(+), 96 deletions(-) create mode 100644 Help/policy/CMP0168.rst create mode 100644 Help/release/dev/fetchcontent-direct.rst create mode 100644 Modules/ExternalProject/stepscript.cmake.in create mode 100644 Tests/RunCMake/FetchContent/DownloadTwice-direct-result.txt create mode 100644 Tests/RunCMake/FetchContent/DownloadTwice-direct-stderr.txt create mode 100644 Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-result.txt create mode 100644 Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-stderr.txt create mode 100644 Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-result.txt create mode 100644 Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-stderr.txt create mode 100644 Tests/RunCMake/FetchContent/ManualSourceDirectoryRelative-direct-stderr.txt diff --git a/Help/manual/cmake-policies.7.rst b/Help/manual/cmake-policies.7.rst index 43d54dc..72aedba 100644 --- a/Help/manual/cmake-policies.7.rst +++ b/Help/manual/cmake-policies.7.rst @@ -57,6 +57,7 @@ Policies Introduced by CMake 3.30 .. toctree:: :maxdepth: 1 + CMP0168: FetchContent implements steps directly instead of through a sub-build. CMP0167: The FindBoost module is removed. CMP0166: TARGET_PROPERTY evaluates link properties transitively over private dependencies of static libraries. CMP0165: enable_language() must not be called before project(). diff --git a/Help/policy/CMP0168.rst b/Help/policy/CMP0168.rst new file mode 100644 index 0000000..8317351 --- /dev/null +++ b/Help/policy/CMP0168.rst @@ -0,0 +1,64 @@ +CMP0168 +------- + +.. versionadded:: 3.30 + +The :module:`FetchContent` module implements steps directly instead of through +a sub-build. + +CMake 3.29 and below implement FetchContent as a separate sub-build. +This required configuring that separate project and using a build tool. +This approach can be very slow with some generators and operating systems. +CMake 3.30 and above prefer to implement the download, update, and patch +steps directly as part of the main project. + +The ``NEW`` behavior has the following characteristics: + +* No sub-build is used. All operations are implemented directly from the + main project's CMake configure step. When running in CMake script mode, + no build tool needs to be available. +* Generator expressions and GNU Make variables of the form ``$(SOMEVAR)`` are + not supported. They should not be used in any argument to + :command:`FetchContent_Declare` or :command:`FetchContent_Populate`. +* All ``LOG_...`` and ``USES_TERMINAL_...`` options, the ``QUIET`` option, and + the :variable:`FETCHCONTENT_QUIET` variable are ignored. + :module:`FetchContent` output is always part of the main project's configure + output. This also means it now respects the message logging level (see + :variable:`CMAKE_MESSAGE_LOG_LEVEL` and + :option:`--log-level `). The default message log level + should be comparable to using ``QUIET`` with the ``OLD`` policy setting, + except that warnings will now be shown. +* The ``PREFIX``, ``TMP_DIR``, ``STAMP_DIR``, ``LOG_DIR``, and ``DOWNLOAD_DIR`` + options and their associated directory properties are ignored. The + :module:`FetchContent` module controls those locations internally. + +The ``OLD`` behavior has the following characteristics: + +* A sub-build is always used to implement the download, update, and patch + steps. A build tool must be available, even when using + :command:`FetchContent_Populate` in CMake script mode. +* Generator expressions and GNU Make variables of the form ``$(SOMEVAR)`` can + be used, although such use is almost always inappropriate. They are evaluated + in the sub-build, so they do not see any information from the main build. +* All logging, terminal control, and directory options related to the download, + update, or patch steps are supported. +* If the ``QUIET`` option is used, or the :variable:`FETCHCONTENT_QUIET` + variable is set to true, warnings will not be shown in the output. + +There's a reasonably good chance that users can set the +:variable:`CMAKE_POLICY_DEFAULT_CMP0168 >` +variable to ``NEW`` to globally switch to the ``NEW`` behavior while waiting +for the project and its dependencies to be updated use the ``NEW`` policy +setting by default. Projects don't typically make use of the features that the +``NEW`` behavior no longer supports, and even those projects that do will often +still work fine when those options are ignored. Before setting this behavior +globally, check whether any :command:`FetchContent_Declare` or +:command:`FetchContent_Populate` calls use the ignored options in a way that +would change observable behavior, other than putting temporary or +internally-generated files in different locations. + +.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 3.30 +.. |WARNS_OR_DOES_NOT_WARN| replace:: does *not* warn +.. include:: STANDARD_ADVICE.txt + +.. include:: DEPRECATED.txt diff --git a/Help/release/dev/fetchcontent-direct.rst b/Help/release/dev/fetchcontent-direct.rst new file mode 100644 index 0000000..7cb33ab --- /dev/null +++ b/Help/release/dev/fetchcontent-direct.rst @@ -0,0 +1,9 @@ +fetchcontent-direct +------------------- + +* :module:`FetchContent` now prefers to populate content directly rather + than using a separate sub-build. This may significantly improve configure + times on some systems (Windows especially, but also on macOS when using + the Xcode generator). Policy :policy:`CMP0168` provides backward + compatibility for those projects that still rely on using a sub-build for + content population. diff --git a/Modules/ExternalProject/download.cmake.in b/Modules/ExternalProject/download.cmake.in index f21a91a..77d43d7 100644 --- a/Modules/ExternalProject/download.cmake.in +++ b/Modules/ExternalProject/download.cmake.in @@ -21,14 +21,14 @@ function(check_file_hash has_hash hash_is_good) set("${has_hash}" TRUE PARENT_SCOPE) - message(STATUS "verifying file... + message(VERBOSE "verifying file... file='@LOCAL@'") file("@ALGO@" "@LOCAL@" actual_value) if(NOT "${actual_value}" STREQUAL "@EXPECT_VALUE@") set("${hash_is_good}" FALSE PARENT_SCOPE) - message(STATUS "@ALGO@ hash of + message(VERBOSE "@ALGO@ hash of @LOCAL@ does not match expected value expected: '@EXPECT_VALUE@' @@ -44,7 +44,7 @@ function(sleep_before_download attempt) endif() if(attempt EQUAL 1) - message(STATUS "Retrying...") + message(VERBOSE "Retrying...") return() endif() @@ -66,7 +66,7 @@ function(sleep_before_download attempt) set(sleep_seconds 1200) endif() - message(STATUS "Retry after ${sleep_seconds} seconds (attempt #${attempt}) ...") + message(VERBOSE "Retry after ${sleep_seconds} seconds (attempt #${attempt}) ...") execute_process(COMMAND "${CMAKE_COMMAND}" -E sleep "${sleep_seconds}") endfunction() @@ -75,17 +75,17 @@ if(EXISTS "@LOCAL@") check_file_hash(has_hash hash_is_good) if(has_hash) if(hash_is_good) - message(STATUS "File already exists and hash match (skip download): + message(VERBOSE "File already exists and hash match (skip download): file='@LOCAL@' @ALGO@='@EXPECT_VALUE@'" ) return() else() - message(STATUS "File already exists but hash mismatch. Removing...") + message(VERBOSE "File already exists but hash mismatch. Removing...") file(REMOVE "@LOCAL@") endif() else() - message(STATUS "File already exists but no hash specified (use URL_HASH): + message(VERBOSE "File already exists but no hash specified (use URL_HASH): file='@LOCAL@' Old file will be removed and new file downloaded from URL." ) @@ -95,7 +95,7 @@ endif() set(retry_number 5) -message(STATUS "Downloading... +message(VERBOSE "Downloading... dst='@LOCAL@' timeout='@TIMEOUT_MSG@' inactivity timeout='@INACTIVITY_TIMEOUT_MSG@'" @@ -109,7 +109,7 @@ foreach(i RANGE ${retry_number}) endif() foreach(url IN ITEMS @REMOTE@) if(NOT url IN_LIST skip_url_list) - message(STATUS "Using src='${url}'") + message(VERBOSE "Using src='${url}'") @TLS_VERSION_CODE@ @TLS_VERIFY_CODE@ @@ -135,10 +135,10 @@ foreach(i RANGE ${retry_number}) if(status_code EQUAL 0) check_file_hash(has_hash hash_is_good) if(has_hash AND NOT hash_is_good) - message(STATUS "Hash mismatch, removing...") + message(VERBOSE "Hash mismatch, removing...") file(REMOVE "@LOCAL@") else() - message(STATUS "Downloading... done") + message(VERBOSE "Downloading... done") return() endif() else() diff --git a/Modules/ExternalProject/extractfile.cmake.in b/Modules/ExternalProject/extractfile.cmake.in index 984565b..39daaff 100644 --- a/Modules/ExternalProject/extractfile.cmake.in +++ b/Modules/ExternalProject/extractfile.cmake.in @@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.5) get_filename_component(filename "@filename@" ABSOLUTE) get_filename_component(directory "@directory@" ABSOLUTE) -message(STATUS "extracting... +message(VERBOSE "extracting... src='${filename}' dst='${directory}'" ) @@ -28,21 +28,21 @@ file(MAKE_DIRECTORY "${ut_dir}") # Extract it: # -message(STATUS "extracting... [tar @args@]") +message(VERBOSE "extracting... [tar @args@]") execute_process(COMMAND ${CMAKE_COMMAND} -E tar @args@ ${filename} @options@ WORKING_DIRECTORY ${ut_dir} RESULT_VARIABLE rv ) if(NOT rv EQUAL 0) - message(STATUS "extracting... [error clean up]") + message(VERBOSE "extracting... [error clean up]") file(REMOVE_RECURSE "${ut_dir}") message(FATAL_ERROR "Extract of '${filename}' failed") endif() # Analyze what came out of the tar file: # -message(STATUS "extracting... [analysis]") +message(VERBOSE "extracting... [analysis]") file(GLOB contents "${ut_dir}/*") list(REMOVE_ITEM contents "${ut_dir}/.DS_Store") list(LENGTH contents n) @@ -52,14 +52,14 @@ endif() # Move "the one" directory to the final directory: # -message(STATUS "extracting... [rename]") +message(VERBOSE "extracting... [rename]") file(REMOVE_RECURSE ${directory}) get_filename_component(contents ${contents} ABSOLUTE) file(RENAME ${contents} ${directory}) # Clean up: # -message(STATUS "extracting... [clean up]") +message(VERBOSE "extracting... [clean up]") file(REMOVE_RECURSE "${ut_dir}") -message(STATUS "extracting... done") +message(VERBOSE "extracting... done") diff --git a/Modules/ExternalProject/gitclone.cmake.in b/Modules/ExternalProject/gitclone.cmake.in index 94b329a..93424ed 100644 --- a/Modules/ExternalProject/gitclone.cmake.in +++ b/Modules/ExternalProject/gitclone.cmake.in @@ -5,16 +5,26 @@ cmake_minimum_required(VERSION 3.5) if(EXISTS "@gitclone_stampfile@" AND EXISTS "@gitclone_infofile@" AND "@gitclone_stampfile@" IS_NEWER_THAN "@gitclone_infofile@") - message(STATUS + message(VERBOSE "Avoiding repeated git clone, stamp file is up to date: " "'@gitclone_stampfile@'" ) return() endif() +# Even at VERBOSE level, we don't want to see the commands executed, but +# enabling them to be shown for DEBUG may be useful to help diagnose problems. +cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) +if(active_log_level MATCHES "DEBUG|TRACE") + set(maybe_show_command "COMMAND_ECHO STDOUT") +else() + set(maybe_show_command "") +endif() + execute_process( COMMAND ${CMAKE_COMMAND} -E rm -rf "@source_dir@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to remove directory: '@source_dir@'") @@ -29,11 +39,12 @@ while(error_code AND number_of_tries LESS 3) clone @git_clone_options@ "@git_repository@" "@src_name@" WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code + ${maybe_show_command} ) math(EXPR number_of_tries "${number_of_tries} + 1") endwhile() if(number_of_tries GREATER 1) - message(STATUS "Had to git clone more than once: ${number_of_tries} times.") + message(NOTICE "Had to git clone more than once: ${number_of_tries} times.") endif() if(error_code) message(FATAL_ERROR "Failed to clone repository: '@git_repository@'") @@ -44,6 +55,7 @@ execute_process( checkout "@git_tag@" @git_checkout_explicit--@ WORKING_DIRECTORY "@work_dir@/@src_name@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to checkout tag: '@git_tag@'") @@ -56,6 +68,7 @@ if(init_submodules) submodule update @git_submodules_recurse@ --init @git_submodules@ WORKING_DIRECTORY "@work_dir@/@src_name@" RESULT_VARIABLE error_code + ${maybe_show_command} ) endif() if(error_code) @@ -67,6 +80,7 @@ endif() execute_process( COMMAND ${CMAKE_COMMAND} -E copy "@gitclone_infofile@" "@gitclone_stampfile@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to copy script-last-run stamp file: '@gitclone_stampfile@'") diff --git a/Modules/ExternalProject/gitupdate.cmake.in b/Modules/ExternalProject/gitupdate.cmake.in index 171aa7b..a56fb67 100644 --- a/Modules/ExternalProject/gitupdate.cmake.in +++ b/Modules/ExternalProject/gitupdate.cmake.in @@ -3,12 +3,22 @@ cmake_minimum_required(VERSION 3.5) +# Even at VERBOSE level, we don't want to see the commands executed, but +# enabling them to be shown for DEBUG may be useful to help diagnose problems. +cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) +if(active_log_level MATCHES "DEBUG|TRACE") + set(maybe_show_command "COMMAND_ECHO STDOUT") +else() + set(maybe_show_command "") +endif() + function(do_fetch) message(VERBOSE "Fetching latest from the remote @git_remote_name@") execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git fetch --tags --force "@git_remote_name@" WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL LAST + ${maybe_show_command} ) endfunction() @@ -34,6 +44,9 @@ if(head_sha STREQUAL "") message(FATAL_ERROR "Failed to get the hash for HEAD:\n${error_msg}") endif() +if("${can_fetch}" STREQUAL "") + set(can_fetch "@can_fetch_default@") +endif() execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git show-ref "@git_tag@" @@ -97,7 +110,7 @@ else() # because it can be confusing for users to see a failed git command. # That failure is being handled here, so it isn't an error. if(NOT error_msg STREQUAL "") - message(VERBOSE "${error_msg}") + message(DEBUG "${error_msg}") endif() do_fetch() set(checkout_name "@git_tag@") @@ -181,6 +194,7 @@ if(need_stash) COMMAND "@git_EXECUTABLE@" --git-dir=.git stash save @git_stash_save_options@ WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} ) endif() @@ -189,6 +203,7 @@ if(git_update_strategy STREQUAL "CHECKOUT") COMMAND "@git_EXECUTABLE@" --git-dir=.git checkout "${checkout_name}" WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} ) else() execute_process( @@ -203,6 +218,7 @@ else() execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git rebase --abort WORKING_DIRECTORY "@work_dir@" + ${maybe_show_command} ) if(NOT git_update_strategy STREQUAL "REBASE_CHECKOUT") @@ -211,6 +227,7 @@ else() execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --index --quiet WORKING_DIRECTORY "@work_dir@" + ${maybe_show_command} ) endif() message(FATAL_ERROR "\nFailed to rebase in: '@work_dir@'." @@ -236,12 +253,14 @@ else() ${tag_name} WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} ) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git checkout "${checkout_name}" WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} ) endif() endif() @@ -252,27 +271,32 @@ if(need_stash) COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --index --quiet WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) # Stash pop --index failed: Try again dropping the index execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git reset --hard --quiet WORKING_DIRECTORY "@work_dir@" + ${maybe_show_command} ) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --quiet WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) # Stash pop failed: Restore previous state. execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git reset --hard --quiet ${head_sha} WORKING_DIRECTORY "@work_dir@" + ${maybe_show_command} ) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --index --quiet WORKING_DIRECTORY "@work_dir@" + ${maybe_show_command} ) message(FATAL_ERROR "\nFailed to unstash changes in: '@work_dir@'." "\nYou will have to resolve the conflicts manually") @@ -288,5 +312,6 @@ if(init_submodules) submodule update @git_submodules_recurse@ --init @git_submodules@ WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} ) endif() diff --git a/Modules/ExternalProject/hgclone.cmake.in b/Modules/ExternalProject/hgclone.cmake.in index e2b55ba..993ab7f 100644 --- a/Modules/ExternalProject/hgclone.cmake.in +++ b/Modules/ExternalProject/hgclone.cmake.in @@ -5,16 +5,26 @@ cmake_minimum_required(VERSION 3.5) if(EXISTS "@hgclone_stampfile@" AND EXISTS "@hgclone_infofile@" AND "@hgclone_stampfile@" IS_NEWER_THAN "@hgclone_infofile@") - message(STATUS + message(VERBOSE "Avoiding repeated hg clone, stamp file is up to date: " "'@hgclone_stampfile@'" ) return() endif() +# Even at VERBOSE level, we don't want to see the commands executed, but +# enabling them to be shown for DEBUG may be useful to help diagnose problems. +cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) +if(active_log_level MATCHES "DEBUG|TRACE") + set(maybe_show_command "COMMAND_ECHO STDOUT") +else() + set(maybe_show_command "") +endif() + execute_process( COMMAND ${CMAKE_COMMAND} -E rm -rf "@source_dir@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to remove directory: '@source_dir@'") @@ -24,6 +34,7 @@ execute_process( COMMAND "@hg_EXECUTABLE@" clone -U "@hg_repository@" "@src_name@" WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to clone repository: '@hg_repository@'") @@ -33,6 +44,7 @@ execute_process( COMMAND "@hg_EXECUTABLE@" update @hg_tag@ WORKING_DIRECTORY "@work_dir@/@src_name@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to checkout tag: '@hg_tag@'") @@ -43,6 +55,7 @@ endif() execute_process( COMMAND ${CMAKE_COMMAND} -E copy "@hgclone_infofile@" "@hgclone_stampfile@" RESULT_VARIABLE error_code + ${maybe_show_command} ) if(error_code) message(FATAL_ERROR "Failed to copy script-last-run stamp file: '@hgclone_stampfile@'") diff --git a/Modules/ExternalProject/shared_internal_commands.cmake b/Modules/ExternalProject/shared_internal_commands.cmake index c6ab99b..6c4f61a 100644 --- a/Modules/ExternalProject/shared_internal_commands.cmake +++ b/Modules/ExternalProject/shared_internal_commands.cmake @@ -764,7 +764,62 @@ function(_ep_get_git_submodules_recurse git_submodules_recurse) endfunction() +function(_ep_add_script_commands script_var work_dir cmd) + # We only support a subset of what ep_replace_location_tags() handles + set(location_tags + SOURCE_DIR + SOURCE_SUBDIR + BINARY_DIR + TMP_DIR + DOWNLOAD_DIR + DOWNLOADED_FILE + ) + + # There can be multiple COMMANDs, but we have to split those up to + # one command per call to execute_process() + set(execute_process_cmd + "execute_process(\n" + " WORKING_DIRECTORY \"${work_dir}\"\n" + " COMMAND_ERROR_IS_FATAL LAST\n" + ) + cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) + if(active_log_level MATCHES "VERBOSE|DEBUG|TRACE") + string(APPEND execute_process_cmd " COMMAND_ECHO STDOUT\n") + endif() + string(APPEND execute_process_cmd " COMMAND ") + + string(APPEND ${script_var} "${execute_process_cmd}") + + foreach(cmd_arg IN LISTS cmd) + if(cmd_arg STREQUAL "COMMAND") + string(APPEND ${script_var} "\n)\n${execute_process_cmd}") + else() + if(_EP_LIST_SEPARATOR) + string(REPLACE "${_EP_LIST_SEPARATOR}" "\\;" cmd_arg "${cmd_arg}") + endif() + foreach(dir IN LISTS location_tags) + string(REPLACE "<${dir}>" "${_EP_${dir}}" cmd_arg "${cmd_arg}") + endforeach() + string(APPEND ${script_var} " [====[${cmd_arg}]====]") + endif() + endforeach() + + string(APPEND ${script_var} "\n)") + set(${script_var} "${${script_var}}" PARENT_SCOPE) +endfunction() + + function(_ep_add_download_command name) + set(noValueOptions ) + set(singleValueOptions + SCRIPT_FILE # These should only be used by FetchContent + DEPENDS_VARIABLE # + ) + set(multiValueOptions ) + cmake_parse_arguments(PARSE_ARGV 1 arg + "${noValueOptions}" "${singleValueOptions}" "${multiValueOptions}" + ) + # The various _EP_... variables mentioned here and throughout this function # are expected to already have been set by the caller via a call to # _ep_parse_arguments() or ep_parse_arguments_to_vars(). Other variables @@ -787,6 +842,7 @@ function(_ep_add_download_command name) # TODO: Perhaps file:// should be copied to download dir before extraction. string(REGEX REPLACE "file://" "" url "${url}") + set(step_script_contents) set(depends) set(comment) set(work_dir) @@ -795,6 +851,14 @@ function(_ep_add_download_command name) if(DEFINED _EP_DOWNLOAD_COMMAND) set(work_dir ${download_dir}) set(method custom) + if(NOT "x${cmd}" STREQUAL "x" AND arg_SCRIPT_FILE) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() + elseif(cvs_repository) set(method cvs) find_package(CVS QUIET) @@ -819,6 +883,13 @@ function(_ep_add_download_command name) -d ${src_name} ${cvs_module} ) + if(arg_SCRIPT_FILE) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() elseif(svn_repository) set(method svn) @@ -862,6 +933,13 @@ function(_ep_add_download_command name) ${svn_user_pw_args} ${src_name} ) + if(arg_SCRIPT_FILE) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() elseif(git_repository) set(method git) @@ -928,8 +1006,9 @@ CMP0097=${_EP_CMP0097} # create a cmake script to invoke as download command. # The script will delete the source directory and then call git clone. # + set(clone_script ${tmp_dir}/${name}-gitclone.cmake) _ep_write_gitclone_script( - ${tmp_dir}/${name}-gitclone.cmake + ${clone_script} ${source_dir} ${GIT_EXECUTABLE} ${git_repository} @@ -949,7 +1028,15 @@ CMP0097=${_EP_CMP0097} "${tls_verify}" ) set(comment "Performing download step (git clone) for '${name}'") - set(cmd ${CMAKE_COMMAND} -P ${tmp_dir}/${name}-gitclone.cmake) + set(cmd ${CMAKE_COMMAND} + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P ${clone_script} + ) + + if(arg_SCRIPT_FILE) + set(step_script_contents "include(\"${clone_script}\")") + list(APPEND depends ${clone_script}) + endif() elseif(hg_repository) set(method hg) @@ -978,8 +1065,9 @@ CMP0097=${_EP_CMP0097} # create a cmake script to invoke as download command. # The script will delete the source directory and then call hg clone. # + set(clone_script ${tmp_dir}/${name}-hgclone.cmake) _ep_write_hgclone_script( - ${tmp_dir}/${name}-hgclone.cmake + ${clone_script} ${source_dir} ${HG_EXECUTABLE} ${hg_repository} @@ -990,7 +1078,15 @@ CMP0097=${_EP_CMP0097} ${stamp_dir}/${name}-hgclone-lastrun.txt ) set(comment "Performing download step (hg clone) for '${name}'") - set(cmd ${CMAKE_COMMAND} -P ${tmp_dir}/${name}-hgclone.cmake) + set(cmd ${CMAKE_COMMAND} + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P ${clone_script} + ) + + if(arg_SCRIPT_FILE) + set(step_script_contents "include(\"${clone_script}\")") + list(APPEND depends ${clone_script}) + endif() elseif(url) set(method url) @@ -1045,9 +1141,21 @@ hash=${hash} ${CMAKE_COMMAND} -E rm -rf ${source_dir} COMMAND ${CMAKE_COMMAND} -E copy_directory ${abs_dir} ${source_dir} ) + if(arg_SCRIPT_FILE) + # While it may be tempting to implement the two operations directly + # with file(), the behavior is different. file(COPY) preserves input + # file timestamps, which we don't want. Therefore, still use the same + # external commands so that we get the same behavior. + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() else() set(no_extract "${_EP_DOWNLOAD_NO_EXTRACT}") string(APPEND extra_repo_info "no_extract=${no_extract}\n") + set(verify_script "${stamp_dir}/verify-${name}.cmake") if("${url}" MATCHES "^[a-z]+://") # TODO: Should download and extraction be different steps? if("x${fname}" STREQUAL "x") @@ -1097,9 +1205,15 @@ hash=${hash} "${netrc_file}" ) set(cmd - ${CMAKE_COMMAND} -P "${download_script}" + ${CMAKE_COMMAND} + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P "${download_script}" COMMAND ) + if(arg_SCRIPT_FILE) + set(step_script_contents "include(\"${download_script}\")\n") + endif() + if (no_extract) set(steps "download and verify") else () @@ -1107,7 +1221,7 @@ hash=${hash} endif () set(comment "Performing download step (${steps}) for '${name}'") # already verified by 'download_script' - file(WRITE "${stamp_dir}/verify-${name}.cmake" "") + file(WRITE "${verify_script}" "") # Rather than adding everything to the RepositoryInfo.txt file, it is # more robust to just depend on the download script. That way, we will @@ -1122,12 +1236,19 @@ hash=${hash} endif () set(comment "Performing download step (${steps}) for '${name}'") _ep_write_verifyfile_script( - "${stamp_dir}/verify-${name}.cmake" + "${verify_script}" "${file}" "${hash}" ) endif() - list(APPEND cmd ${CMAKE_COMMAND} -P ${stamp_dir}/verify-${name}.cmake) + list(APPEND cmd ${CMAKE_COMMAND} + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P ${verify_script} + ) + if(arg_SCRIPT_FILE) + string(APPEND step_script_contents "include(\"${verify_script}\")\n") + list(APPEND depends ${verify_script}) + endif() set(extract_timestamp "${_EP_DOWNLOAD_EXTRACT_TIMESTAMP}") if(no_extract) if(DEFINED _EP_DOWNLOAD_EXTRACT_TIMESTAMP) @@ -1136,7 +1257,24 @@ hash=${hash} "DOWNLOAD_NO_EXTRACT TRUE" ) endif() - set_property(TARGET ${name} PROPERTY _EP_DOWNLOADED_FILE ${file}) + if(arg_SCRIPT_FILE) + # There's no target to record the location of the downloaded file. + # Instead, we copy it to the source directory within the script, + # which is what FetchContent always does in this situation. + cmake_path(SET safe_file NORMALIZE "${file}") + cmake_path(GET safe_file FILENAME filename) + string(APPEND step_script_contents + "file(COPY_FILE\n" + " \"${file}\"\n" + " \"${source_dir}/${filename}\"\n" + " ONLY_IF_DIFFERENT\n" + " INPUT_MAY_BE_RECENT\n" + ")" + ) + list(APPEND depends ${source_dir}/${filename}) + else() + set_property(TARGET ${name} PROPERTY _EP_DOWNLOADED_FILE ${file}) + endif() else() if(NOT DEFINED _EP_DOWNLOAD_EXTRACT_TIMESTAMP) # Default depends on policy CMP0135 @@ -1165,16 +1303,23 @@ hash=${hash} else() set(options "--touch") endif() + set(extract_script "${stamp_dir}/extract-${name}.cmake") _ep_write_extractfile_script( - "${stamp_dir}/extract-${name}.cmake" + "${extract_script}" "${name}" "${file}" "${source_dir}" "${options}" ) list(APPEND cmd - COMMAND ${CMAKE_COMMAND} -P ${stamp_dir}/extract-${name}.cmake + COMMAND ${CMAKE_COMMAND} + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P ${extract_script} ) + if(arg_SCRIPT_FILE) + string(APPEND step_script_contents "include(\"${extract_script}\")\n") + list(APPEND depends ${extract_script}) + endif() endif () endif() else() @@ -1194,6 +1339,9 @@ hash=${hash} " * CVS_REPOSITORY and CVS_MODULE" ) endif() + if(arg_SCRIPT_FILE) + set(step_script_contents "message(VERBOSE [[Using SOURCE_DIR as is]])") + endif() endif() # We use configure_file() to write the repo_info_file so that the file's @@ -1207,6 +1355,20 @@ hash=${hash} @ONLY ) + if(arg_SCRIPT_FILE) + set(step_name download) + configure_file( + "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/stepscript.cmake.in" + "${arg_SCRIPT_FILE}" + @ONLY + ) + set(${arg_DEPENDS_VARIABLE} "${depends}" PARENT_SCOPE) + return() + endif() + + # Nothing below this point is applicable when we've been asked to put the + # download step in a script file (which is the FetchContent case). + if(_EP_LOG_DOWNLOAD) set(log LOG 1) else() @@ -1253,6 +1415,16 @@ function(_ep_get_update_disconnected var name) endfunction() function(_ep_add_update_command name) + set(noValueOptions ) + set(singleValueOptions + SCRIPT_FILE # These should only be used by FetchContent + DEPEND_VARIABLE # + ) + set(multiValueOptions ) + cmake_parse_arguments(PARSE_ARGV 1 arg + "${noValueOptions}" "${singleValueOptions}" "${multiValueOptions}" + ) + # The various _EP_... variables mentioned here and throughout this function # are expected to already have been set by the caller via a call to # _ep_parse_arguments() or ep_parse_arguments_to_vars(). Other variables @@ -1280,7 +1452,13 @@ function(_ep_add_update_command name) set(work_dir ${source_dir}) if(NOT "x${cmd}" STREQUAL "x") set(always 1) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) endif() + elseif(cvs_repository) if(NOT CVS_EXECUTABLE) message(FATAL_ERROR "error: could not find cvs for update of ${name}") @@ -1290,6 +1468,15 @@ function(_ep_add_update_command name) set(cvs_tag "${_EP_CVS_TAG}") set(cmd ${CVS_EXECUTABLE} -d ${cvs_repository} -q up -dP ${cvs_tag}) set(always 1) + + if(arg_SCRIPT_FILE) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() + elseif(svn_repository) if(NOT Subversion_SVN_EXECUTABLE) message(FATAL_ERROR "error: could not find svn for update of ${name}") @@ -1326,6 +1513,15 @@ function(_ep_add_update_command name) ${svn_user_pw_args} ) set(always 1) + + if(arg_SCRIPT_FILE) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() + elseif(git_repository) # FetchContent gives us these directly, so don't try to recompute them if(NOT GIT_EXECUTABLE OR NOT GIT_VERSION_STRING) @@ -1393,9 +1589,27 @@ function(_ep_add_update_command name) "${tls_version}" "${tls_verify}" ) - set(cmd ${CMAKE_COMMAND} -Dcan_fetch=YES -P ${update_script}) - set(cmd_disconnected ${CMAKE_COMMAND} -Dcan_fetch=NO -P ${update_script}) + set(cmd ${CMAKE_COMMAND} + -Dcan_fetch=YES + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P ${update_script} + ) + set(cmd_disconnected ${CMAKE_COMMAND} + -Dcan_fetch=NO + -DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE + -P ${update_script} + ) set(always 1) + + if(arg_SCRIPT_FILE) + if(update_disconnected) + set(can_fetch_default NO) + else() + set(can_fetch_default YES) + endif() + set(step_script_contents "include(\"${update_script}\")") + endif() + elseif(hg_repository) if(NOT HG_EXECUTABLE) message(FATAL_ERROR "error: could not find hg for pull of ${name}") @@ -1427,6 +1641,28 @@ Update to Mercurial >= 2.1.1. ) set(cmd_disconnected ${HG_EXECUTABLE} update ${hg_tag}) set(always 1) + + if(arg_SCRIPT_FILE) + # These commands are simple, and we know whether updates need to be + # disconnected or not for this case, so write them directly instead of + # forming them from "cmd" and "cmd_disconnected". + if(NOT update_disconnected) + string(APPEND step_script_contents + "execute_process(\n" + " WORKING_DIRECTORY \"${work_dir}\"\n" + " COMMAND_ERROR_IS_FATAL LAST\n" + " COMMAND \"${HG_EXECUTABLE}\" pull\n" + ")" + ) + endif() + string(APPEND step_script_contents + "execute_process(\n" + " WORKING_DIRECTORY \"${work_dir}\"\n" + " COMMAND_ERROR_IS_FATAL LAST\n" + " COMMAND \"${HG_EXECUTABLE}\" update \"${hg_tag}\"\n" + ")" + ) + endif() endif() # We use configure_file() to write the update_info_file so that the file's @@ -1442,6 +1678,20 @@ Update to Mercurial >= 2.1.1. @ONLY ) + if(arg_SCRIPT_FILE) + set(step_name update) + configure_file( + "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/stepscript.cmake.in" + "${arg_SCRIPT_FILE}" + @ONLY + ) + set(${arg_DEPENDS_VARIABLE} "${file_deps}" PARENT_SCOPE) + return() + endif() + + # Nothing below this point is applicable when we've been asked to put the + # update step in a script file (which is the FetchContent case). + if(_EP_LOG_UPDATE) set(log LOG 1) else() @@ -1499,6 +1749,15 @@ endfunction() function(_ep_add_patch_command name) + set(noValueOptions ) + set(singleValueOptions + SCRIPT_FILE # These should only be used by FetchContent + ) + set(multiValueOptions ) + cmake_parse_arguments(PARSE_ARGV 1 arg + "${noValueOptions}" "${singleValueOptions}" "${multiValueOptions}" + ) + # The various _EP_... variables mentioned here and throughout this function # are expected to already have been set by the caller via a call to # _ep_parse_arguments() or ep_parse_arguments_to_vars(). Other variables @@ -1509,10 +1768,18 @@ function(_ep_add_patch_command name) set(stamp_dir "${_EP_STAMP_DIR}") set(cmd "${_EP_PATCH_COMMAND}") + set(step_script_contents "") set(work_dir) if(DEFINED _EP_PATCH_COMMAND) set(work_dir ${source_dir}) + if(arg_SCRIPT_FILE) + _ep_add_script_commands( + step_script_contents + "${work_dir}" + "${cmd}" # Must be a single quoted argument + ) + endif() endif() # We use configure_file() to write the patch_info_file so that the file's @@ -1524,6 +1791,19 @@ function(_ep_add_patch_command name) @ONLY ) + if(arg_SCRIPT_FILE) + set(step_name patch) + configure_file( + "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/stepscript.cmake.in" + "${arg_SCRIPT_FILE}" + @ONLY + ) + return() + endif() + + # Nothing below this point is applicable when we've been asked to put the + # patch step in a script file (which is the FetchContent case). + if(_EP_LOG_PATCH) set(log LOG 1) else() diff --git a/Modules/ExternalProject/stepscript.cmake.in b/Modules/ExternalProject/stepscript.cmake.in new file mode 100644 index 0000000..12e157e --- /dev/null +++ b/Modules/ExternalProject/stepscript.cmake.in @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.29) + +message(VERBOSE "Executing @step_name@ step for @name@") + +block(SCOPE_FOR VARIABLES) + +@step_script_contents@ + +endblock() diff --git a/Modules/ExternalProject/verify.cmake.in b/Modules/ExternalProject/verify.cmake.in index c06da4e..30d0487 100644 --- a/Modules/ExternalProject/verify.cmake.in +++ b/Modules/ExternalProject/verify.cmake.in @@ -12,7 +12,7 @@ if(NOT EXISTS "@LOCAL@") endif() if("@ALGO@" STREQUAL "") - message(WARNING "File will not be verified since no URL_HASH specified") + message(WARNING "File cannot be verified since no URL_HASH specified") return() endif() @@ -20,7 +20,7 @@ if("@EXPECT_VALUE@" STREQUAL "") message(FATAL_ERROR "EXPECT_VALUE can't be empty") endif() -message(STATUS "verifying file... +message(VERBOSE "verifying file... file='@LOCAL@'") file("@ALGO@" "@LOCAL@" actual_value) @@ -34,4 +34,4 @@ does not match expected value ") endif() -message(STATUS "verifying file... done") +message(VERBOSE "verifying file... done") diff --git a/Modules/FetchContent.cmake b/Modules/FetchContent.cmake index 3c01c2a..5006069 100644 --- a/Modules/FetchContent.cmake +++ b/Modules/FetchContent.cmake @@ -141,6 +141,11 @@ Commands exception, see :command:`FetchContent_MakeAvailable` for details on how that affects behavior. + .. versionchanged:: 3.30 + When policy :policy:`CMP0168` is set to ``NEW``, some output-related and + directory-related options are ignored. See the policy documentation for + details. + In most cases, ```` will just be a couple of options defining the download method and method-specific details like a commit tag or archive hash. For example: @@ -437,12 +442,13 @@ Commands like variable or directory scope. Therefore, it doesn't matter where in the project the details were previously declared, as long as they have been declared before the call to ``FetchContent_Populate()``. Those saved details - are then used to construct a call to :command:`ExternalProject_Add` in a - private sub-build to perform the content population immediately. The - implementation of ``ExternalProject_Add()`` ensures that if the content has - already been populated in a previous CMake run, that content will be reused - rather than repopulating them again. For the common case where population - involves downloading content, the cost of the download is only paid once. + are then used to populate the content using a method based on + :command:`ExternalProject_Add` (see policy :policy:`CMP0168` for important + behavioral aspects of how that is done). The implementation ensures that if + the content has already been populated in a previous CMake run, that content + will be reused rather than repopulating them again. For the common case + where population involves downloading content, the cost of the download is + only paid once. An internal global property records when a particular content population request has been processed. If ``FetchContent_Populate()`` is called more @@ -529,6 +535,13 @@ Commands cache variable has no effect on ``FetchContent_Populate()`` calls where the content details are provided directly. + .. versionchanged:: 3.30 + The ``QUIET`` option and global ``FETCHCONTENT_QUIET`` variable have no + effect when policy :policy:`CMP0168` is set to ``NEW``. The output is + still quiet by default in that case, but verbosity is controlled by the + message logging level (see :variable:`CMAKE_MESSAGE_LOG_LEVEL` and + :option:`--log-level `). + ``SUBBUILD_DIR`` The ``SUBBUILD_DIR`` argument can be provided to change the location of the sub-build created to perform the population. The default value is @@ -538,6 +551,10 @@ Commands This option should not be confused with the ``SOURCE_SUBDIR`` option which only affects the :command:`FetchContent_MakeAvailable` command. + .. versionchanged:: 3.30 + ``SUBBUILD_DIR`` is ignored when policy :policy:`CMP0168` is set to + ``NEW``, since there is no sub-build in that case. + ``SOURCE_DIR``, ``BINARY_DIR`` The ``SOURCE_DIR`` and ``BINARY_DIR`` arguments are supported by :command:`ExternalProject_Add`, but different default values are used by @@ -548,7 +565,7 @@ Commands :variable:`CMAKE_CURRENT_BINARY_DIR`. In addition to the above explicit options, any other unrecognized options are - passed through unmodified to :command:`ExternalProject_Add` to perform the + passed through unmodified to :command:`ExternalProject_Add` to set up the download, patch and update steps. The following options are explicitly prohibited (they are disabled by the ``FetchContent_Populate()`` command): @@ -564,6 +581,11 @@ Commands :variable:`CMAKE_MAKE_PROGRAM` variables will need to be set appropriately on the command line invoking the script. + .. versionchanged:: 3.30 + If policy :policy:`CMP0168` is set to ``NEW``, no sub-build is used. + Within CMake's script mode, that allows ``FetchContent_Populate()`` to be + called without any build tool or CMake generator. + .. versionadded:: 3.18 Added support for the ``DOWNLOAD_NO_EXTRACT`` option. @@ -675,6 +697,13 @@ A number of cache variables can influence the behavior where details from a problems with hung downloads, temporarily switching this option off may help diagnose which content population is causing the issue. + .. versionchanged:: 3.30 + ``FETCHCONTENT_QUIET`` is ignored if policy :policy:`CMP0168` is set to + ``NEW``. The output is still quiet by default in that case, but verbosity + is controlled by the message logging level (see + :variable:`CMAKE_MESSAGE_LOG_LEVEL` and + :option:`--log-level `). + .. variable:: FETCHCONTENT_FULLY_DISCONNECTED When this option is enabled, no attempt is made to download or update @@ -682,7 +711,7 @@ A number of cache variables can influence the behavior where details from a a previous run or the source directories have been pointed at existing contents the developer has provided manually (using options described further below). When the developer knows that no changes have been made to - any content details, turning this option ``ON`` can significantly speed up + any content details, turning this option ``ON`` can speed up the configure stage. It is ``OFF`` by default. .. note:: @@ -1167,7 +1196,13 @@ function(__FetchContent_declareDetails contentName) set(__findPackageArgs) set(__sawQuietKeyword NO) set(__sawGlobalKeyword NO) + set(__direct_population NO) foreach(__item IN LISTS ARGN) + if(__item STREQUAL "__DIRECT_POPULATION") + set(__direct_population YES) + continue() + endif() + if(DEFINED __findPackageArgs) # All remaining args are for find_package() string(APPEND __findPackageArgs " [==[${__item}]==]") @@ -1206,6 +1241,10 @@ function(__FetchContent_declareDetails contentName) string(APPEND __cmdArgs " [==[${__item}]==]") endforeach() + set_property(GLOBAL PROPERTY + "_FetchContent_${contentNameLower}_direct_population" ${__direct_population} + ) + define_property(GLOBAL PROPERTY ${savedDetailsPropertyName}) cmake_language(EVAL CODE "set_property(GLOBAL PROPERTY ${savedDetailsPropertyName} ${__cmdArgs})" @@ -1372,16 +1411,24 @@ function(FetchContent_Declare contentName) endif() # Add back in the keyword args we pulled out and potentially tweaked/added + set(forward_args "${ARG_UNPARSED_ARGUMENTS}") set(sep EXTERNALPROJECT_INTERNAL_ARGUMENT_SEPARATOR) foreach(key IN LISTS oneValueArgs) if(DEFINED ARG_${key}) - list(PREPEND ARG_UNPARSED_ARGUMENTS ${key} "${ARG_${key}}" ${sep}) + list(PREPEND forward_args ${key} "${ARG_${key}}" ${sep}) set(sep "") endif() endforeach() + cmake_policy(GET CMP0168 cmp0168 + PARENT_SCOPE # undocumented, do not use outside of CMake + ) + if(cmp0168 STREQUAL "NEW") + list(PREPEND forward_args __DIRECT_POPULATION ${sep}) + endif() + set(__argsQuoted) - foreach(__item IN LISTS ARG_UNPARSED_ARGUMENTS) + foreach(__item IN LISTS forward_args) string(APPEND __argsQuoted " [==[${__item}]==]") endforeach() cmake_language(EVAL CODE @@ -1498,7 +1545,7 @@ endfunction() # The value of contentName will always have been lowercased by the caller. # All other arguments are assumed to be options that are understood by # ExternalProject_Add(), except for QUIET and SUBBUILD_DIR. -function(__FetchContent_directPopulate contentName) +function(__FetchContent_doPopulation contentName) set(options QUIET @@ -1533,8 +1580,14 @@ function(__FetchContent_directPopulate contentName) cmake_parse_arguments(PARSE_ARGV 1 ARG "${options}" "${oneValueArgs}" "${multiValueArgs}") + get_property(direct_population GLOBAL PROPERTY + "_FetchContent_${contentNameLower}_direct_population" + ) + if(NOT ARG_SUBBUILD_DIR) - message(FATAL_ERROR "Internal error: SUBBUILD_DIR not set") + if(NOT direct_population) + message(FATAL_ERROR "Internal error: SUBBUILD_DIR not set") + endif() elseif(NOT IS_ABSOLUTE "${ARG_SUBBUILD_DIR}") set(ARG_SUBBUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/${ARG_SUBBUILD_DIR}") endif() @@ -1558,6 +1611,148 @@ function(__FetchContent_directPopulate contentName) set(${contentName}_SOURCE_DIR "${ARG_SOURCE_DIR}" PARENT_SCOPE) set(${contentName}_BINARY_DIR "${ARG_BINARY_DIR}" PARENT_SCOPE) + if(direct_population) + __FetchContent_populateDirect() + else() + __FetchContent_populateSubbuild() + endif() +endfunction() + + +function(__FetchContent_populateDirect) + # Policies CMP0097, CMP0135 and CMP0150 are handled in FetchContent_Declare() + # and the stored arguments already account for them. + # For CMP0097, the arguments will always assume NEW behavior by the time + # we get to here, so ensure ExternalProject sees that. + set(_EP_CMP0097 NEW) + + set(args_to_parse + "${ARG_UNPARSED_ARGUMENTS}" + SOURCE_DIR "${ARG_SOURCE_DIR}" + BINARY_DIR "${ARG_BINARY_DIR}" + ) + if(ARG_DOWNLOAD_NO_EXTRACT) + list(APPEND args_to_parse DOWNLOAD_NO_EXTRACT YES) + endif() + + get_property(cmake_role GLOBAL PROPERTY CMAKE_ROLE) + if(cmake_role STREQUAL "PROJECT") + # We don't support direct population where a project makes a direct call + # to FetchContent_Populate(). That always goes through ExternalProject and + # will soon be deprecated anyway. + set(function_for_args FetchContent_Declare) + elseif(cmake_role STREQUAL "SCRIPT") + set(function_for_args FetchContent_Populate) + else() + message(FATAL_ERROR "Unsupported context for direct population") + endif() + + _ep_get_add_keywords(keywords) + _ep_parse_arguments_to_vars( + ${function_for_args} + "${keywords}" + ${contentName} + _EP_ + "${args_to_parse}" + ) + + # We use a simplified set of directories here. We do not need the full set + # of directories that ExternalProject supports, and we don't need the + # extensive customization options it supports either. Note that + # _EP_SOURCE_DIR and _EP_BINARY_DIR are always included in the saved args, + # so we must not set them here. + set(_EP_STAMP_DIR "${FETCHCONTENT_BASE_DIR}/${contentNameLower}-stamp") + set(_EP_TMP_DIR "${FETCHCONTENT_BASE_DIR}/${contentNameLower}-tmp") + set(_EP_DOWNLOAD_DIR "${_EP_TMP_DIR}") + + file(MAKE_DIRECTORY + "${_EP_SOURCE_DIR}" + "${_EP_BINARY_DIR}" + "${_EP_STAMP_DIR}" + "${_EP_TMP_DIR}" + ) + + # We take over the stamp files and use our own for detecting whether each + # step is up-to-date. The method used by ExternalProject is specific to + # using a sub-build and is not appropriate for us here. + + set(download_script ${_EP_TMP_DIR}/download.cmake) + set(update_script ${_EP_TMP_DIR}/upload.cmake) + set(patch_script ${_EP_TMP_DIR}/patch.cmake) + _ep_add_download_command(${contentName} + SCRIPT_FILE ${download_script} + DEPENDS_VARIABLE download_depends + ) + _ep_add_update_command(${contentName} + SCRIPT_FILE ${update_script} + DEPENDS_VARIABLE update_depends + ) + _ep_add_patch_command(${contentName} + SCRIPT_FILE ${patch_script} + # No additional dependencies for the patch step + ) + + set(download_stamp ${_EP_STAMP_DIR}/download.stamp) + set(update_stamp ${_EP_STAMP_DIR}/upload.stamp) + set(patch_stamp ${_EP_STAMP_DIR}/patch.stamp) + __FetchContent_doStepDirect( + SCRIPT_FILE ${download_script} + STAMP_FILE ${download_stamp} + DEPENDS ${download_depends} + ) + __FetchContent_doStepDirect( + SCRIPT_FILE ${update_script} + STAMP_FILE ${update_stamp} + DEPENDS ${update_depends} ${download_stamp} + ) + __FetchContent_doStepDirect( + SCRIPT_FILE ${patch_script} + STAMP_FILE ${patch_stamp} + DEPENDS ${update_stamp} + ) + +endfunction() + + +function(__FetchContent_doStepDirect) + set(noValueOptions ) + set(singleValueOptions + SCRIPT_FILE + STAMP_FILE + ) + set(multiValueOptions + DEPENDS + ) + cmake_parse_arguments(PARSE_ARGV 0 arg + "${noValueOptions}" "${singleValueOptions}" "${multiValueOptions}" + ) + + if(NOT EXISTS ${arg_STAMP_FILE}) + set(do_step YES) + else() + set(do_step NO) + foreach(dep_file IN LISTS arg_DEPENDS arg_SCRIPT_FILE) + if(NOT EXISTS "${arg_STAMP_FILE}" OR + NOT EXISTS "${dep_file}" OR + NOT "${arg_STAMP_FILE}" IS_NEWER_THAN "${dep_file}") + set(do_step YES) + break() + endif() + endforeach() + endif() + + if(do_step) + include(${arg_SCRIPT_FILE}) + file(TOUCH "${arg_STAMP_FILE}") + endif() +endfunction() + + +function(__FetchContent_populateSubbuild) + # All argument parsing is done in __FetchContent_doPopulate(), since it is + # common to both the subbuild and direct population strategies. + # Parsed arguments are in ARG_... variables. + # The unparsed arguments may contain spaces, so build up ARG_EXTRA # in such a way that it correctly substitutes into the generated # CMakeLists.txt file with each argument quoted. @@ -1736,7 +1931,7 @@ function(FetchContent_Populate contentName) if(ARGN) # This is the direct population form with details fully specified # as part of the call, so we already have everything we need - __FetchContent_directPopulate( + __FetchContent_doPopulation( ${contentNameLower} SUBBUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/${contentNameLower}-subbuild" SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/${contentNameLower}-src" @@ -1853,7 +2048,7 @@ function(FetchContent_Populate contentName) endif() endforeach() cmake_language(EVAL CODE " - __FetchContent_directPopulate( + __FetchContent_doPopulation( ${contentNameLower} ${quietFlag} UPDATE_DISCONNECTED ${disconnectUpdates} diff --git a/Source/cmPolicies.h b/Source/cmPolicies.h index ed159fe..9dd3ce5 100644 --- a/Source/cmPolicies.h +++ b/Source/cmPolicies.h @@ -514,7 +514,11 @@ class cmMakefile; "private dependencies of static libraries.", \ 3, 30, 0, cmPolicies::WARN) \ SELECT(POLICY, CMP0167, "The FindBoost module is removed.", 3, 30, 0, \ - cmPolicies::WARN) + cmPolicies::WARN) \ + SELECT(POLICY, CMP0168, \ + "FetchContent implements steps directly instead of through a " \ + "sub-build.", \ + 3, 30, 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/FetchContent/CMakeLists.txt b/Tests/RunCMake/FetchContent/CMakeLists.txt index 3cc2e43..eb0b40c 100644 --- a/Tests/RunCMake/FetchContent/CMakeLists.txt +++ b/Tests/RunCMake/FetchContent/CMakeLists.txt @@ -4,4 +4,10 @@ project(${RunCMake_TEST} NONE) # Tests assume no previous downloads in the output directory file(REMOVE_RECURSE ${CMAKE_CURRENT_BINARY_DIR}/_deps) +if(CMP0168 STREQUAL "NEW") + cmake_policy(SET CMP0168 NEW) + string(REGEX REPLACE "-direct$" "" RunCMake_TEST "${RunCMake_TEST}") +else() + cmake_policy(SET CMP0168 OLD) +endif() include(${RunCMake_TEST}.cmake) diff --git a/Tests/RunCMake/FetchContent/DownloadFile.cmake b/Tests/RunCMake/FetchContent/DownloadFile.cmake index 741b6d3..e21ae28 100644 --- a/Tests/RunCMake/FetchContent/DownloadFile.cmake +++ b/Tests/RunCMake/FetchContent/DownloadFile.cmake @@ -1,8 +1,12 @@ include(FetchContent) +# The file hash depends on the line endings used by git +file(MD5 ${CMAKE_CURRENT_LIST_DIR}/dummyFile.txt md5_hash) + FetchContent_Declare( t1 URL ${CMAKE_CURRENT_LIST_DIR}/dummyFile.txt + URL_HASH MD5=${md5_hash} DOWNLOAD_NO_EXTRACT YES ) diff --git a/Tests/RunCMake/FetchContent/DownloadTwice-direct-result.txt b/Tests/RunCMake/FetchContent/DownloadTwice-direct-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/FetchContent/DownloadTwice-direct-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/FetchContent/DownloadTwice-direct-stderr.txt b/Tests/RunCMake/FetchContent/DownloadTwice-direct-stderr.txt new file mode 100644 index 0000000..e793902 --- /dev/null +++ b/Tests/RunCMake/FetchContent/DownloadTwice-direct-stderr.txt @@ -0,0 +1,2 @@ +CMake Error at .*/Modules/FetchContent\.cmake:[0-9]+ \(message\): + Content t1 already populated in diff --git a/Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-result.txt b/Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-stderr.txt b/Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-stderr.txt new file mode 100644 index 0000000..9c3fc27 --- /dev/null +++ b/Tests/RunCMake/FetchContent/MakeAvailableUndeclared-direct-stderr.txt @@ -0,0 +1,2 @@ +CMake Error at .*/Modules/FetchContent\.cmake:[0-9]+ \(message\): + No content details recorded for NoDetails diff --git a/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-result.txt b/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-stderr.txt b/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-stderr.txt new file mode 100644 index 0000000..7ecb06b --- /dev/null +++ b/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing-direct-stderr.txt @@ -0,0 +1,2 @@ + *Manually specified source directory is missing: ++ *FETCHCONTENT_SOURCE_DIR_WITHPROJECT --> .*/ADirThatDoesNotExist diff --git a/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing.cmake b/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing.cmake index 0e24c1a..82e5c46 100644 --- a/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing.cmake +++ b/Tests/RunCMake/FetchContent/ManualSourceDirectoryMissing.cmake @@ -1,3 +1,4 @@ +message(STATUS "FETCHCONTENT_SOURCE_DIR_WITHPROJECT = ${FETCHCONTENT_SOURCE_DIR_WITHPROJECT}") include(FetchContent) FetchContent_Declare( diff --git a/Tests/RunCMake/FetchContent/ManualSourceDirectoryRelative-direct-stderr.txt b/Tests/RunCMake/FetchContent/ManualSourceDirectoryRelative-direct-stderr.txt new file mode 100644 index 0000000..3defcb4 --- /dev/null +++ b/Tests/RunCMake/FetchContent/ManualSourceDirectoryRelative-direct-stderr.txt @@ -0,0 +1,3 @@ + *Relative source directory specified. This is not safe, as it depends on + *the calling directory scope. ++ *FETCHCONTENT_SOURCE_DIR_WITHPROJECT --> WithProject diff --git a/Tests/RunCMake/FetchContent/RunCMakeTest.cmake b/Tests/RunCMake/FetchContent/RunCMakeTest.cmake index 0f443a7..72a458c 100644 --- a/Tests/RunCMake/FetchContent/RunCMakeTest.cmake +++ b/Tests/RunCMake/FetchContent/RunCMakeTest.cmake @@ -2,81 +2,107 @@ include(RunCMake) unset(RunCMake_TEST_NO_CLEAN) -run_cmake(MissingDetails) -run_cmake(DirectIgnoresDetails) -run_cmake(FirstDetailsWin) -run_cmake(DownloadTwice) -run_cmake(DownloadFile) -run_cmake(IgnoreToolchainFile) -run_cmake(SameGenerator) -run_cmake(System) -run_cmake(VarDefinitions) -run_cmake(VarPassthroughs) -run_cmake(GetProperties) -run_cmake(UsesTerminalOverride) -run_cmake(MakeAvailable) -run_cmake(MakeAvailableTwice) -run_cmake(MakeAvailableUndeclared) -run_cmake(VerifyHeaderSet) +function(run_cmake_with_cmp0168 name) + run_cmake_with_options("${name}" -D CMP0168=OLD ${ARGN}) + run_cmake_with_options("${name}-direct" -D CMP0168=NEW ${ARGN}) +endfunction() + +# Won't get to the part where CMP0168 matters +run_cmake_with_options(MissingDetails -D CMP0168=NEW) + +# These are testing specific aspects of the sub-build +run_cmake_with_options(SameGenerator -D CMP0168=OLD) +run_cmake_with_options(VarPassthroughs -D CMP0168=OLD) -run_cmake_with_options(FindDependencyExport +run_cmake_with_cmp0168(DirectIgnoresDetails) +run_cmake_with_cmp0168(FirstDetailsWin) +run_cmake_with_cmp0168(DownloadTwice) +run_cmake_with_cmp0168(DownloadFile) +run_cmake_with_cmp0168(IgnoreToolchainFile) +run_cmake_with_cmp0168(System) +run_cmake_with_cmp0168(VarDefinitions) +run_cmake_with_cmp0168(GetProperties) +run_cmake_with_cmp0168(UsesTerminalOverride) +run_cmake_with_cmp0168(MakeAvailable) +run_cmake_with_cmp0168(MakeAvailableTwice) +run_cmake_with_cmp0168(MakeAvailableUndeclared) +run_cmake_with_cmp0168(VerifyHeaderSet) + +run_cmake_with_cmp0168(FindDependencyExport -D "CMAKE_PROJECT_TOP_LEVEL_INCLUDES=${CMAKE_CURRENT_LIST_DIR}/FindDependencyExportDP.cmake" ) -run_cmake_with_options(ManualSourceDirectory +run_cmake_with_cmp0168(ManualSourceDirectory -D "FETCHCONTENT_SOURCE_DIR_WITHPROJECT=${CMAKE_CURRENT_LIST_DIR}/WithProject" ) -run_cmake_with_options(ManualSourceDirectoryMissing +run_cmake_with_cmp0168(ManualSourceDirectoryMissing -D "FETCHCONTENT_SOURCE_DIR_WITHPROJECT=${CMAKE_CURRENT_LIST_DIR}/ADirThatDoesNotExist" ) # Need to use :STRING to prevent CMake from automatically converting it to an # absolute path -run_cmake_with_options(ManualSourceDirectoryRelative +run_cmake_with_cmp0168(ManualSourceDirectoryRelative -D "FETCHCONTENT_SOURCE_DIR_WITHPROJECT:STRING=WithProject" ) -function(run_FetchContent_DirOverrides) - set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/DirOverrides-build) +function(run_FetchContent_DirOverrides cmp0168) + if(cmp0168 STREQUAL "NEW") + set(suffix "-direct") + else() + set(suffix "") + endif() + set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/DirOverrides${suffix}-build) file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}") file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}") - run_cmake(DirOverrides) + run_cmake_with_options(DirOverrides${suffix} -D CMP0168=${cmp0168}) set(RunCMake_TEST_NO_CLEAN 1) - run_cmake_with_options(DirOverridesDisconnected + run_cmake_with_options(DirOverridesDisconnected${suffix} + -D CMP0168=${cmp0168} -D FETCHCONTENT_FULLY_DISCONNECTED=YES ) endfunction() -run_FetchContent_DirOverrides() +run_FetchContent_DirOverrides(OLD) +run_FetchContent_DirOverrides(NEW) set(RunCMake_TEST_OUTPUT_MERGE 1) -run_cmake(PreserveEmptyArgs) +run_cmake_with_cmp0168(PreserveEmptyArgs) set(RunCMake_TEST_OUTPUT_MERGE 0) -# We need to pass through CMAKE_GENERATOR and CMAKE_MAKE_PROGRAM -# to ensure the test can run on machines where the build tool -# isn't on the PATH. Some build slaves explicitly test with such -# an arrangement (e.g. to test with spaces in the path). We also -# pass through the platform and toolset for completeness, even -# though we don't build anything, just in case this somehow affects -# the way the build tool is invoked. -run_cmake_command(ScriptMode - ${CMAKE_COMMAND} - -DCMAKE_GENERATOR=${RunCMake_GENERATOR} - -DCMAKE_GENERATOR_PLATFORM=${RunCMake_GENERATOR_PLATFORM} - -DCMAKE_GENERATOR_TOOLSET=${RunCMake_GENERATOR_TOOLSET} - -DCMAKE_MAKE_PROGRAM=${RunCMake_MAKE_PROGRAM} - -P ${CMAKE_CURRENT_LIST_DIR}/ScriptMode.cmake -) - function(run_FetchContent_ExcludeFromAll) set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/ExcludeFromAll-build) file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}") file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}") - run_cmake(ExcludeFromAll) + # We're testing FetchContent_MakeAvailable()'s add_subdirectory() behavior, + # so it doesn't matter if we use OLD or NEW for CMP0168, but NEW is faster. + run_cmake(ExcludeFromAll -D CMP0168=NEW) set(RunCMake_TEST_NO_CLEAN 1) run_cmake_command(ExcludeFromAll-build ${CMAKE_COMMAND} --build .) endfunction() run_FetchContent_ExcludeFromAll() + +# Script mode testing requires more care for CMP0168 set to OLD. +# We need to pass through CMAKE_GENERATOR and CMAKE_MAKE_PROGRAM +# to ensure the test can run on machines where the build tool +# isn't on the PATH. Some build machines explicitly test with such +# an arrangement (e.g. to test with spaces in the path). We also +# pass through the platform and toolset for completeness, even +# though we don't build anything, just in case this somehow affects +# the way the build tool is invoked. +run_cmake_command(ScriptMode + ${CMAKE_COMMAND} + -DCMP0168=OLD + -DCMAKE_GENERATOR=${RunCMake_GENERATOR} + -DCMAKE_GENERATOR_PLATFORM=${RunCMake_GENERATOR_PLATFORM} + -DCMAKE_GENERATOR_TOOLSET=${RunCMake_GENERATOR_TOOLSET} + -DCMAKE_MAKE_PROGRAM=${RunCMake_MAKE_PROGRAM} + -P ${CMAKE_CURRENT_LIST_DIR}/ScriptMode.cmake +) +# CMP0168 NEW doesn't need a build tool or generator, so don't set them. +run_cmake_command(ScriptMode-direct + ${CMAKE_COMMAND} + -DCMP0168=NEW + -P ${CMAKE_CURRENT_LIST_DIR}/ScriptMode.cmake +) diff --git a/Tests/RunCMake/FetchContent/ScriptMode.cmake b/Tests/RunCMake/FetchContent/ScriptMode.cmake index 0a93d62..bf66140 100644 --- a/Tests/RunCMake/FetchContent/ScriptMode.cmake +++ b/Tests/RunCMake/FetchContent/ScriptMode.cmake @@ -1,3 +1,5 @@ +cmake_policy(SET CMP0168 ${CMP0168}) + include(FetchContent) file(WRITE tmpFile.txt "Generated contents, not important") -- cgit v0.12