1# SPDX-FileCopyrightText: 2016 Gleb Popov <6yearold@gmail.com>
2#
3# SPDX-License-Identifier: BSD-3-Clause
4
5#[=======================================================================[.rst:
6ECMWinResolveSymlinks
7--------------------------
8
9Resolve pseudo-symlinks created by git when cloning on Windows.
10
11::
12
13  ecm_win_resolve_symlinks(<dir>)
14
15When git checks out a repository with UNIX symlinks on Windows machine,
16it creates a text file for each symlink, containing a relative path to the
17real file.
18This function would recursively walk over specified directory and replace
19pseudo-symlinks with corresponding real file's contents. It would then run
20git update-index --assume-unchanged on them to trick git.
21
22This is useful for projects like "breeze-icons" that contain many identical
23icons implemented as symlinks.
24
25Since 5.28
26#]=======================================================================]
27
28function(ECM_WIN_RESOLVE_SYMLINKS _dir)
29  get_filename_component(dir ${_dir} ABSOLUTE)
30  find_program(GIT_EXECUTABLE git)
31  if(NOT GIT_EXECUTABLE)
32    message(SEND_ERROR "Git executable not found!")
33  endif()
34
35  message(STATUS "Resolving symlinks in ${dir}...")
36  _find_symlinks(${dir} symlinks)
37  _portioned_list(symlinks ${symlinks})
38  foreach(s IN LISTS symlinks)
39    string(REPLACE ":" ";" s ${s})
40    _assume_unchanged(NO ${dir} "${s}")
41    _checkout_symlinks(${dir} "${s}")
42    _resolve_symlinks(${dir} "${s}")
43    _assume_unchanged(YES ${dir} "${s}")
44  endforeach()
45  message(STATUS "Resolving symlinks in ${dir}... Done.")
46
47  # touch cache every build to force CMake to re-run these functions every time
48  if(NOT TARGET wrs_touch_cache)
49    add_custom_target(wrs_touch_cache ALL
50      COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_BINARY_DIR}/CMakeCache.txt
51    )
52  endif()
53endfunction()
54
55function(_assume_unchanged mode dir symlinks)
56  if(mode)
57    set(flag --assume-unchanged)
58  else()
59    set(flag --no-assume-unchanged)
60  endif()
61
62  execute_process(COMMAND ${GIT_EXECUTABLE} update-index ${flag} ${symlinks}
63    WORKING_DIRECTORY ${dir}
64  )
65endfunction()
66
67function(_find_symlinks dir outvar)
68  execute_process(COMMAND ${GIT_EXECUTABLE} ls-files -s
69    WORKING_DIRECTORY ${dir}
70    OUTPUT_VARIABLE out
71    OUTPUT_STRIP_TRAILING_WHITESPACE
72  )
73
74  set(${outvar})
75  if(out)
76    string(REPLACE "\n" ";" out ${out})
77
78    foreach(f IN LISTS out)
79      # 120000 0db97052931e18484b6705f3bc644484ef9403b0 0 <tab> icons-dark/actions/16/CVnamespace.svg
80      string(REPLACE "\t" ";" f "${f}")
81      string(REPLACE " " ";" f "${f}")
82      list(GET f 0 mode)
83      if(mode STREQUAL "120000")
84        list(GET f 3 symlink)
85        list(APPEND ${outvar} ${symlink})
86      endif()
87    endforeach()
88  endif()
89  set(${outvar} ${${outvar}} PARENT_SCOPE)
90endfunction()
91
92# In functions like _checkout_symlinks() the command line can become too lengthy for Windows.
93# So we partition it, but in a hacky way due to CMake doesn't have list of lists.
94function(_portioned_list outvar)
95  list(LENGTH ARGN arglen)
96
97  if(arglen EQUAL 0)
98    set(${outvar} "" PARENT_SCOPE)
99    return()
100  endif()
101
102  set(init)
103  set(tail)
104  math(EXPR range "${arglen} - 1")
105  foreach(i RANGE ${range})
106    list(GET ARGN ${i} v)
107    string(LENGTH "${init}" initlen)
108    string(LENGTH ${v} vlen)
109    math(EXPR sumlen "${initlen} + ${vlen}")
110    if(sumlen LESS 8192)
111      list(APPEND init ${v})
112    else()
113      list(APPEND tail ${v})
114    endif()
115  endforeach()
116
117  _portioned_list(tail_portioned ${tail})
118  string(REPLACE ";" ":" init "${init}") # Generally this is not safe, because filepath can contain ':' character. But not on Windows. Phew.
119  set(${outvar} ${init} ${tail_portioned} PARENT_SCOPE)
120endfunction()
121
122function(_checkout_symlinks dir symlinks)
123  execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${symlinks}
124    WORKING_DIRECTORY ${dir}
125  )
126endfunction()
127
128function(_resolve_symlinks dir symlinks)
129  foreach(s IN LISTS symlinks)
130    file(READ ${dir}/${s} sdata)
131    get_filename_component(sdir ${dir}/${s} DIRECTORY)
132    set(f "${sdir}/${sdata}")
133    file(READ ${f} fdata)
134    file(WRITE ${dir}/${s} ${fdata})
135  endforeach()
136endfunction()
137