aboutsummaryrefslogtreecommitdiff
path: root/vcpkg/scripts/cmake/z_vcpkg_fixup_rpath_macho.cmake
blob: c8a6269052ce82d1ffe8d1d4de0382bcb2715fca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
function(z_vcpkg_calculate_corrected_macho_rpath)
    cmake_parse_arguments(PARSE_ARGV 0 "arg"
      ""
      "MACHO_FILE_DIR;OUT_NEW_RPATH_VAR"
      "")

    if(DEFINED arg_UNPARSED_ARGUMENTS)
        message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION} was passed extra arguments: ${arg_UNPARSED_ARGUMENTS}")
    endif()

    set(current_prefix "${CURRENT_PACKAGES_DIR}")
    set(current_installed_prefix "${CURRENT_INSTALLED_DIR}")
    file(RELATIVE_PATH relative_from_packages "${CURRENT_PACKAGES_DIR}" "${arg_MACHO_FILE_DIR}")
    if("${relative_from_packages}/" MATCHES "^debug/" OR "${relative_from_packages}/" MATCHES "^(manual-)?tools/.*/debug/.*")
        set(current_prefix "${CURRENT_PACKAGES_DIR}/debug")
        set(current_installed_prefix "${CURRENT_INSTALLED_DIR}/debug")
    endif()

    # compute path relative to lib
    file(RELATIVE_PATH relative_to_lib "${arg_MACHO_FILE_DIR}" "${current_prefix}/lib")
    # remove trailing slash
    string(REGEX REPLACE "/+$" "" relative_to_lib "${relative_to_lib}")

    if(NOT relative_to_lib STREQUAL "")
        set(new_rpath "@loader_path/${relative_to_lib}")
    else()
        set(new_rpath "@loader_path")
    endif()

    set("${arg_OUT_NEW_RPATH_VAR}" "${new_rpath}" PARENT_SCOPE)
endfunction()

function(z_vcpkg_regex_escape)
    cmake_parse_arguments(PARSE_ARGV 0 "arg"
      ""
      "STRING;OUT_REGEX_ESCAPED_STRING_VAR"
      "")
  string(REGEX REPLACE "([][+.*()^])" "\\\\\\1" regex_escaped "${arg_STRING}")
  set("${arg_OUT_REGEX_ESCAPED_STRING_VAR}" "${regex_escaped}" PARENT_SCOPE)
endfunction()

function(z_vcpkg_fixup_macho_rpath_in_dir)
    # We need to iterate through everything because we
    # can't predict where a Mach-O file will be located
    file(GLOB root_entries LIST_DIRECTORIES TRUE "${CURRENT_PACKAGES_DIR}/*")

    # Skip some folders for better throughput
    list(APPEND folders_to_skip "include")
    list(JOIN folders_to_skip "|" folders_to_skip_regex)
    set(folders_to_skip_regex "^(${folders_to_skip_regex})$")

    find_program(
        install_name_tool_cmd
        NAMES install_name_tool
        DOC "Absolute path of install_name_tool cmd"
        REQUIRED
    )

    find_program(
        otool_cmd
        NAMES otool
        DOC "Absolute path of otool cmd"
        REQUIRED
    )

    find_program(
        file_cmd
        NAMES file
        DOC "Absolute path of file cmd"
        REQUIRED
      )

    foreach(folder IN LISTS root_entries)
        if(NOT IS_DIRECTORY "${folder}")
            continue()
        endif()

        get_filename_component(folder_name "${folder}" NAME)
        if(folder_name MATCHES "${folders_to_skip_regex}")
            continue()
        endif()

        file(GLOB_RECURSE macho_files LIST_DIRECTORIES FALSE "${folder}/*")
        list(FILTER macho_files EXCLUDE REGEX [[\.(cpp|cc|cxx|c|hpp|h|hh|hxx|inc|json|toml|yaml|man|m4|ac|am|in|log|txt|pyi?|pyc|pyx|pxd|pc|cmake|f77|f90|f03|fi|f|cu|mod|ini|whl|cat|csv|rst|md|npy|npz|template|build)$]])
        list(FILTER macho_files EXCLUDE REGEX "/(copyright|LICENSE|METADATA)$")

        foreach(macho_file IN LISTS macho_files)
            if(IS_SYMLINK "${macho_file}")
                continue()
            endif()

            # Determine if the file is a Mach-O executable or shared library
            execute_process(
                COMMAND "${file_cmd}" -b "${macho_file}"
                OUTPUT_VARIABLE file_output
                OUTPUT_STRIP_TRAILING_WHITESPACE
            )
            if(file_output MATCHES ".*Mach-O.*shared library.*")
                set(file_type "shared")
            elseif(file_output MATCHES ".*Mach-O.*executable.*")
                set(file_type "executable")
            else()
                debug_message("File `${macho_file}` reported as `${file_output}` is not a Mach-O file")
                continue()
            endif()

            list(APPEND macho_executables_and_shared_libs "${macho_file}")

            get_filename_component(macho_file_dir "${macho_file}" DIRECTORY)
            get_filename_component(macho_file_name "${macho_file}" NAME)

            z_vcpkg_calculate_corrected_macho_rpath(
                MACHO_FILE_DIR "${macho_file_dir}"
                OUT_NEW_RPATH_VAR new_rpath
            )

            if("${file_type}" STREQUAL "shared")
                # Set the install name for shared libraries
                execute_process(
                    COMMAND "${otool_cmd}" -D "${macho_file}"
                    OUTPUT_VARIABLE get_id_ov
                    RESULT_VARIABLE get_id_rv
                )
                if(NOT get_id_rv EQUAL 0)
                    message(FATAL_ERROR "Could not obtain install name id from '${macho_file}'")
                endif()
                set(macho_new_id "@rpath/${macho_file_name}")
                message(STATUS "Setting install name id of '${macho_file}' to '@rpath/${macho_file_name}'")
                execute_process(
                    COMMAND "${install_name_tool_cmd}" -id "${macho_new_id}" "${macho_file}"
                    OUTPUT_QUIET
                    ERROR_VARIABLE set_id_error
                    RESULT_VARIABLE set_id_exit_code
                )
                if(NOT "${set_id_error}" STREQUAL "" AND NOT set_id_exit_code EQUAL 0)
                    message(WARNING "Couldn't adjust install name of '${macho_file}': ${set_id_error}")
                    continue()
                endif()

                # otool -D <macho_file> typically returns lines like:

                # <macho_file>:
                # <id>

                # But also with ARM64 binaries, it can return:
                # <macho_file> (architecture arm64):
                # <id>

                # Either way we need to remove the first line and trim the trailing newline char.
                string(REGEX REPLACE "[^\n]+:\n" "" get_id_ov "${get_id_ov}")
                string(REGEX REPLACE "\n.*" "" get_id_ov "${get_id_ov}")
                list(APPEND adjusted_shared_lib_old_ids "${get_id_ov}")
                list(APPEND adjusted_shared_lib_new_ids "${macho_new_id}")
            endif()

            # List all existing rpaths
            execute_process(
                COMMAND "${otool_cmd}" -l "${macho_file}"
                OUTPUT_VARIABLE get_rpath_ov
                RESULT_VARIABLE get_rpath_rv
            )

            if(NOT get_rpath_rv EQUAL 0)
                message(FATAL_ERROR "Could not obtain rpath list from '${macho_file}'")
            endif()
            # Extract the LC_RPATH load commands and extract the paths
            string(REGEX REPLACE "[^\n]+cmd LC_RPATH\n[^\n]+\n[^\n]+path ([^\n]+) \\(offset[^\n]+\n" "rpath \\1\n" get_rpath_ov "${get_rpath_ov}")
            string(REGEX MATCHALL "rpath [^\n]+" get_rpath_ov "${get_rpath_ov}")
            string(REGEX REPLACE "rpath " "" rpath_list "${get_rpath_ov}")

            list(FIND rpath_list "${new_rpath}" has_new_rpath)
            if(NOT has_new_rpath EQUAL -1)
                list(REMOVE_AT rpath_list ${has_new_rpath})
                set(rpath_args)
            else()
                set(rpath_args -add_rpath "${new_rpath}")
            endif()
            foreach(rpath IN LISTS rpath_list)
                list(APPEND rpath_args "-delete_rpath" "${rpath}")
            endforeach()
            if(NOT rpath_args)
                continue()
            endif()

            # Set the new rpath
            execute_process(
                COMMAND "${install_name_tool_cmd}" ${rpath_args} "${macho_file}"
                OUTPUT_QUIET
                ERROR_VARIABLE set_rpath_error
                RESULT_VARIABLE set_rpath_exit_code
            )

            if(NOT "${set_rpath_error}" STREQUAL "" AND NOT set_rpath_exit_code EQUAL 0)
                message(WARNING "Couldn't adjust RPATH of '${macho_file}': ${set_rpath_error}")
                continue()
            endif()

            message(STATUS "Adjusted RPATH of '${macho_file}' to '${new_rpath}'")
        endforeach()
    endforeach()

    # Check for dependent libraries in executables and shared libraries that
    # need adjusting after id change
    list(LENGTH adjusted_shared_lib_old_ids last_adjusted_index)
    if(NOT last_adjusted_index EQUAL 0)
        math(EXPR last_adjusted_index "${last_adjusted_index} - 1")
        foreach(macho_file IN LISTS macho_executables_and_shared_libs)
            execute_process(
                COMMAND "${otool_cmd}" -L "${macho_file}"
                OUTPUT_VARIABLE get_deps_ov
                RESULT_VARIABLE get_deps_rv
            )
            if(NOT get_deps_rv EQUAL 0)
                message(FATAL_ERROR "Could not obtain dependencies list from '${macho_file}'")
            endif()
            # change adjusted_shared_lib_old_ids[i] -> adjusted_shared_lib_new_ids[i]
            foreach(i RANGE ${last_adjusted_index})
                list(GET adjusted_shared_lib_old_ids ${i} adjusted_old_id)
                z_vcpkg_regex_escape(
                    STRING "${adjusted_old_id}"
                    OUT_REGEX_ESCAPED_STRING_VAR regex
                )
                if(NOT get_deps_ov MATCHES "[ \t]${regex} ")
                    continue()
                endif()
                list(GET adjusted_shared_lib_new_ids ${i} adjusted_new_id)

                # Replace the old id with the new id
                execute_process(
                    COMMAND "${install_name_tool_cmd}" -change "${adjusted_old_id}" "${adjusted_new_id}" "${macho_file}"
                    OUTPUT_QUIET
                    ERROR_VARIABLE change_id_error
                    RESULT_VARIABLE change_id_exit_code
                )
                if(NOT "${change_id_error}" STREQUAL "" AND NOT change_id_exit_code EQUAL 0)
                    message(WARNING "Couldn't adjust dependent shared library install name in '${macho_file}': ${change_id_error}")
                    continue()
                endif()
                message(STATUS "Adjusted dependent shared library install name in '${macho_file}' (From '${adjusted_old_id}' -> To '${adjusted_new_id}')")
            endforeach()
        endforeach()
    endif()
endfunction()