From 380774106261e282bed6b7c5fc9b604d5d9c77d7 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 5 Nov 2026 22:17:09 +0100 Subject: [PATCH] [CMake] Always patch the builtins starting from the pristine sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExternalProject re-runs the patch step not only after a new download, but also when the patch command changes, and then on the already patched tree. Since 0db7792e1ec changed the unuran patch command, this breaks the incremental opensuse CI build. The configure-time checks of the other builtins with patches fail the other way around: after a new download, they skip the patches. Add ROOT_BUILTIN_PATCH_COMMAND, whose patch step commits the pristine sources to a git repository in the source directory and resets to them before applying the patches, so it can always run. Trees patched by the old commands are handled by undoing their patches first, or, if that fails, by downloading the sources again. Use it for all builtins with patches. 🤖 Done with the help of AI --- builtins/cfitsio/CMakeLists.txt | 10 +- builtins/cfitsio/cfitsio-marker.diff | 4 - builtins/civetweb/CMakeLists.txt | 16 +-- builtins/civetweb/civetweb-marker.diff | 4 - builtins/ftgl/CMakeLists.txt | 14 +-- builtins/ftgl/ftgl-marker.diff | 4 - builtins/gl2ps/CMakeLists.txt | 7 +- builtins/gl2ps/gl2ps-marker.patch | 4 - builtins/libgif/CMakeLists.txt | 5 +- builtins/unuran/CMakeLists.txt | 8 +- builtins/xxhash/CMakeLists.txt | 9 +- cmake/modules/RootMacros.cmake | 30 ++++++ cmake/scripts/apply_builtin_patches.cmake | 113 ++++++++++++++++++++++ 13 files changed, 163 insertions(+), 65 deletions(-) delete mode 100644 builtins/cfitsio/cfitsio-marker.diff delete mode 100644 builtins/civetweb/civetweb-marker.diff delete mode 100644 builtins/ftgl/ftgl-marker.diff delete mode 100644 builtins/gl2ps/gl2ps-marker.patch create mode 100644 cmake/scripts/apply_builtin_patches.cmake diff --git a/builtins/cfitsio/CMakeLists.txt b/builtins/cfitsio/CMakeLists.txt index c14098bad7f0d..ab9b9839a8d8e 100644 --- a/builtins/cfitsio/CMakeLists.txt +++ b/builtins/cfitsio/CMakeLists.txt @@ -27,12 +27,10 @@ if(WIN32) set(patch_args --ignore-whitespace) endif() -if(NOT EXISTS "${CFITSIO_PREFIX}/src/BUILTIN_CFITSIO/._patched") - set(CFITSIO_PATCH_COMMAND PATCH_COMMAND git init COMMAND git apply ${patch_args} - ${CMAKE_CURRENT_SOURCE_DIR}/cfitsio-no-find-zlib.diff - ${CMAKE_CURRENT_SOURCE_DIR}/no-fortran-wrapper.diff - ${CMAKE_CURRENT_SOURCE_DIR}/cfitsio-marker.diff) -endif() +ROOT_BUILTIN_PATCH_COMMAND(CFITSIO_PATCH_COMMAND + PATCHES ${CMAKE_CURRENT_SOURCE_DIR}/cfitsio-no-find-zlib.diff + ${CMAKE_CURRENT_SOURCE_DIR}/no-fortran-wrapper.diff + GIT_APPLY_ARGS ${patch_args}) ExternalProject_Add( BUILTIN_CFITSIO diff --git a/builtins/cfitsio/cfitsio-marker.diff b/builtins/cfitsio/cfitsio-marker.diff deleted file mode 100644 index 6be2f6ccec01a..0000000000000 --- a/builtins/cfitsio/cfitsio-marker.diff +++ /dev/null @@ -1,4 +0,0 @@ ---- /dev/null 2026-03-12 08:23:04 -+++ ._patched 2026-04-28 08:20:11 -@@ -0,0 +1,1 @@ -+cfitsio has been patched diff --git a/builtins/civetweb/CMakeLists.txt b/builtins/civetweb/CMakeLists.txt index c4d44a39ce686..b3167f9eea0b1 100644 --- a/builtins/civetweb/CMakeLists.txt +++ b/builtins/civetweb/CMakeLists.txt @@ -11,20 +11,14 @@ set(ROOT_CIVETWEB_VERSION 1.16) # with some patches, so almost 1.17 set(ROOT_CIVETWEB_PREFIX ${CMAKE_BINARY_DIR}/builtins/CIVETWEB-prefix) set(ROOT_CIVETWEB_LIBRARY ${ROOT_CIVETWEB_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}civetweb${CMAKE_STATIC_LIBRARY_SUFFIX}) -if (NOT DEFINED GIT_EXECUTABLE) - set(GIT_EXECUTABLE "git") -endif() if(WIN32) set(GIT_PATCH_ARGS "--ignore-space-change") endif() -# check if the patch has already been applied -if(NOT EXISTS "${ROOT_CIVETWEB_PREFIX}/src/BUILTIN_CIVETWEB/._patched") - set(ROOT_CIVETWEB_PATCH_COMMAND PATCH_COMMAND ${GIT_EXECUTABLE} init -b master COMMAND ${GIT_EXECUTABLE} apply ${GIT_PATCH_ARGS} - ${CMAKE_CURRENT_SOURCE_DIR}/1385.patch # https://github.com/civetweb/civetweb/pull/1385 - ${CMAKE_CURRENT_SOURCE_DIR}/1389.patch # https://github.com/civetweb/civetweb/pull/1389 - ${CMAKE_CURRENT_SOURCE_DIR}/1423.patch # https://github.com/civetweb/civetweb/pull/1423 for OpenSSL4 compatibility - ${CMAKE_CURRENT_SOURCE_DIR}/civetweb-marker.diff) -endif() +ROOT_BUILTIN_PATCH_COMMAND(ROOT_CIVETWEB_PATCH_COMMAND + PATCHES ${CMAKE_CURRENT_SOURCE_DIR}/1385.patch # https://github.com/civetweb/civetweb/pull/1385 + ${CMAKE_CURRENT_SOURCE_DIR}/1389.patch # https://github.com/civetweb/civetweb/pull/1389 + ${CMAKE_CURRENT_SOURCE_DIR}/1423.patch # https://github.com/civetweb/civetweb/pull/1423 for OpenSSL4 compatibility + GIT_APPLY_ARGS ${GIT_PATCH_ARGS}) include(ExternalProject) diff --git a/builtins/civetweb/civetweb-marker.diff b/builtins/civetweb/civetweb-marker.diff deleted file mode 100644 index 025de22048d1f..0000000000000 --- a/builtins/civetweb/civetweb-marker.diff +++ /dev/null @@ -1,4 +0,0 @@ ---- /dev/null 2026-03-12 08:23:04 -+++ ._patched 2026-04-28 08:20:11 -@@ -0,0 +1,1 @@ -+civetweb has been patched diff --git a/builtins/ftgl/CMakeLists.txt b/builtins/ftgl/CMakeLists.txt index 9a718ca88fa0d..9370c0dc01fa2 100644 --- a/builtins/ftgl/CMakeLists.txt +++ b/builtins/ftgl/CMakeLists.txt @@ -23,17 +23,9 @@ set(ROOT_FTGL_PATCH_FILE_3 "${CMAKE_CURRENT_SOURCE_DIR}/0003-fix-nullptr.patch") set(ROOT_FTGL_PREFIX ${CMAKE_BINARY_DIR}/builtins/FTGL-prefix) set(ROOT_FTGL_LIBRARY ${ROOT_FTGL_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}ftgl${ROOT_FTGL_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}) -if (NOT DEFINED GIT_EXECUTABLE) - set(GIT_EXECUTABLE "git") -endif() - -if(NOT EXISTS "${ROOT_FTGL_PREFIX}/src/BUILTIN_FTGL/._patched") - set(ROOT_FTGL_PATCH_COMMAND PATCH_COMMAND ${GIT_EXECUTABLE} init COMMAND ${GIT_EXECUTABLE} apply --ignore-space-change --ignore-whitespace - ${ROOT_FTGL_PATCH_FILE_1} - ${ROOT_FTGL_PATCH_FILE_2} - ${ROOT_FTGL_PATCH_FILE_3} - ${CMAKE_CURRENT_SOURCE_DIR}/ftgl-marker.diff) -endif() +ROOT_BUILTIN_PATCH_COMMAND(ROOT_FTGL_PATCH_COMMAND + PATCHES ${ROOT_FTGL_PATCH_FILE_1} ${ROOT_FTGL_PATCH_FILE_2} ${ROOT_FTGL_PATCH_FILE_3} + GIT_APPLY_ARGS --ignore-space-change --ignore-whitespace) include(ExternalProject) diff --git a/builtins/ftgl/ftgl-marker.diff b/builtins/ftgl/ftgl-marker.diff deleted file mode 100644 index 6294690ba8a05..0000000000000 --- a/builtins/ftgl/ftgl-marker.diff +++ /dev/null @@ -1,4 +0,0 @@ ---- /dev/null 2026-03-12 08:23:04 -+++ ._patched 2026-04-28 08:20:11 -@@ -0,0 +1,1 @@ -+ftgl has been patched diff --git a/builtins/gl2ps/CMakeLists.txt b/builtins/gl2ps/CMakeLists.txt index b45b510f302b2..a60d3dce2d6f3 100644 --- a/builtins/gl2ps/CMakeLists.txt +++ b/builtins/gl2ps/CMakeLists.txt @@ -10,7 +10,7 @@ set(ROOT_GL2PS_VERSION 1.4.2) set(ROOT_GL2PS_HASH "8d1c00c1018f96b4b97655482e57dcb0ce42ae2f1d349cd6d4191e7848d9ffe9") # Backport fix for static Win32 having being overwritten by shared lib: https://gitlab.onelab.info/gl2ps/gl2ps/-/work_items/30 # We use all seven commits between 1.4.2 and before bump to 1.4.3 -set(ROOT_GL2PS_PATCH_FILES ${CMAKE_CURRENT_SOURCE_DIR}/v142_post_tag_fixes.patch ${CMAKE_CURRENT_SOURCE_DIR}/gl2ps-marker.patch) +set(ROOT_GL2PS_PATCH_FILES ${CMAKE_CURRENT_SOURCE_DIR}/v142_post_tag_fixes.patch) set(ROOT_GL2PS_PREFIX ${CMAKE_BINARY_DIR}/builtins/GL2PS-prefix) @@ -29,10 +29,7 @@ if(WIN32) set(patch_args --ignore-whitespace) endif() -# check if the patch has already been applied -if(NOT EXISTS "${ROOT_GL2PS_PREFIX}/src/BUILTIN_GL2PS/._patched") - set(GL2PS_PATCH_COMMAND PATCH_COMMAND git init COMMAND git apply ${patch_args} ${ROOT_GL2PS_PATCH_FILES}) -endif() +ROOT_BUILTIN_PATCH_COMMAND(GL2PS_PATCH_COMMAND PATCHES ${ROOT_GL2PS_PATCH_FILES} GIT_APPLY_ARGS ${patch_args}) ExternalProject_Add( BUILTIN_GL2PS diff --git a/builtins/gl2ps/gl2ps-marker.patch b/builtins/gl2ps/gl2ps-marker.patch deleted file mode 100644 index 44b7174ea80ff..0000000000000 --- a/builtins/gl2ps/gl2ps-marker.patch +++ /dev/null @@ -1,4 +0,0 @@ ---- /dev/null 2026-03-12 08:23:04 -+++ ._patched 2026-04-28 08:20:11 -@@ -0,0 +1,1 @@ -+gl2ps has been patched diff --git a/builtins/libgif/CMakeLists.txt b/builtins/libgif/CMakeLists.txt index efb7338762c6f..099dd7989712f 100644 --- a/builtins/libgif/CMakeLists.txt +++ b/builtins/libgif/CMakeLists.txt @@ -28,10 +28,7 @@ if(WIN32) set(patch_args --ignore-whitespace) endif() -# check if the patch has already been applied -if(NOT EXISTS "${ROOT_LIBGIF_PREFIX}/src/BUILTIN_LIBGIF/CMakeLists.txt") - set(LIBGIF_PATCH_COMMAND PATCH_COMMAND git init COMMAND git apply ${patch_args} ${ROOT_LIBGIF_PATCH_FILE}) -endif() +ROOT_BUILTIN_PATCH_COMMAND(LIBGIF_PATCH_COMMAND PATCHES ${ROOT_LIBGIF_PATCH_FILE} GIT_APPLY_ARGS ${patch_args}) ExternalProject_Add( BUILTIN_LIBGIF diff --git a/builtins/unuran/CMakeLists.txt b/builtins/unuran/CMakeLists.txt index 1858fcf19cc91..26619e695740d 100644 --- a/builtins/unuran/CMakeLists.txt +++ b/builtins/unuran/CMakeLists.txt @@ -11,9 +11,8 @@ set(ROOT_UNURAN_HASH "098793854c590b4c2c7e98bc48a45408875f48c5ad47650b5fabbd3e94 set(ROOT_UNURAN_PREFIX ${CMAKE_BINARY_DIR}/builtins/UNURAN-prefix) set(ROOT_UNURAN_LIBRARY ${ROOT_UNURAN_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}unuran${CMAKE_STATIC_LIBRARY_SUFFIX}) set(ROOT_UNURAN_PATCH_FILE_1 "${CMAKE_CURRENT_SOURCE_DIR}/0001-allow-compilation-from-CMake.patch") -if (NOT DEFINED GIT_EXECUTABLE) - set(GIT_EXECUTABLE "git") -endif() +ROOT_BUILTIN_PATCH_COMMAND(ROOT_UNURAN_PATCH_COMMAND PATCHES ${ROOT_UNURAN_PATCH_FILE_1} + GIT_APPLY_ARGS --ignore-space-change --ignore-whitespace) include(ExternalProject) @@ -39,8 +38,7 @@ ExternalProject_Add( URL ${lcgpackages}/unuran-${ROOT_UNURAN_VERSION}.tar.gz https://statmath.wu.ac.at/unuran/unuran-${ROOT_UNURAN_VERSION}.tar.gz URL_HASH SHA256=${ROOT_UNURAN_HASH} - PATCH_COMMAND ${GIT_EXECUTABLE} init -q - COMMAND ${GIT_EXECUTABLE} apply --ignore-space-change --ignore-whitespace ${ROOT_UNURAN_PATCH_FILE_1} + ${ROOT_UNURAN_PATCH_COMMAND} SOURCE_SUBDIR src LOG_DOWNLOAD TRUE # The following three are disabled to avoid CI failure in Ubuntu22 that has outdated CMake version diff --git a/builtins/xxhash/CMakeLists.txt b/builtins/xxhash/CMakeLists.txt index 9759452bbfdee..fc5d4afe0d638 100644 --- a/builtins/xxhash/CMakeLists.txt +++ b/builtins/xxhash/CMakeLists.txt @@ -15,13 +15,8 @@ set(ROOT_XXHASH_PATCH_FILE ${CMAKE_CURRENT_SOURCE_DIR}/xxhash_add_cmakelists.pat set(ROOT_XXHASH_PREFIX ${CMAKE_BINARY_DIR}/builtins/XXHASH-prefix) -if (NOT DEFINED GIT_EXECUTABLE) - set(GIT_EXECUTABLE "git") -endif() -# check if the patch has already been applied -if(NOT EXISTS "${ROOT_XXHASH_PREFIX}/src/BUILTIN_XXHASH/CMakeLists.txt") - set(XXHASH_PATCH_COMMAND PATCH_COMMAND ${GIT_EXECUTABLE} init COMMAND ${GIT_EXECUTABLE} apply --ignore-space-change --ignore-whitespace ${ROOT_XXHASH_PATCH_FILE}) -endif() +ROOT_BUILTIN_PATCH_COMMAND(XXHASH_PATCH_COMMAND PATCHES ${ROOT_XXHASH_PATCH_FILE} + GIT_APPLY_ARGS --ignore-space-change --ignore-whitespace) set(ROOT_XXHASH_LIBRARY ${ROOT_XXHASH_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}xxhash${CMAKE_STATIC_LIBRARY_SUFFIX}) diff --git a/cmake/modules/RootMacros.cmake b/cmake/modules/RootMacros.cmake index 3abb71ec5b1f2..5cc523aa360ee 100644 --- a/cmake/modules/RootMacros.cmake +++ b/cmake/modules/RootMacros.cmake @@ -3789,3 +3789,33 @@ function (ROOT_GET_CLANG_LIBRARIES clang_libraries) endforeach(extra_lib) SET(${clang_libraries} "${found_libraries}" PARENT_SCOPE) endfunction(ROOT_GET_CLANG_LIBRARIES) + +#--------------------------------------------------------------------------------------------------- +# ROOT_BUILTIN_PATCH_COMMAND( PATCHES ... [GIT_APPLY_ARGS ...]) +# +# Sets to the PATCH_COMMAND for ExternalProject_Add that applies the given patches, in this +# order, to the sources of a builtin with git apply. +# +# The patch step always starts again from the pristine sources (see +# cmake/scripts/apply_builtin_patches.cmake), so that it doesn't matter why ExternalProject re-runs +# it: after a new download, or because the patch command changed. The command contains a hash of +# the patches, so that also changing the content of a patch re-runs the patch step. +#--------------------------------------------------------------------------------------------------- +function(ROOT_BUILTIN_PATCH_COMMAND var) + cmake_parse_arguments(ARG "" "" "PATCHES;GIT_APPLY_ARGS" ${ARGN}) + set(script ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../scripts/apply_builtin_patches.cmake) + + set(hashes) + foreach(file IN LISTS ARG_PATCHES ITEMS ${script}) + file(SHA256 ${file} hash) + list(APPEND hashes ${hash}) + endforeach() + string(SHA256 hash "${hashes}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${ARG_PATCHES} ${script}) + + # ExternalProject doesn't replace in the commands, but is + # the same directory, unless LOG_DIR is set. + set(${var} PATCH_COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR= -DSTAMP_DIR= + -DPATCHES_HASH=${hash} -P ${script} -- ${ARG_GIT_APPLY_ARGS} ${ARG_PATCHES} + PARENT_SCOPE) +endfunction() diff --git a/cmake/scripts/apply_builtin_patches.cmake b/cmake/scripts/apply_builtin_patches.cmake new file mode 100644 index 0000000000000..9adc789f300b2 --- /dev/null +++ b/cmake/scripts/apply_builtin_patches.cmake @@ -0,0 +1,113 @@ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +# Patch step of the builtins, see ROOT_BUILTIN_PATCH_COMMAND in RootMacros.cmake: +# +# cmake -DSOURCE_DIR= -DSTAMP_DIR= -P apply_builtin_patches.cmake +# -- [...] ... +# +# ExternalProject re-runs the patch step not only on a freshly downloaded source +# tree, but also whenever the patch command changes, and then on the tree that +# is already patched. Therefore, the patches are always applied to the pristine +# sources: the first run commits them to a git repository in the source +# directory (unless the sources are a git clone already), and later runs reset +# the tree to that commit before applying the patches again. + +set(apply_args) +set(patches) +set(after_separator FALSE) +math(EXPR last_arg "${CMAKE_ARGC} - 1") +foreach(i RANGE ${last_arg}) + set(arg "${CMAKE_ARGV${i}}") + if(NOT after_separator) + if(arg STREQUAL "--") + set(after_separator TRUE) + endif() + elseif(arg MATCHES "^-") + list(APPEND apply_args ${arg}) + else() + list(APPEND patches ${arg}) + endif() +endforeach() + +# Git is looked up here and not passed in the patch command, so that a different +# path to git doesn't change the command, which would re-run the patch step. +find_package(Git REQUIRED) + +# Never convert line endings, so that resetting restores the sources as they +# were, and work also in build trees owned by another user. +set(git ${GIT_EXECUTABLE} -c core.autocrlf=false -c core.safecrlf=false -c core.fsmonitor=false + -c gc.auto=0 -c safe.directory=*) + +function(run_git) + execute_process(COMMAND ${git} ${ARGN} WORKING_DIRECTORY ${SOURCE_DIR} COMMAND_ERROR_IS_FATAL ANY) +endfunction() + +# Returns in whether the git command succeeded, without output. +function(try_git result_var) + execute_process(COMMAND ${git} ${ARGN} WORKING_DIRECTORY ${SOURCE_DIR} + RESULT_VARIABLE result OUTPUT_QUIET ERROR_QUIET) + if(result EQUAL 0) + set(${result_var} TRUE PARENT_SCOPE) + else() + set(${result_var} FALSE PARENT_SCOPE) + endif() +endfunction() + +# Left behind if an earlier run was interrupted. +file(REMOVE ${SOURCE_DIR}/.git/index.lock) + +set(has_commit FALSE) +if(EXISTS ${SOURCE_DIR}/.git) + try_git(has_commit rev-parse -q --verify HEAD) +endif() + +if(has_commit) + # Undo the patches of an earlier run. + run_git(reset -q --hard) + run_git(clean -q -d -f -f -x) +else() + # The repository also makes git apply take the paths in the patches relative + # to the source directory, when the build directory is inside a repository. + run_git(init -q) + + # The patches might have been applied already without committing the pristine + # sources, by the patch commands of the builtins before this script was used. + # Undo those that are there, in reverse order. If this doesn't result in the + # pristine sources, applying the patches below fails, and the sources are + # downloaded again with the next build. + set(reversed_patches ${patches}) + list(REVERSE reversed_patches) + foreach(patch IN LISTS reversed_patches) + try_git(reversible apply ${apply_args} --reverse --check ${patch}) + if(reversible) + run_git(apply ${apply_args} --reverse ${patch}) + endif() + endforeach() + + run_git(add -A -f) + run_git(-c user.name=ROOT -c user.email=root@localhost -c commit.gpgsign=false + commit -q --no-verify --allow-empty -m "Pristine sources") +endif() + +# One patch at a time, because git apply would otherwise take the -p level it +# guessed for a patch in the traditional diff format also for the next patches. +foreach(patch IN LISTS patches) + execute_process(COMMAND ${git} apply ${apply_args} ${patch} WORKING_DIRECTORY ${SOURCE_DIR} + RESULT_VARIABLE apply_result) + if(NOT apply_result EQUAL 0) + # The commit might not hold the pristine sources. Removing the stamp of the + # download step makes the next build start again from a new download. + file(GLOB_RECURSE download_stamps ${STAMP_DIR}/*-download) + if(download_stamps) + file(REMOVE ${download_stamps}) + message(FATAL_ERROR "Failed to apply ${patch} to ${SOURCE_DIR}. " + "The sources will be downloaded again with the next build.") + endif() + message(FATAL_ERROR "Failed to apply ${patch} to ${SOURCE_DIR}. " + "Remove that directory to start again from the pristine sources.") + endif() +endforeach()