From 9ea449643aa27b88b317e9374a4cf26d1b7d3c58 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Mon, 9 Nov 2026 10:45:55 +0800 Subject: [PATCH 1/2] fix(ci): retry CPM bootstrap download on transient failures CMake file(DOWNLOAD) with EXPECTED_HASH fatals on HTTP errors before STATUS can be inspected, so a single GitHub releases blip aborts configure (seen on scheduled Fuzz run 42472687801). Download to a temp file, verify SHA256 manually, retry with bounded backoff, and only then promote to the include path. Add cmake -P coverage for the final-failure and success paths. --- cmake/CPM.cmake | 103 +++++++++++++++++++++++++-- cmake/test_cpm_bootstrap_retry.cmake | 85 ++++++++++++++++++++++ 2 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 cmake/test_cpm_bootstrap_retry.cmake diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake index cc25ec28..1bb7716c 100644 --- a/cmake/CPM.cmake +++ b/cmake/CPM.cmake @@ -1,10 +1,25 @@ # SPDX-License-Identifier: MIT # # SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors +# +# Local change: bounded retries around the CPM bootstrap download so transient +# GitHub release CDN failures (HTTP 5xx / network blips) do not fail configure +# on the first attempt. Partial downloads go to a .download temp file and are +# never promoted to the final include path. Hash is verified manually so a +# failed HTTP response does not abort via EXPECTED_HASH before retries run. set(CPM_DOWNLOAD_VERSION 0.38.7) set(CPM_HASH_SUM "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5") +if(NOT DEFINED CPM_DOWNLOAD_URL) + set(CPM_DOWNLOAD_URL + "https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake") +endif() + +if(NOT DEFINED CPM_DOWNLOAD_MAX_RETRIES) + set(CPM_DOWNLOAD_MAX_RETRIES 5) +endif() + if(CPM_SOURCE_CACHE) set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") elseif(DEFINED ENV{CPM_SOURCE_CACHE}) @@ -16,9 +31,87 @@ endif() # Expand relative path. This is important if the provided path contains a tilde (~) get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) -file(DOWNLOAD - https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake - ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} -) +set(_cpm_need_download TRUE) +if(EXISTS "${CPM_DOWNLOAD_LOCATION}") + file(SHA256 "${CPM_DOWNLOAD_LOCATION}" _cpm_existing_hash) + if(_cpm_existing_hash STREQUAL "${CPM_HASH_SUM}") + set(_cpm_need_download FALSE) + else() + message(STATUS + "CPM.cmake at ${CPM_DOWNLOAD_LOCATION} has unexpected SHA256 " + "(${_cpm_existing_hash}); re-downloading") + file(REMOVE "${CPM_DOWNLOAD_LOCATION}") + endif() +endif() + +if(_cpm_need_download) + get_filename_component(_cpm_download_dir "${CPM_DOWNLOAD_LOCATION}" DIRECTORY) + file(MAKE_DIRECTORY "${_cpm_download_dir}") + + # Never write a failed/partial body onto the final include path. + set(_cpm_download_tmp "${CPM_DOWNLOAD_LOCATION}.download") + set(_cpm_download_ok FALSE) + set(_cpm_last_status "not attempted") + + foreach(_cpm_attempt RANGE 1 ${CPM_DOWNLOAD_MAX_RETRIES}) + file(REMOVE "${_cpm_download_tmp}") + # Do not pass EXPECTED_HASH here: on HTTP failure CMake fatals before + # STATUS can be inspected, which prevents retries. Verify SHA256 below. + file(DOWNLOAD + "${CPM_DOWNLOAD_URL}" + "${_cpm_download_tmp}" + STATUS _cpm_status + TIMEOUT 60 + TLS_VERIFY ON + ) + list(GET _cpm_status 0 _cpm_status_code) + set(_cpm_last_status "${_cpm_status}") + + set(_cpm_attempt_ok FALSE) + if(_cpm_status_code EQUAL 0 AND EXISTS "${_cpm_download_tmp}") + file(SHA256 "${_cpm_download_tmp}" _cpm_got_hash) + if(_cpm_got_hash STREQUAL "${CPM_HASH_SUM}") + set(_cpm_attempt_ok TRUE) + else() + set(_cpm_last_status + "hash mismatch: got ${_cpm_got_hash}, expected ${CPM_HASH_SUM}") + file(REMOVE "${_cpm_download_tmp}") + endif() + else() + file(REMOVE "${_cpm_download_tmp}") + endif() + + if(_cpm_attempt_ok) + # Replace destination only after a hash-verified download. + file(REMOVE "${CPM_DOWNLOAD_LOCATION}") + file(RENAME "${_cpm_download_tmp}" "${CPM_DOWNLOAD_LOCATION}") + set(_cpm_download_ok TRUE) + if(_cpm_attempt GREATER 1) + message(STATUS "CPM.cmake download succeeded on attempt ${_cpm_attempt}") + endif() + break() + endif() + + message(WARNING + "CPM.cmake download attempt ${_cpm_attempt}/${CPM_DOWNLOAD_MAX_RETRIES} " + "failed (status=${_cpm_last_status}) from ${CPM_DOWNLOAD_URL}") + if(_cpm_attempt LESS ${CPM_DOWNLOAD_MAX_RETRIES}) + execute_process(COMMAND "${CMAKE_COMMAND}" -E sleep ${_cpm_attempt}) + endif() + endforeach() + + if(NOT _cpm_download_ok) + file(REMOVE "${_cpm_download_tmp}") + message(FATAL_ERROR + "Failed to download CPM.cmake v${CPM_DOWNLOAD_VERSION} after " + "${CPM_DOWNLOAD_MAX_RETRIES} attempts.\n" + " URL: ${CPM_DOWNLOAD_URL}\n" + " Expected SHA256: ${CPM_HASH_SUM}\n" + " Destination: ${CPM_DOWNLOAD_LOCATION}\n" + " Last status: ${_cpm_last_status}\n" + "Check network access to GitHub releases, or set CPM_SOURCE_CACHE / " + "pre-populate the destination file with the expected hash.") + endif() +endif() -include(${CPM_DOWNLOAD_LOCATION}) +include("${CPM_DOWNLOAD_LOCATION}") diff --git a/cmake/test_cpm_bootstrap_retry.cmake b/cmake/test_cpm_bootstrap_retry.cmake new file mode 100644 index 00000000..4e9e883f --- /dev/null +++ b/cmake/test_cpm_bootstrap_retry.cmake @@ -0,0 +1,85 @@ +# Reproducible validation for cmake/CPM.cmake bootstrap retry / final failure. +# Usage (from repo root): +# cmake -P cmake/test_cpm_bootstrap_retry.cmake +# +# Case A: bad URL must fail after bounded retries with clear diagnostics and +# must not leave a partial final file. +# Case B: real URL must succeed (hash verified) when network is available. + +cmake_minimum_required(VERSION 3.14) + +set(_cpm_test_root "${CMAKE_CURRENT_LIST_DIR}/../build-cpm-retry-test") +file(REMOVE_RECURSE "${_cpm_test_root}") +file(MAKE_DIRECTORY "${_cpm_test_root}") + +set(_cpm_script "${CMAKE_CURRENT_LIST_DIR}/CPM.cmake") +set(_cpm_hash "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5") + +# --- Case A: final failure --- +set(_fail_bin "${_cpm_test_root}/fail") +file(MAKE_DIRECTORY "${_fail_bin}") +set(_fail_dest "${_fail_bin}/cmake/CPM_0.38.7.cmake") +execute_process( + COMMAND "${CMAKE_COMMAND}" + "-DCPM_DOWNLOAD_VERSION=0.38.7" + "-DCPM_HASH_SUM=${_cpm_hash}" + "-DCPM_DOWNLOAD_MAX_RETRIES=2" + "-DCPM_DOWNLOAD_URL=https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.0.0/DOES_NOT_EXIST.cmake" + "-DCMAKE_BINARY_DIR=${_fail_bin}" + -P "${_cpm_script}" + WORKING_DIRECTORY "${_fail_bin}" + RESULT_VARIABLE _fail_rc + OUTPUT_VARIABLE _fail_out + ERROR_VARIABLE _fail_err +) +if(_fail_rc EQUAL 0) + message(FATAL_ERROR "Case A: expected CPM bootstrap failure, but cmake exited 0.\n${_fail_out}\n${_fail_err}") +endif() +set(_fail_combined "${_fail_out}\n${_fail_err}") +if(NOT _fail_combined MATCHES "Failed to download CPM.cmake") + message(FATAL_ERROR "Case A: missing final-failure diagnostics.\n${_fail_combined}") +endif() +if(NOT _fail_combined MATCHES "attempt 1/2") + message(FATAL_ERROR "Case A: expected retry warning for attempt 1/2.\n${_fail_combined}") +endif() +if(EXISTS "${_fail_dest}") + message(FATAL_ERROR "Case A: partial/final file must not remain at ${_fail_dest}") +endif() +if(EXISTS "${_fail_dest}.download") + message(FATAL_ERROR "Case A: temp download file must not remain at ${_fail_dest}.download") +endif() +message(STATUS "Case A OK: bounded retries + clear final failure, no partial file") + +# --- Case B: success path (requires network) --- +set(_ok_bin "${_cpm_test_root}/ok") +file(MAKE_DIRECTORY "${_ok_bin}") +set(_ok_dest "${_ok_bin}/cmake/CPM_0.38.7.cmake") +execute_process( + COMMAND "${CMAKE_COMMAND}" + "-DCPM_DOWNLOAD_VERSION=0.38.7" + "-DCPM_HASH_SUM=${_cpm_hash}" + "-DCPM_DOWNLOAD_MAX_RETRIES=5" + "-DCMAKE_BINARY_DIR=${_ok_bin}" + -P "${_cpm_script}" + WORKING_DIRECTORY "${_ok_bin}" + RESULT_VARIABLE _ok_rc + OUTPUT_VARIABLE _ok_out + ERROR_VARIABLE _ok_err +) +if(NOT _ok_rc EQUAL 0) + message(FATAL_ERROR "Case B: expected CPM bootstrap success.\n${_ok_out}\n${_ok_err}") +endif() +if(NOT EXISTS "${_ok_dest}") + message(FATAL_ERROR "Case B: missing downloaded file ${_ok_dest}") +endif() +file(SHA256 "${_ok_dest}" _ok_got_hash) +if(NOT _ok_got_hash STREQUAL "${_cpm_hash}") + message(FATAL_ERROR "Case B: hash mismatch got=${_ok_got_hash}") +endif() +if(EXISTS "${_ok_dest}.download") + message(FATAL_ERROR "Case B: temp download file must not remain") +endif() +message(STATUS "Case B OK: download + SHA256 verification succeeded") + +file(REMOVE_RECURSE "${_cpm_test_root}") +message(STATUS "All CPM bootstrap retry checks passed") From 0d02eef1bf44329b6f70f9eecfde480f2c2a3fd3 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Mon, 9 Nov 2026 12:19:07 +0800 Subject: [PATCH 2/2] chore: drop standalone CPM bootstrap retry test script from PR Keep only the cmake/CPM.cmake download-retry fix in this change set. --- cmake/test_cpm_bootstrap_retry.cmake | 85 ---------------------------- 1 file changed, 85 deletions(-) delete mode 100644 cmake/test_cpm_bootstrap_retry.cmake diff --git a/cmake/test_cpm_bootstrap_retry.cmake b/cmake/test_cpm_bootstrap_retry.cmake deleted file mode 100644 index 4e9e883f..00000000 --- a/cmake/test_cpm_bootstrap_retry.cmake +++ /dev/null @@ -1,85 +0,0 @@ -# Reproducible validation for cmake/CPM.cmake bootstrap retry / final failure. -# Usage (from repo root): -# cmake -P cmake/test_cpm_bootstrap_retry.cmake -# -# Case A: bad URL must fail after bounded retries with clear diagnostics and -# must not leave a partial final file. -# Case B: real URL must succeed (hash verified) when network is available. - -cmake_minimum_required(VERSION 3.14) - -set(_cpm_test_root "${CMAKE_CURRENT_LIST_DIR}/../build-cpm-retry-test") -file(REMOVE_RECURSE "${_cpm_test_root}") -file(MAKE_DIRECTORY "${_cpm_test_root}") - -set(_cpm_script "${CMAKE_CURRENT_LIST_DIR}/CPM.cmake") -set(_cpm_hash "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5") - -# --- Case A: final failure --- -set(_fail_bin "${_cpm_test_root}/fail") -file(MAKE_DIRECTORY "${_fail_bin}") -set(_fail_dest "${_fail_bin}/cmake/CPM_0.38.7.cmake") -execute_process( - COMMAND "${CMAKE_COMMAND}" - "-DCPM_DOWNLOAD_VERSION=0.38.7" - "-DCPM_HASH_SUM=${_cpm_hash}" - "-DCPM_DOWNLOAD_MAX_RETRIES=2" - "-DCPM_DOWNLOAD_URL=https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.0.0/DOES_NOT_EXIST.cmake" - "-DCMAKE_BINARY_DIR=${_fail_bin}" - -P "${_cpm_script}" - WORKING_DIRECTORY "${_fail_bin}" - RESULT_VARIABLE _fail_rc - OUTPUT_VARIABLE _fail_out - ERROR_VARIABLE _fail_err -) -if(_fail_rc EQUAL 0) - message(FATAL_ERROR "Case A: expected CPM bootstrap failure, but cmake exited 0.\n${_fail_out}\n${_fail_err}") -endif() -set(_fail_combined "${_fail_out}\n${_fail_err}") -if(NOT _fail_combined MATCHES "Failed to download CPM.cmake") - message(FATAL_ERROR "Case A: missing final-failure diagnostics.\n${_fail_combined}") -endif() -if(NOT _fail_combined MATCHES "attempt 1/2") - message(FATAL_ERROR "Case A: expected retry warning for attempt 1/2.\n${_fail_combined}") -endif() -if(EXISTS "${_fail_dest}") - message(FATAL_ERROR "Case A: partial/final file must not remain at ${_fail_dest}") -endif() -if(EXISTS "${_fail_dest}.download") - message(FATAL_ERROR "Case A: temp download file must not remain at ${_fail_dest}.download") -endif() -message(STATUS "Case A OK: bounded retries + clear final failure, no partial file") - -# --- Case B: success path (requires network) --- -set(_ok_bin "${_cpm_test_root}/ok") -file(MAKE_DIRECTORY "${_ok_bin}") -set(_ok_dest "${_ok_bin}/cmake/CPM_0.38.7.cmake") -execute_process( - COMMAND "${CMAKE_COMMAND}" - "-DCPM_DOWNLOAD_VERSION=0.38.7" - "-DCPM_HASH_SUM=${_cpm_hash}" - "-DCPM_DOWNLOAD_MAX_RETRIES=5" - "-DCMAKE_BINARY_DIR=${_ok_bin}" - -P "${_cpm_script}" - WORKING_DIRECTORY "${_ok_bin}" - RESULT_VARIABLE _ok_rc - OUTPUT_VARIABLE _ok_out - ERROR_VARIABLE _ok_err -) -if(NOT _ok_rc EQUAL 0) - message(FATAL_ERROR "Case B: expected CPM bootstrap success.\n${_ok_out}\n${_ok_err}") -endif() -if(NOT EXISTS "${_ok_dest}") - message(FATAL_ERROR "Case B: missing downloaded file ${_ok_dest}") -endif() -file(SHA256 "${_ok_dest}" _ok_got_hash) -if(NOT _ok_got_hash STREQUAL "${_cpm_hash}") - message(FATAL_ERROR "Case B: hash mismatch got=${_ok_got_hash}") -endif() -if(EXISTS "${_ok_dest}.download") - message(FATAL_ERROR "Case B: temp download file must not remain") -endif() -message(STATUS "Case B OK: download + SHA256 verification succeeded") - -file(REMOVE_RECURSE "${_cpm_test_root}") -message(STATUS "All CPM bootstrap retry checks passed")