Skip to content

Commit 5060d97

Browse files
committed
fix: Patch commands fail trying to re-apply the same patch twice
Fixes #618
1 parent d9364ce commit 5060d97

File tree

3 files changed

+179
-28
lines changed

3 files changed

+179
-28
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ build/
22
/build*
33
/.vscode
44
*.DS_Store
5+
# Autogenerated code by CPM.cmake
6+
cmake/cpm_apply_patches.cmake

cmake/CPM.cmake

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,62 @@ macro(cpm_set_policies)
112112
endmacro()
113113
cpm_set_policies()
114114

115+
macro(cpm_generate_apply_patches_script)
116+
set(_cpm_patch_script "${CPM_CURRENT_DIRECTORY}/cpm_apply_patches.cmake")
117+
118+
if(NOT EXISTS "${_cpm_patch_script}")
119+
file(
120+
WRITE "${_cpm_patch_script}"
121+
[=[
122+
# Auto-generated patch application script
123+
separate_arguments(PATCH_FILES)
124+
125+
foreach(patch_file IN LISTS PATCH_FILES)
126+
message(STATUS "Checking patch: ${patch_file}")
127+
128+
execute_process(
129+
COMMAND "${PATCH_EXECUTABLE}" --dry-run -p1
130+
INPUT_FILE "${patch_file}"
131+
RESULT_VARIABLE dry_run_result
132+
OUTPUT_VARIABLE dry_out
133+
ERROR_VARIABLE dry_err
134+
)
135+
136+
if(dry_run_result EQUAL 0)
137+
message(STATUS "Applying patch: ${patch_file}")
138+
execute_process(
139+
COMMAND "${PATCH_EXECUTABLE}" -p1
140+
INPUT_FILE "${patch_file}"
141+
RESULT_VARIABLE apply_result
142+
OUTPUT_VARIABLE apply_out
143+
ERROR_VARIABLE apply_err
144+
)
145+
if(apply_result EQUAL 0)
146+
message(STATUS "Applied patch: ${patch_file}")
147+
else()
148+
message(FATAL_ERROR "Patch failed: ${patch_file}\n${apply_err}")
149+
endif()
150+
else()
151+
execute_process(
152+
COMMAND "${PATCH_EXECUTABLE}" --dry-run -p1 --reverse
153+
INPUT_FILE "${patch_file}"
154+
RESULT_VARIABLE reverse_result
155+
OUTPUT_VARIABLE reverse_out
156+
ERROR_VARIABLE reverse_err
157+
)
158+
if(reverse_result EQUAL 0)
159+
message(STATUS "Patch already applied: ${patch_file}")
160+
else()
161+
message(FATAL_ERROR "Patch cannot be applied and is not already applied: ${patch_file}\n${dry_err}")
162+
endif()
163+
endif()
164+
endforeach()
165+
]=]
166+
)
167+
endif()
168+
endmacro()
169+
cpm_generate_apply_patches_script()
170+
115171
option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies"
116172
$ENV{CPM_USE_LOCAL_PACKAGES}
117173
)
@@ -541,66 +597,69 @@ endfunction()
541597
# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended
542598
# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`.
543599
function(cpm_add_patches)
544-
# Return if no patch files are supplied.
600+
# Return early if no patch files are provided
545601
if(NOT ARGN)
546602
return()
547603
endif()
548604

549-
# Find the patch program.
605+
# -----------------------------------------------------------------------------------------------
606+
# Locate the 'patch' executable
607+
# -----------------------------------------------------------------------------------------------
550608
find_program(PATCH_EXECUTABLE patch)
609+
551610
if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE)
552611
# The Windows git executable is distributed with patch.exe. Find the path to the executable, if
553612
# it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe.
554613
find_package(Git QUIET)
555614
if(GIT_EXECUTABLE)
556-
get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY)
557-
get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY)
558-
get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY)
615+
get_filename_component(_git_bin_dir "${GIT_EXECUTABLE}" DIRECTORY)
616+
get_filename_component(_git_root_1up "${_git_bin_dir}" DIRECTORY)
617+
get_filename_component(_git_root_2up "${_git_root_1up}" DIRECTORY)
618+
559619
find_program(
560-
PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin"
561-
"${extra_search_path_2up}/usr/bin"
620+
PATCH_EXECUTABLE patch HINTS "${_git_root_1up}/usr/bin" "${_git_root_2up}/usr/bin"
562621
)
563622
endif()
564623
endif()
624+
565625
if(NOT PATCH_EXECUTABLE)
566626
message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.")
567627
endif()
568628

569-
# Create a temporary
570-
set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS})
629+
# -----------------------------------------------------------------------------------------------
630+
# Resolve and validate all patch file paths
631+
# -----------------------------------------------------------------------------------------------
632+
set(resolved_patch_files)
571633

572-
# Ensure each file exists (or error out) and add it to the list.
573-
set(first_item True)
574-
foreach(PATCH_FILE ${ARGN})
634+
foreach(PATCH_FILE IN LISTS ARGN)
575635
# Make sure the patch file exists, if we can't find it, try again in the current directory.
576636
if(NOT EXISTS "${PATCH_FILE}")
577-
if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}")
637+
set(_fallback_path "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}")
638+
if(NOT EXISTS "${_fallback_path}")
578639
message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'")
579640
endif()
580-
set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}")
641+
set(PATCH_FILE "${_fallback_path}")
581642
endif()
582643

583644
# Convert to absolute path for use with patch file command.
584645
get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE)
585-
586-
# The first patch entry must be preceded by "PATCH_COMMAND" while the following items are
587-
# preceded by "&&".
588-
if(first_item)
589-
set(first_item False)
590-
list(APPEND temp_list "PATCH_COMMAND")
591-
else()
592-
list(APPEND temp_list "&&")
593-
endif()
594-
# Add the patch command to the list
595-
list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}")
646+
list(APPEND resolved_patch_files "${PATCH_FILE}")
596647
endforeach()
597648

598-
# Move temp out into parent scope.
649+
# -----------------------------------------------------------------------------------------------
650+
# Construct the patch command
651+
# -----------------------------------------------------------------------------------------------
652+
string(JOIN " " joined_patch_files ${resolved_patch_files})
653+
654+
set(_patch_command cmake -D "PATCH_FILES=${joined_patch_files}" -D
655+
"PATCH_EXECUTABLE=${PATCH_EXECUTABLE}" -P "${_cpm_patch_script}"
656+
)
657+
658+
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS PATCH_COMMAND ${_patch_command})
599659
set(CPM_ARGS_UNPARSED_ARGUMENTS
600-
${temp_list}
660+
"${CPM_ARGS_UNPARSED_ARGUMENTS}"
601661
PARENT_SCOPE
602662
)
603-
604663
endfunction()
605664

606665
# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload

test/unit/package_patch.cmake

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
2+
3+
include(${CPM_PATH}/testing.cmake)
4+
include(${CPM_PATH}/CPM.cmake)
5+
6+
# ----------------------------------------------------------------------------------------
7+
# Setup: Define common environment
8+
# ----------------------------------------------------------------------------------------
9+
set(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
10+
11+
# ----------------------------------------------------------------------------------------
12+
# Test Case 1: Single patch file
13+
# ----------------------------------------------------------------------------------------
14+
function(run_test_single_patch)
15+
set(_patch1 "${CMAKE_CURRENT_BINARY_DIR}/dummy1.patch")
16+
file(WRITE "${_patch1}" "dummy patch content")
17+
18+
unset(CPM_ARGS_UNPARSED_ARGUMENTS)
19+
cpm_add_patches("${_patch1}")
20+
21+
list(FIND CPM_ARGS_UNPARSED_ARGUMENTS "PATCH_COMMAND" _idx1)
22+
assert_not_equal("${_idx1}" "-1")
23+
24+
math(EXPR _start1 "${_idx1} + 1")
25+
list(SUBLIST CPM_ARGS_UNPARSED_ARGUMENTS ${_start1} -1 _args1)
26+
27+
set(_found1 FALSE)
28+
foreach(arg IN LISTS _args1)
29+
if(arg MATCHES "PATCH_FILES=.*dummy1\\.patch")
30+
set(_found1 TRUE)
31+
endif()
32+
endforeach()
33+
assert_truthy(_found1)
34+
35+
file(REMOVE "${_patch1}")
36+
endfunction()
37+
38+
# ----------------------------------------------------------------------------------------
39+
# Test Case 2: Multiple patch files
40+
# ----------------------------------------------------------------------------------------
41+
function(run_test_multiple_patches)
42+
set(_patch2 "${CMAKE_CURRENT_BINARY_DIR}/dummy2.patch")
43+
set(_patch3 "${CMAKE_CURRENT_BINARY_DIR}/dummy3.patch")
44+
file(WRITE "${_patch2}" "dummy patch 2")
45+
file(WRITE "${_patch3}" "dummy patch 3")
46+
47+
unset(CPM_ARGS_UNPARSED_ARGUMENTS)
48+
cpm_add_patches("${_patch2}" "${_patch3}")
49+
50+
list(FIND CPM_ARGS_UNPARSED_ARGUMENTS "PATCH_COMMAND" _idx2)
51+
assert_not_equal("${_idx2}" "-1")
52+
53+
math(EXPR _start2 "${_idx2} + 1")
54+
list(SUBLIST CPM_ARGS_UNPARSED_ARGUMENTS ${_start2} -1 _args2)
55+
56+
set(_found2 FALSE)
57+
set(_found3 FALSE)
58+
59+
foreach(arg IN LISTS _args2)
60+
if(arg MATCHES "dummy2\\.patch")
61+
set(_found2 TRUE)
62+
endif()
63+
if(arg MATCHES "dummy3\\.patch")
64+
set(_found3 TRUE)
65+
endif()
66+
endforeach()
67+
68+
assert_truthy(_found2)
69+
assert_truthy(_found3)
70+
71+
file(REMOVE "${_patch2}")
72+
file(REMOVE "${_patch3}")
73+
endfunction()
74+
75+
# ----------------------------------------------------------------------------------------
76+
# Test Case 3: No patch files
77+
# ----------------------------------------------------------------------------------------
78+
function(run_test_no_patches)
79+
unset(CPM_ARGS_UNPARSED_ARGUMENTS)
80+
cpm_add_patches()
81+
82+
assert_not_defined(CPM_ARGS_UNPARSED_ARGUMENTS)
83+
endfunction()
84+
85+
# ----------------------------------------------------------------------------------------
86+
# Run all test cases
87+
# ----------------------------------------------------------------------------------------
88+
run_test_single_patch()
89+
run_test_multiple_patches()
90+
run_test_no_patches()

0 commit comments

Comments
 (0)