From 436f2e2b15c3eac5d2c33940238b48a1cfa144af Mon Sep 17 00:00:00 2001 From: jmorganca Date: Sat, 18 Apr 2026 23:34:54 -0700 Subject: [PATCH] llama/compat: make patch-apply idempotent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FetchContent's PATCH_COMMAND runs after each update step — including on incremental rebuilds. `git apply` fails when the patch is already applied, which bricks the build until the dev wipes build/ entirely. Fix by routing the apply through a small apply-patch.cmake helper that checks `git apply --reverse --check` first. If the patch cleanly reverses, it's already applied and we skip. Otherwise apply forward. Both branches surface real errors (drift against upstream, missing patch file, etc.). Verified: fresh configure+build applies the patch once; re-running the same commands is a no-op with no errors. --- llama/compat/apply-patch.cmake | 44 ++++++++++++++++++++++++++++++++++ llama/compat/compat.cmake | 11 +++++---- 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 llama/compat/apply-patch.cmake diff --git a/llama/compat/apply-patch.cmake b/llama/compat/apply-patch.cmake new file mode 100644 index 000000000..708be4332 --- /dev/null +++ b/llama/compat/apply-patch.cmake @@ -0,0 +1,44 @@ +# Idempotent patch applier used by compat.cmake. +# +# Invocation (from a CMake PATCH_COMMAND): +# cmake -DPATCH_FILE= -P apply-patch.cmake +# +# The patch is applied in the current working directory (which ExternalProject +# / FetchContent sets to the fetched source's SOURCE_DIR). If the patch is +# already applied — detected via `git apply --reverse --check` — this script +# is a no-op. This makes re-configuring and re-building the project safe. + +if(NOT DEFINED PATCH_FILE) + message(FATAL_ERROR "apply-patch.cmake: PATCH_FILE not set") +endif() +if(NOT EXISTS "${PATCH_FILE}") + message(FATAL_ERROR "apply-patch.cmake: PATCH_FILE does not exist: ${PATCH_FILE}") +endif() + +find_package(Git QUIET REQUIRED) + +# If the patch can be REVERSED cleanly, it's already applied. Skip. +execute_process( + COMMAND ${GIT_EXECUTABLE} apply --reverse --check "${PATCH_FILE}" + RESULT_VARIABLE _reverse_check + OUTPUT_QUIET ERROR_QUIET +) +if(_reverse_check EQUAL 0) + message(STATUS "llama/compat: patch already applied, skipping") + return() +endif() + +# Otherwise, apply forward. +execute_process( + COMMAND ${GIT_EXECUTABLE} apply --whitespace=nowarn "${PATCH_FILE}" + RESULT_VARIABLE _apply_result +) +if(NOT _apply_result EQUAL 0) + message(FATAL_ERROR + "llama/compat: failed to apply ${PATCH_FILE}\n" + "This usually means upstream llama.cpp has drifted. " + "Regenerate the patch (see llama/compat/README.md) against the " + "pinned LLAMA_CPP_VERSION and retry.") +endif() + +message(STATUS "llama/compat: applied patch") diff --git a/llama/compat/compat.cmake b/llama/compat/compat.cmake index db6d429fb..ef8fc9dfe 100644 --- a/llama/compat/compat.cmake +++ b/llama/compat/compat.cmake @@ -24,9 +24,9 @@ set(_compat_dir ${CMAKE_CURRENT_LIST_DIR}) # Expose a single variable the main CMakeLists passes into FetchContent's -# PATCH_COMMAND. Uses CMake's own `-E copy` so it's cross-platform; uses -# `git apply` because the patch is in unified git-diff format (same as what -# `git diff` produces — regeneration is one command, see README). +# PATCH_COMMAND. Uses CMake's own `-E copy` for cross-platform file copy. +# The patch itself is applied via a small CMake script so the step is +# idempotent — re-configuring or rebuilding won't fail with "already applied". set(OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${_compat_dir}/llama-ollama-compat.h" @@ -34,8 +34,9 @@ set(OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND COMMAND ${CMAKE_COMMAND} -E copy "${_compat_dir}/llama-ollama-compat.cpp" "src/llama-ollama-compat.cpp" - COMMAND git apply --whitespace=nowarn - "${_compat_dir}/upstream-edits.patch" + COMMAND ${CMAKE_COMMAND} + -DPATCH_FILE=${_compat_dir}/upstream-edits.patch + -P ${_compat_dir}/apply-patch.cmake CACHE INTERNAL "llama.cpp compat patch command for FetchContent") # Also export the individual paths in case callers want to do something