diff --git a/README.md b/README.md index 99e756fa..33fbbc89 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Afterwards, any targets defined in the dependency can be used directly. CPMAddPackage( NAME # The unique name of the dependency (should be the exported target's name) VERSION # The minimum version of the dependency (optional, defaults to 0) + PATCHES # Patch files to be applied sequentially using patch and PATCH_OPTIONS (optional) OPTIONS # Configuration options passed to the dependency (optional) DOWNLOAD_ONLY # If set, the project is downloaded, but not configured (optional) [...] # Origin parameters forwarded to FetchContent_Declare, see below @@ -78,6 +79,8 @@ If `GIT_TAG` hasn't been explicitly specified it defaults to `v(VERSION)`, a com On the other hand, if `VERSION` hasn't been explicitly specified, CPM can automatically identify the version from the git tag in some common cases. `GIT_TAG` can also be set to a specific commit or a branch name such as `master`, however this isn't recommended, as such packages will only be updated when the cache is cleared. +`PATCHES` takes a list of patch files to apply sequentially. For a basic example, see [Highway](examples/highway/CMakeLists.txt). + If an additional optional parameter `EXCLUDE_FROM_ALL` is set to a truthy value, then any targets defined inside the dependency won't be built by default. See the [CMake docs](https://cmake.org/cmake/help/latest/prop_tgt/EXCLUDE_FROM_ALL.html) for details. If an additional optional parameter `SYSTEM` is set to a truthy value, the SYSTEM directory property of the subdirectory added will be set to true. diff --git a/cmake/.cmake-format-additional_commands-cpm b/cmake/.cmake-format-additional_commands-cpm index 5dd80256..36f54b0d 100644 --- a/cmake/.cmake-format-additional_commands-cpm +++ b/cmake/.cmake-format-additional_commands-cpm @@ -31,6 +31,7 @@ parse: EXCLUDE_FROM_ALL: 1 SYSTEM: 1 SOURCE_SUBDIR: 1 + PATCHES: + OPTIONS: + cpmfindpackage: pargs: diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake index ba62fd20..b273c3bb 100644 --- a/cmake/CPM.cmake +++ b/cmake/CPM.cmake @@ -464,6 +464,69 @@ function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) endfunction() +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path ${extra_search_path} DIRECTORY) + find_program(PATCH_EXECUTABLE patch HINTS "${extra_search_path}/usr/bin") + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + # method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload # FetchContent calls. As these are internal cmake properties, this method should be used carefully # and may need modification in future CMake versions. Source: @@ -537,7 +600,7 @@ function(CPMAddPackage) CUSTOM_CACHE_KEY ) - set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") @@ -628,6 +691,7 @@ function(CPMAddPackage) SOURCE_DIR "${PACKAGE_SOURCE}" EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" OPTIONS "${CPM_ARGS_OPTIONS}" SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" @@ -683,6 +747,8 @@ function(CPMAddPackage) set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) endif() + cpm_add_patches(${CPM_ARGS_PATCHES}) + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) elseif(DEFINED CPM_ARGS_SOURCE_DIR) diff --git a/examples/highway/CMakeLists.txt b/examples/highway/CMakeLists.txt new file mode 100644 index 00000000..96241dfc --- /dev/null +++ b/examples/highway/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(CPMExamplePatchHighway) + +# ---- Dependencies ---- + +include(../../cmake/CPM.cmake) + +# Google's highway Includes a SIMD sorting function that is faster than x86-simd-sort for larger +# arrays. See: https://github.com/google/highway/blob/master/g3doc/quick_reference.md +CPMAddPackage( + NAME highway + URL https://github.com/google/highway/archive/refs/tags/1.1.0.tar.gz + URL_HASH SHA256=354a8b4539b588e70b98ec70844273e3f2741302c4c377bcc4e81b3d1866f7c9 + PATCHES "highway.patch" # This adds SYSTEM to the includes. + OPTIONS "HWY_ENABLE_EXAMPLES OFF" "HWY_ENABLE_INSTALL OFF" "HWY_ENABLE_TESTS OFF" +) + +# ---- Executable ---- + +if(LINUX) + # This would cause a float compare error inside the highway header code if the patch is NOT + # applied. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wfloat-equal") +endif() + +add_executable(CPMExamplePatchHighway "main.cpp") +target_link_libraries(CPMExamplePatchHighway hwy hwy_contrib) diff --git a/examples/highway/highway.patch b/examples/highway/highway.patch new file mode 100644 index 00000000..0b9537e8 --- /dev/null +++ b/examples/highway/highway.patch @@ -0,0 +1,28 @@ +Common subdirectories: a/.bcr and b/.bcr +Common subdirectories: a/.github and b/.github +diff -u a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt 2024-05-21 12:50:37.738318520 -0500 ++++ b/CMakeLists.txt 2024-05-21 12:49:59.914226871 -0500 +@@ -350,7 +350,7 @@ + target_compile_options(hwy PRIVATE ${HWY_FLAGS}) + set_property(TARGET hwy PROPERTY POSITION_INDEPENDENT_CODE ON) + set_target_properties(hwy PROPERTIES VERSION ${LIBRARY_VERSION} SOVERSION ${LIBRARY_SOVERSION}) +-target_include_directories(hwy PUBLIC ++target_include_directories(hwy SYSTEM PUBLIC + $ + $) + target_compile_features(hwy PUBLIC cxx_std_11) +@@ -370,7 +370,7 @@ + target_compile_options(hwy_contrib PRIVATE ${HWY_FLAGS}) + set_property(TARGET hwy_contrib PROPERTY POSITION_INDEPENDENT_CODE ON) + set_target_properties(hwy_contrib PROPERTIES VERSION ${LIBRARY_VERSION} SOVERSION ${LIBRARY_SOVERSION}) +-target_include_directories(hwy_contrib PUBLIC ++target_include_directories(hwy_contrib SYSTEM PUBLIC + $ + $) + target_compile_features(hwy_contrib PUBLIC cxx_std_11) +Common subdirectories: a/cmake and b/cmake +Common subdirectories: a/debian and b/debian +Common subdirectories: a/docs and b/docs +Common subdirectories: a/g3doc and b/g3doc +Common subdirectories: a/hwy and b/hwy diff --git a/examples/highway/main.cpp b/examples/highway/main.cpp new file mode 100644 index 00000000..c1d5a7af --- /dev/null +++ b/examples/highway/main.cpp @@ -0,0 +1,27 @@ +#include // hwy::VQSort() for large data sets + +#include +#include +#include + +// Use hwy::VQSort to sort larger vectors +inline void sort_large(std::vector& v) { + hwy::VQSort(v.data(), v.size(), hwy::SortAscending{}); +} + +int main(int, char**) { + std::random_device random_device; + std::default_random_engine random_engine(random_device()); + std::uniform_real_distribution uniform_dist(0.0, 100.0); + + const std::size_t sz = 100000; + std::vector v; + v.reserve(sz); + for (std::size_t i = 0; i < sz; ++i) { + v.push_back(uniform_dist(random_engine)); + } + + sort_large(v); + + return 0; +} diff --git a/test/integration/templates/using-patch-adder/lists.in.cmake b/test/integration/templates/using-patch-adder/lists.in.cmake new file mode 100644 index 00000000..83c48d34 --- /dev/null +++ b/test/integration/templates/using-patch-adder/lists.in.cmake @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(using-patch-adder) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +include("%{cpm_path}") + +%{packages} + +add_executable(using-patch-adder using-patch-adder.cpp) + +target_link_libraries(using-patch-adder adder) diff --git a/test/integration/templates/using-patch-adder/patches/001-test_patches_command.patch b/test/integration/templates/using-patch-adder/patches/001-test_patches_command.patch new file mode 100644 index 00000000..281b1b8b --- /dev/null +++ b/test/integration/templates/using-patch-adder/patches/001-test_patches_command.patch @@ -0,0 +1,12 @@ +diff --git a/code/adder/adder.hpp b/code/adder/adder.hpp +index fdb9324..7c2fa00 100644 +--- a/code/adder/adder.hpp ++++ b/code/adder/adder.hpp +@@ -1,6 +1,6 @@ + #pragma once + +-namespace adder ++namespace patched + { + int add(int a, int b); + } diff --git a/test/integration/templates/using-patch-adder/patches/002-test_patches_command.patch b/test/integration/templates/using-patch-adder/patches/002-test_patches_command.patch new file mode 100644 index 00000000..57146fc0 --- /dev/null +++ b/test/integration/templates/using-patch-adder/patches/002-test_patches_command.patch @@ -0,0 +1,12 @@ +diff --git a/code/adder/adder.cpp b/code/adder/adder.cpp +index fc6e767..44b1197 100644 +--- a/code/adder/adder.cpp ++++ b/code/adder/adder.cpp +@@ -1,6 +1,6 @@ + #include "adder.hpp" + +-namespace adder ++namespace patched + { + int add(int a, int b) + { diff --git a/test/integration/templates/using-patch-adder/using-patch-adder.cpp b/test/integration/templates/using-patch-adder/using-patch-adder.cpp new file mode 100644 index 00000000..5e6659ad --- /dev/null +++ b/test/integration/templates/using-patch-adder/using-patch-adder.cpp @@ -0,0 +1,8 @@ +#include +#include + +int main() { + int sum = patched::add(5, 3); + std::printf("%d\n", sum); + return 0; +} diff --git a/test/integration/test_patches_command.rb b/test/integration/test_patches_command.rb new file mode 100644 index 00000000..f9555b74 --- /dev/null +++ b/test/integration/test_patches_command.rb @@ -0,0 +1,27 @@ +require_relative './lib' + +# Tests using a multi-argumenet PATCHES command to fetch and modify a dependency + +class DownloadCommand < IntegrationTest + + def test_fetch_dependency_using_download_command + prj = make_project from_template: 'using-patch-adder' + + prj.create_lists_from_default_template package: <<~PACK + set(DOWNLOAD_DIR ${CMAKE_BINARY_DIR}/_deps/testpack-adder-src) + CPMAddPackage( + NAME testpack-adder + DOWNLOAD_COMMAND git clone --depth 1 --branch v1.0.0 https://github.com/cpm-cmake/testpack-adder.git ${DOWNLOAD_DIR} + OPTIONS "ADDER_BUILD_TESTS OFF" "ADDER_BUILD_EXAMPLES OFF" + PATCHES + patches/001-test_patches_command.patch + patches/002-test_patches_command.patch + ) + PACK + + # configure with unpopulated cache + assert_success prj.configure + assert_success prj.build + end + +end