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