1#------------------------------------------------------------------------------- 2# Copyright (c) 2022 Cypress Semiconductor Corporation (an Infineon company) 3# or an affiliate of Cypress Semiconductor Corporation. All rights reserved. 4# Copyright (c) 2023 Arm Limited. All rights reserved. 5# 6# SPDX-License-Identifier: BSD-3-Clause 7# 8#------------------------------------------------------------------------------- 9 10include(FetchContent) 11set(FETCHCONTENT_QUIET FALSE) 12 13find_package(Git) 14 15# This function applies patches if they are not applied yet. 16# It assumes that patches have not been applied if it's not possible to revert them. 17# 18# WORKING_DIRECTORY - working directory where patches should be applied. 19# PATCH_FILES - list of patches. Patches will be applied in alphabetical order. 20function(apply_patches WORKING_DIRECTORY PATCH_FILES) 21 # Validate if patches are already applied by reverting patches in reverse order 22 # Step 1 - keep changes in stash with random message/name to detect 23 # that stash has been created by git 24 string(RANDOM LENGTH 16 STASH_NAME) 25 set(STASH_NAME "tfm-remote_library-apply_patches-${STASH_NAME}") 26 execute_process(COMMAND "${GIT_EXECUTABLE}" stash push -u -m "${STASH_NAME}" 27 WORKING_DIRECTORY ${WORKING_DIRECTORY} 28 RESULT_VARIABLE VALIDATION_STATUS 29 ERROR_QUIET OUTPUT_QUIET 30 ) 31 # Step 2 - get list of stashes to validate that stash has been created 32 if (VALIDATION_STATUS EQUAL 0) 33 execute_process(COMMAND "${GIT_EXECUTABLE}" stash list 34 WORKING_DIRECTORY ${WORKING_DIRECTORY} 35 OUTPUT_VARIABLE STASH_LIST 36 RESULT_VARIABLE VALIDATION_STATUS 37 ERROR_QUIET 38 ) 39 # Look for stash message to detect stash creation 40 string(FIND "${STASH_LIST}" "${STASH_NAME}" STASH_INDEX) 41 if (STASH_INDEX LESS 0) 42 # Stash is not created, most probably because there is no changes 43 set(VALIDATION_STATUS 0) 44 else() 45 # Step 3 - restore changes with git stash apply 46 if (VALIDATION_STATUS EQUAL 0) 47 execute_process(COMMAND "${GIT_EXECUTABLE}" stash apply 48 WORKING_DIRECTORY ${WORKING_DIRECTORY} 49 RESULT_VARIABLE VALIDATION_STATUS 50 ERROR_QUIET OUTPUT_QUIET 51 ) 52 endif() 53 endif() 54 endif() 55 # Step 4 - revert patches in reverse order 56 if (VALIDATION_STATUS EQUAL 0) 57 # Sort list of patches in descending order for validation 58 list(SORT PATCH_FILES ORDER DESCENDING) 59 foreach(PATCH ${PATCH_FILES}) 60 execute_process(COMMAND "${GIT_EXECUTABLE}" apply --reverse --verbose "${PATCH}" 61 WORKING_DIRECTORY ${WORKING_DIRECTORY} 62 RESULT_VARIABLE VALIDATION_STATUS 63 ERROR_QUIET OUTPUT_QUIET 64 ) 65 if (NOT VALIDATION_STATUS EQUAL 0) 66 # patch failed to be applied, assume that we need to restore and 67 # apply all patch set 68 break() 69 endif() 70 endforeach() 71 endif() 72 # Step 5 - pop stash to restore original state 73 if (STASH_INDEX GREATER_EQUAL 0) 74 # Clear index before restore 75 execute_process(COMMAND "${GIT_EXECUTABLE}" clean -df 76 WORKING_DIRECTORY ${WORKING_DIRECTORY} 77 ERROR_QUIET OUTPUT_QUIET 78 ) 79 execute_process(COMMAND "${GIT_EXECUTABLE}" reset --hard 80 WORKING_DIRECTORY ${WORKING_DIRECTORY} 81 ERROR_QUIET OUTPUT_QUIET 82 ) 83 execute_process(COMMAND "${GIT_EXECUTABLE}" stash pop --index 84 WORKING_DIRECTORY ${WORKING_DIRECTORY} 85 ERROR_QUIET OUTPUT_QUIET 86 ) 87 else() 88 # There is no stash, restore commit by clearing index 89 execute_process(COMMAND "${GIT_EXECUTABLE}" clean -df 90 WORKING_DIRECTORY ${WORKING_DIRECTORY} 91 ERROR_QUIET OUTPUT_QUIET 92 ) 93 execute_process(COMMAND "${GIT_EXECUTABLE}" reset --hard 94 WORKING_DIRECTORY ${WORKING_DIRECTORY} 95 ERROR_QUIET OUTPUT_QUIET 96 ) 97 endif() 98 99 if (NOT VALIDATION_STATUS EQUAL 0) 100 # Validation has been failed, so we assume that patches should be applied 101 # Sort list of patches in ascending order 102 list(SORT PATCH_FILES ORDER ASCENDING) 103 104 set(EXECUTE_COMMAND "${GIT_EXECUTABLE}" apply --verbose ${PATCH_FILES}) 105 execute_process(COMMAND ${EXECUTE_COMMAND} 106 WORKING_DIRECTORY ${WORKING_DIRECTORY} 107 RESULT_VARIABLE PATCH_STATUS 108 COMMAND_ECHO STDOUT 109 ) 110 if (NOT PATCH_STATUS EQUAL 0) 111 message( FATAL_ERROR "Failed to apply patches at ${WORKING_DIRECTORY}" ) 112 endif() 113 endif() 114endfunction() 115 116 117# Returns a repository URL and a reference to the commit used to checkout the repository. 118# 119# REPO_URL_VAR - name of variable which receives repository URL. 120# TAG_VAR - name of variable which receives reference to commit. 121function(_get_fetch_remote_properties REPO_URL_VAR TAG_VAR) 122 # Parse arguments 123 set(options "") 124 set(oneValueArgs GIT_REPOSITORY GIT_TAG) 125 set(multiValueArgs "") 126 cmake_parse_arguments(PARSE_ARGV 2 ARG "${options}" "${oneValueArgs}" "${multiValueArgs}") 127 128 if (ARG_GIT_REPOSITORY) 129 set(${REPO_URL_VAR} ${ARG_GIT_REPOSITORY} PARENT_SCOPE) 130 set(${TAG_VAR} ${ARG_GIT_TAG} PARENT_SCOPE) 131 endif() 132endfunction() 133 134 135# This function helps to handle options with an empty string values. 136# There is a feature/bug in CMake that result in problem with the empty string arguments. 137# See https://gitlab.kitware.com/cmake/cmake/-/issues/16341 for details 138# 139# Arguments: 140# [in] KEY - option name 141# [out] KEY_VAR - name of variable that is set to ${KEY} on exit if value is not 142# an empty string otherwise to the empty string. 143# [out] VALUE_VAR - name of variable that is set to option value for ${KEY}. 144# [in/out] ARG_LIST_VAR - name of variable that holds list of key/value pairs - arguments. 145# Function looks for key/value pair specified by ${KEY} variable in 146# this list. Function removes key/value pair specified by ${KEY} on 147# exit. 148# 149# Example #1: 150# # We have following key/options: 151# # GIT_SUBMODULES "" 152# # BOO "abc" 153# # HEY "hi" 154# set(ARGS GIT_SUBMODULES "" BOO "abc" HEY "hi") 155# # Extract key/value for option "GIT_SUBMODULES" 156# extract_key_value(GIT_SUBMODULES GIT_SUBMODULES_VAR GIT_SUBMODULES_VALUE_VAR ARGS) 157# # ${GIT_SUBMODULES_VAR} is equal to "" 158# # ${GIT_SUBMODULES_VALUE_VAR} is equal to "" 159# 160# Example #2: 161# # We have following key/options: 162# # GIT_SUBMODULES "name" 163# # BOO "abc" 164# # HEY "hi" 165# set(ARGS GIT_SUBMODULES "name" BOO "abc" HEY "hi") 166# # Extract key/value for option "GIT_SUBMODULES" 167# extract_key_value(GIT_SUBMODULES GIT_SUBMODULES_VAR GIT_SUBMODULES_VALUE_VAR ARGS) 168# # ${GIT_SUBMODULES_VAR} is equal to "GIT_SUBMODULES" 169# # ${GIT_SUBMODULES_VALUE_VAR} is equal to "name" 170function(extract_key_value KEY KEY_VAR VALUE_VAR ARG_LIST_VAR) 171 list(FIND ${ARG_LIST_VAR} ${KEY} KEY_INDEX) 172 if(${KEY_INDEX} GREATER_EQUAL 0) 173 # Variable has been set, remove KEY 174 list(REMOVE_AT ${ARG_LIST_VAR} ${KEY_INDEX}) 175 176 # Validate that there is an option value in the list of arguments 177 list(LENGTH ${ARG_LIST_VAR} ARG_LIST_LENGTH) 178 if(${KEY_INDEX} GREATER_EQUAL ${ARG_LIST_LENGTH}) 179 message(FATAL_ERROR "Missing option value for ${KEY}") 180 endif() 181 182 # Get value 183 list(GET ${ARG_LIST_VAR} ${KEY_INDEX} VALUE) 184 185 # Remove value in the list 186 list(REMOVE_AT ${ARG_LIST_VAR} ${KEY_INDEX}) 187 188 # Update argument list 189 set(${ARG_LIST_VAR} ${${ARG_LIST_VAR}} PARENT_SCOPE) 190 191 # Set KEY_VAR & VALUE_VAR 192 set(${KEY_VAR} ${KEY} PARENT_SCOPE) 193 set(${VALUE_VAR} ${VALUE} PARENT_SCOPE) 194 else() 195 # Variable is not defined, set KEY_VAR & VALUE_VAR to empty strings 196 set(${KEY_VAR} "" PARENT_SCOPE) 197 set(${VALUE_VAR} "" PARENT_SCOPE) 198 endif() 199endfunction() 200 201 202# This function allows to fetch library from a remote repository or use a local 203# library copy. 204# 205# You can specify location of directory with patches. Patches are applied in 206# alphabetical order. 207# 208# Arguments: 209# [in] LIB_NAME <name> - library name 210# [in/out] LIB_SOURCE_PATH_VAR <var> - name of variable which holds path to library source 211# or "DOWNLOAD" if sources should be fetched from the remote repository. This 212# variable is updated in case if library is downloaded. It will point 213# to the path where FetchContent_Populate will locate local library copy. 214# [out] LIB_BINARY_PATH_VAR <var> - optional name of variable which is updated to 215# directory intended for use as a corresponding build directory if 216# library is fetched from the remote repository. 217# [in] LIB_BASE_DIR <path> - is used to set FETCHCONTENT_BASE_DIR. 218# [in] LIB_PATCH_DIR <path> - optional path to local folder which contains patches 219# that should be applied. 220# [in] LIB_FORCE_PATCH - optional argument to force applying patches when the path 221# is a local folder instead of fetching from the remote repository. 222# [in] GIT_REPOSITORY, GIT_TAG, ... - see https://cmake.org/cmake/help/latest/module/ExternalProject.html 223# for more details 224# 225# This function set CMP0097 to NEW if CMAKE_VERSION is greater or equal than 3.18.0. 226# Because of https://gitlab.kitware.com/cmake/cmake/-/issues/20579 CMP0097 is 227# non-functional until cmake 3.18.0. 228# See https://cmake.org/cmake/help/latest/policy/CMP0097.html for more info. 229function(fetch_remote_library) 230 # Parse arguments 231 set(options "") 232 set(oneValueArgs LIB_NAME LIB_SOURCE_PATH_VAR LIB_BINARY_PATH_VAR LIB_BASE_DIR LIB_PATCH_DIR LIB_FORCE_PATCH) 233 set(multiValueArgs FETCH_CONTENT_ARGS) 234 cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${oneValueArgs}" "${multiValueArgs}") 235 236 if(ARG_LIB_BASE_DIR) 237 set(FETCHCONTENT_BASE_DIR "${ARG_LIB_BASE_DIR}") 238 endif() 239 240 # Set to not download submodules if that option is available 241 if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.18.0") 242 cmake_policy(SET CMP0097 NEW) 243 endif() 244 245 if ("${${ARG_LIB_SOURCE_PATH_VAR}}" STREQUAL "DOWNLOAD") 246 set(SOURCE_PATH_IS_DOWNLOAD TRUE) 247 # Process arguments which can be an empty string 248 # There is a feature/bug in CMake that result in problem with empty string arguments 249 # See https://gitlab.kitware.com/cmake/cmake/-/issues/16341 for details 250 extract_key_value(GIT_SUBMODULES GIT_SUBMODULES GIT_SUBMODULES_VALUE ARG_FETCH_CONTENT_ARGS) 251 252 # Validate that there is no empty arguments to FetchContent_Declare 253 LIST(FIND ARG_FETCH_CONTENT_ARGS "" EMPTY_VALUE_INDEX) 254 if(${EMPTY_VALUE_INDEX} GREATER_EQUAL 0) 255 # There is an unsupported empty string argument, FATAL ERROR! 256 math(EXPR EMPTY_KEY_INDEX "${EMPTY_VALUE_INDEX} - 1") 257 list(GET ARG_FETCH_CONTENT_ARGS ${EMPTY_KEY_INDEX} EMPTY_KEY) 258 # TODO: Use extract_key_value if you have argument with empty value (see GIT_SUBMODULES above) 259 message(FATAL_ERROR "fetch_remote_library: Unexpected empty string value for ${EMPTY_KEY}. " 260 "Please, validate arguments or update fetch_remote_library to support empty value for ${EMPTY_KEY}!!!") 261 endif() 262 263 # Content fetching 264 FetchContent_Declare(${ARG_LIB_NAME} 265 ${ARG_FETCH_CONTENT_ARGS} 266 "${GIT_SUBMODULES}" "${GIT_SUBMODULES_VALUE}" 267 ) 268 269 FetchContent_GetProperties(${ARG_LIB_NAME}) 270 if(NOT ${ARG_LIB_NAME}_POPULATED) 271 FetchContent_Populate(${ARG_LIB_NAME}) 272 273 # Get remote properties 274 _get_fetch_remote_properties(REPO_URL_VAR TAG_VAR ${ARG_FETCH_CONTENT_ARGS}) 275 set(${ARG_LIB_SOURCE_PATH_VAR} ${${ARG_LIB_NAME}_SOURCE_DIR} CACHE PATH "Library has been downloaded from ${REPO_URL_VAR}, tag ${TAG_VAR}" FORCE) 276 if (DEFINED ARG_LIB_BINARY_PATH_VAR) 277 set(${ARG_LIB_BINARY_PATH_VAR} ${${ARG_LIB_NAME}_BINARY_DIR} CACHE PATH "Path to build directory of ${ARG_LIB_NAME}") 278 endif() 279 endif() 280 endif() 281 282 if (ARG_LIB_FORCE_PATCH) 283 set(FORCE_PATCH ${${ARG_LIB_FORCE_PATCH}}) 284 endif() 285 286 if (ARG_LIB_PATCH_DIR AND (SOURCE_PATH_IS_DOWNLOAD OR FORCE_PATCH)) 287 # look for patch files 288 file(GLOB PATCH_FILES "${ARG_LIB_PATCH_DIR}/*.patch") 289 290 if(PATCH_FILES) 291 # Apply patches for existing sources 292 apply_patches("${${ARG_LIB_SOURCE_PATH_VAR}}" "${PATCH_FILES}") 293 endif() 294 endif() 295endfunction() 296