diff --git a/scripts/cmake/z_vcpkg_fixup_rpath_macho.cmake b/scripts/cmake/z_vcpkg_fixup_rpath_macho.cmake new file mode 100644 index 0000000000..cd2bd78537 --- /dev/null +++ b/scripts/cmake/z_vcpkg_fixup_rpath_macho.cmake @@ -0,0 +1,159 @@ +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_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() + + 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 "${install_name_tool_cmd}" -id "@rpath/${macho_file_name}" "${macho_file}" + OUTPUT_QUIET + ERROR_VARIABLE set_id_error + ) + message(STATUS "Set install name id of '${macho_file}' (To '@rpath/${macho_file_name}')") + if(NOT "${set_id_error}" STREQUAL "") + message(WARNING "Couldn't adjust install name of '${macho_file}': ${set_id_error}") + continue() + endif() + endif() + + # Clear 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}") + + foreach(rpath IN LISTS rpath_list) + execute_process( + COMMAND "${install_name_tool_cmd}" -delete_rpath "${rpath}" "${macho_file}" + OUTPUT_QUIET + ERROR_VARIABLE delete_rpath_error + ) + message(STATUS "Remove RPATH from '${macho_file}' ('${rpath}')") + endforeach() + + # Set the new rpath + execute_process( + COMMAND "${install_name_tool_cmd}" -add_rpath "${new_rpath}" "${macho_file}" + OUTPUT_QUIET + ERROR_VARIABLE set_rpath_error + ) + + if(NOT "${set_rpath_error}" STREQUAL "") + 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() +endfunction() diff --git a/scripts/ports.cmake b/scripts/ports.cmake index d4d341fd29..1602f03bbf 100644 --- a/scripts/ports.cmake +++ b/scripts/ports.cmake @@ -91,6 +91,7 @@ include("${SCRIPTS}/cmake/z_vcpkg_prettify_command_line.cmake") include("${SCRIPTS}/cmake/z_vcpkg_setup_pkgconfig_path.cmake") include("${SCRIPTS}/cmake/z_vcpkg_fixup_rpath.cmake") +include("${SCRIPTS}/cmake/z_vcpkg_fixup_rpath_macho.cmake") function(debug_message) if(PORT_DEBUG) @@ -190,10 +191,13 @@ target system or to the host system. Use a prefixed variable instead. include("${CURRENT_PORT_DIR}/portfile.cmake") if(DEFINED PORT) - # Always fixup RPATH on linux unless explicitly disabled. + # Always fixup RPATH on linux and osx unless explicitly disabled. if(VCPKG_FIXUP_ELF_RPATH OR (VCPKG_TARGET_IS_LINUX AND NOT DEFINED VCPKG_FIXUP_ELF_RPATH)) z_vcpkg_fixup_rpath_in_dir() endif() + if(VCPKG_FIXUP_MACHO_RPATH OR (VCPKG_TARGET_IS_OSX AND NOT DEFINED VCPKG_FIXUP_MACHO_RPATH)) + z_vcpkg_fixup_macho_rpath_in_dir() + endif() include("${SCRIPTS}/build_info.cmake") endif() elseif(CMD STREQUAL "CREATE") diff --git a/scripts/test_ports/rpath-test-binaries/portfile.cmake b/scripts/test_ports/rpath-test-binaries/portfile.cmake new file mode 100644 index 0000000000..a3e79919b6 --- /dev/null +++ b/scripts/test_ports/rpath-test-binaries/portfile.cmake @@ -0,0 +1,18 @@ +set(VCPKG_POLICY_EMPTY_INCLUDE_FOLDER enabled) +vcpkg_check_linkage(ONLY_DYNAMIC_LIBRARY) +vcpkg_cmake_configure( + SOURCE_PATH "${CURRENT_PORT_DIR}/project" + OPTIONS_RELEASE + -DTEST_STRING=release + OPTIONS_DEBUG + -DTEST_STRING=debug +) +vcpkg_cmake_install() +if(NOT VCPKG_BUILD_TYPE) + vcpkg_copy_tools(TOOL_NAMES rpath-test-tool + SEARCH_DIR "${CURRENT_PACKAGES_DIR}/debug/bin" + DESTINATION "${CURRENT_PACKAGES_DIR}/tools/${PORT}/debug" + ) +endif() +vcpkg_copy_tools(TOOL_NAMES rpath-test-tool AUTO_CLEAN) +file(WRITE "${CURRENT_PACKAGES_DIR}/share/${PORT}/copyright" "This test port is part of vcpkg.") diff --git a/scripts/test_ports/rpath-test-binaries/project/CMakeLists.txt b/scripts/test_ports/rpath-test-binaries/project/CMakeLists.txt new file mode 100644 index 0000000000..f1e5de703c --- /dev/null +++ b/scripts/test_ports/rpath-test-binaries/project/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.7) +project(rpath-test CXX) + +set(TEST_STRING "" CACHE STRING "") + +add_library(rpath-test-lib lib.cpp) +target_compile_definitions(rpath-test-lib PRIVATE "TEST_STRING=\"${TEST_STRING}\"") + +add_executable(rpath-test-tool main.cpp) +target_link_libraries(rpath-test-tool PRIVATE rpath-test-lib) + +install(TARGETS rpath-test-lib rpath-test-tool) diff --git a/scripts/test_ports/rpath-test-binaries/project/lib.cpp b/scripts/test_ports/rpath-test-binaries/project/lib.cpp new file mode 100644 index 0000000000..7853f1898d --- /dev/null +++ b/scripts/test_ports/rpath-test-binaries/project/lib.cpp @@ -0,0 +1,4 @@ +const char* getTestString() +{ + return TEST_STRING; +} diff --git a/scripts/test_ports/rpath-test-binaries/project/main.cpp b/scripts/test_ports/rpath-test-binaries/project/main.cpp new file mode 100644 index 0000000000..7253b5af7b --- /dev/null +++ b/scripts/test_ports/rpath-test-binaries/project/main.cpp @@ -0,0 +1,8 @@ +#include + +extern const char* getTestString(); + +int main() +{ + puts(getTestString()); +} diff --git a/scripts/test_ports/rpath-test-binaries/vcpkg.json b/scripts/test_ports/rpath-test-binaries/vcpkg.json new file mode 100644 index 0000000000..bf52bd723e --- /dev/null +++ b/scripts/test_ports/rpath-test-binaries/vcpkg.json @@ -0,0 +1,12 @@ +{ + "name": "rpath-test-binaries", + "version-string": "ci", + "description": "Provides installed binaries for rpath fixup test", + "supports": "native & !windows", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + } + ] +} diff --git a/scripts/test_ports/rpath-test/portfile.cmake b/scripts/test_ports/rpath-test/portfile.cmake new file mode 100644 index 0000000000..dd8c5107d3 --- /dev/null +++ b/scripts/test_ports/rpath-test/portfile.cmake @@ -0,0 +1,23 @@ +set(VCPKG_POLICY_EMPTY_PACKAGE enabled) + +vcpkg_execute_in_download_mode( + COMMAND "${CURRENT_INSTALLED_DIR}/tools/rpath-test-binaries/rpath-test-tool" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" + OUTPUT_VARIABLE output + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(NOT output STREQUAL "release") + message(SEND_ERROR "Actual: '${output}', expected: 'release'") +endif() + +if(NOT VCPKG_BUILD_TYPE) + vcpkg_execute_in_download_mode( + COMMAND "${CURRENT_INSTALLED_DIR}/tools/rpath-test-binaries/debug/rpath-test-tool" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" + OUTPUT_VARIABLE output + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT output STREQUAL "debug") + message(SEND_ERROR "Actual: '${output}', expected: 'debug'") + endif() +endif() diff --git a/scripts/test_ports/rpath-test/vcpkg.json b/scripts/test_ports/rpath-test/vcpkg.json new file mode 100644 index 0000000000..7136d0476c --- /dev/null +++ b/scripts/test_ports/rpath-test/vcpkg.json @@ -0,0 +1,8 @@ +{ + "name": "rpath-test", + "version-string": "ci", + "description": "Test rpath fixup", + "dependencies": [ + "rpath-test-binaries" + ] +} diff --git a/scripts/test_ports/unit-test-cmake/portfile.cmake b/scripts/test_ports/unit-test-cmake/portfile.cmake index 7129623b0a..d38a1fe030 100644 --- a/scripts/test_ports/unit-test-cmake/portfile.cmake +++ b/scripts/test_ports/unit-test-cmake/portfile.cmake @@ -190,6 +190,7 @@ if("fixup-pkgconfig" IN_LIST FEATURES) endif() if("fixup-rpath" IN_LIST FEATURES) include("${CMAKE_CURRENT_LIST_DIR}/test-z_vcpkg_calculate_corrected_rpath.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/test-z_vcpkg_calculate_corrected_rpath_macho.cmake") endif() if(Z_VCPKG_UNIT_TEST_HAS_ERROR) diff --git a/scripts/test_ports/unit-test-cmake/test-z_vcpkg_calculate_corrected_rpath_macho.cmake b/scripts/test_ports/unit-test-cmake/test-z_vcpkg_calculate_corrected_rpath_macho.cmake new file mode 100644 index 0000000000..ee4f42a2ea --- /dev/null +++ b/scripts/test_ports/unit-test-cmake/test-z_vcpkg_calculate_corrected_rpath_macho.cmake @@ -0,0 +1,56 @@ +# z_vcpkg_calculate_corrected_macho_rpath_macho(...) + +block(SCOPE_FOR VARIABLES) + +set(CURRENT_PACKAGES_DIR "/P") +set(CURRENT_INSTALLED_DIR "/I") + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/lib") +]] out [[@loader_path]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/plugins/group") +]] out [[@loader_path/../../lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/debug/lib") +]] out [[@loader_path]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/debug/plugins/group") +]] out [[@loader_path/../../lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/tools/port") +]] out [[@loader_path/../../lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/tools/port/bin") +]] out [[@loader_path/../../../lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/tools/port/debug") +]] out [[@loader_path/../../../debug/lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/tools/port/debug/bin") +]] out [[@loader_path/../../../../debug/lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/manual-tools/port") +]] out [[@loader_path/../../lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/manual-tools/port/bin") +]] out [[@loader_path/../../../lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/manual-tools/port/debug") +]] out [[@loader_path/../../../debug/lib]]) + +unit_test_check_variable_equal([[ + z_vcpkg_calculate_corrected_macho_rpath(OUT_NEW_RPATH_VAR "out" MACHO_FILE_DIR "/P/manual-tools/port/debug/bin") +]] out [[@loader_path/../../../../debug/lib]]) + +endblock() diff --git a/scripts/test_ports/vcpkg-fixup-macho-rpath/portfile.cmake b/scripts/test_ports/vcpkg-fixup-macho-rpath/portfile.cmake new file mode 100644 index 0000000000..6754431fad --- /dev/null +++ b/scripts/test_ports/vcpkg-fixup-macho-rpath/portfile.cmake @@ -0,0 +1,65 @@ +set(VCPKG_POLICY_EMPTY_PACKAGE enabled) + +# Test for empty string +set(macho_dir "${CURRENT_PACKAGES_DIR}/lib") +set(test_rpath "") +set(expected "@loader_path") + +z_vcpkg_calculate_corrected_macho_rpath( + MACHO_FILE_DIR "${macho_dir}" + OUT_NEW_RPATH_VAR new_rpath +) + +if(NOT "x${new_rpath}x" STREQUAL "x${expected}x") + message(FATAL_ERROR "--- Calculated rpath does not match expected rpath: '${new_rpath}' != '${expected}' ") +else() + message(STATUS "--- Calculated rpath matches expected rpath: '${new_rpath}' ") +endif() + +# Test for empty string in the tools directory +set(macho_dir "${CURRENT_PACKAGES_DIR}/tools/hdf5") +set(test_rpath "") +set(expected "@loader_path/../../lib") + +z_vcpkg_calculate_corrected_macho_rpath( + MACHO_FILE_DIR "${macho_dir}" + OUT_NEW_RPATH_VAR new_rpath +) + +if(NOT "x${new_rpath}x" STREQUAL "x${expected}x") + message(FATAL_ERROR "--- Calculated rpath does not match expected rpath: '${new_rpath}' != '${expected}' ") +else() + message(STATUS "--- Calculated rpath matches expected rpath: '${new_rpath}' ") +endif() + +# macho dir in subdir +set(macho_dir "${CURRENT_PACKAGES_DIR}/lib/somesubdir") +set(test_rpath "") +set(expected "@loader_path/..") + +z_vcpkg_calculate_corrected_macho_rpath( + MACHO_FILE_DIR "${macho_dir}" + OUT_NEW_RPATH_VAR new_rpath +) + +if(NOT "x${new_rpath}x" STREQUAL "x${expected}x") + message(FATAL_ERROR "--- Calculated rpath for '${macho_dir}' does not match expected rpath: '${new_rpath}' != '${expected}' ") +else() + message(STATUS "--- Calculated rpath matches expected rpath: '${new_rpath}' ") +endif() + +# Getting more complex +set(macho_dir "${CURRENT_PACKAGES_DIR}/plugins/notlib/extrasubdir") +set(test_rpath "") +set(expected "@loader_path/../../../lib") + +z_vcpkg_calculate_corrected_macho_rpath( + MACHO_FILE_DIR "${macho_dir}" + OUT_NEW_RPATH_VAR new_rpath +) + +if(NOT "x${new_rpath}x" STREQUAL "x${expected}x") + message(FATAL_ERROR "--- Calculated rpath does not match expected rpath: '${new_rpath}' != '${expected}' ") +else() + message(STATUS "--- Calculated rpath matches expected rpath: '${new_rpath}' ") +endif() diff --git a/scripts/test_ports/vcpkg-fixup-macho-rpath/vcpkg.json b/scripts/test_ports/vcpkg-fixup-macho-rpath/vcpkg.json new file mode 100644 index 0000000000..17d7c7675c --- /dev/null +++ b/scripts/test_ports/vcpkg-fixup-macho-rpath/vcpkg.json @@ -0,0 +1,6 @@ +{ + "name": "vcpkg-fixup-macho-rpath", + "version-date": "2024-06-15", + "description": "Test port to check the string replacement in z_vcpkg_fixup_macho_rpath", + "supports": "native & osx" +}