Skip to content

Instantly share code, notes, and snippets.

@multiplemonomials
Created September 3, 2018 23:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save multiplemonomials/35b4722b1caae14be39c5f1e291a8aee to your computer and use it in GitHub Desktop.
Save multiplemonomials/35b4722b1caae14be39c5f1e291a8aee to your computer and use it in GitHub Desktop.
BundleOSXDependencies
# Script run at install-time to:
# * locate dependencies of executables and libraries
# * copy them to the install directory
# * fix the install_name of the depender to point to the dependency on the RPATH
# * add a new RPATH entry on the dependency for its new location and remove its old install name
# * repeat above for dependencies of the dependency, and all other dependencies
# arguments:
# PACKAGE_PREFIX -- root of a UNIX-structure package to operate on
# CMAKE_SHARED_LIBRARY_SUFFIX -- pass this variable in from your CMake script
# CMAKE_EXECUTABLE_SUFFIX -- pass this variable in from your CMake script
# EXTRA_RPATH_SEARCH_DIRS -- list of extra directories to search in when trying to resolve @rpath references
# PREFIX_RELATIVE_PYTHONPATH -- pass this variable in from your CMake script to indicate
# where relative to PACKAGE_PREFIX your python packages are located. Empty string means
# no python packages
# notes:
# * assumes that ${PACKAGE_PREFIX}/lib is, and should be, the rpath for all internal and external libraries
# * does not handle @executable_path since Amber doesn't use it; only handles @rpath and @loader_path
# This script was inspired by Hai Nguyen's similar script at https://github.com/Amber-MD/ambertools-binary-build/blob/master/conda_tools/update_gfortran_libs_osx.py
include(GetPrerequisites)
include(${CMAKE_CURRENT_LIST_DIR}/../Shorthand.cmake)
# Returns true iff the given dependency library should be ignored and not copied to the prefix
function(should_ignore_dep_library LIB_PATH OUTPUT_VARIABLE)
if("${LIB_PATH}" MATCHES "\\.framework")
set(${OUTPUT_VARIABLE} 1 PARENT_SCOPE)
elseif("${LIB_PATH}" MATCHES "libSystem")
set(${OUTPUT_VARIABLE} 1 PARENT_SCOPE)
else()
set(${OUTPUT_VARIABLE} 0 PARENT_SCOPE)
endif()
endfunction(should_ignore_dep_library)
# Makes sure that the library named by LIB_PATH has the given RPATH location
function(add_rpath LIB_PATH RPATH)
message(STATUS ">>>> Adding RPATH of \"${RPATH}\" to ${LIB_PATH}")
execute_process(COMMAND install_name_tool
-add_rpath ${RPATH} ${LIB_PATH}
ERROR_VARIABLE INT_ERROR_OUTPUT
RESULT_VARIABLE INT_RESULT_CODE)
# uhhh, I really hope the user has their language set to English...
if("${INT_ERROR_OUTPUT}" MATCHES "would duplicate path")
# do nothing, it already exists which is OK
elseif(NOT ${INT_RESULT_CODE} EQUAL 0)
message("!! Failed to execute install_name_tool! Error message was: ${INT_ERROR_OUTPUT}")
endif()
endfunction(add_rpath)
# Changes the install_name that the given library refers to a dependency by.
# Does nothing if OLD_INSTNAME is not valid.
function(change_dependency_instname LIB_PATH OLD_INSTNAME NEW_INSTNAME)
message(STATUS ">>>> Changing dependency reference \"${OLD_INSTNAME}\" in ${LIB_PATH} to \"${NEW_INSTNAME}\"")
execute_process(COMMAND install_name_tool
-change ${OLD_INSTNAME} ${NEW_INSTNAME} ${LIB_PATH}
ERROR_VARIABLE INT_ERROR_OUTPUT
RESULT_VARIABLE INT_RESULT_CODE)
# uhhh, I really hope the user has their language set to English...
if(NOT ${INT_RESULT_CODE} EQUAL 0)
message("!! Failed to execute install_name_tool! Error message was: ${INT_ERROR_OUTPUT}")
endif()
endfunction(change_dependency_instname)
# Sets the install name (the name that other libraries save at link time, and use at runtime to find the library) of the given library to INSTALL_NAME
function(set_install_name LIB_PATH INSTALL_NAME)
message(STATUS ">> Setting install name of ${LIB_PATH} to \"${INSTALL_NAME}\"")
execute_process(COMMAND install_name_tool
-id ${INSTALL_NAME} ${LIB_PATH}
ERROR_VARIABLE INT_ERROR_OUTPUT
RESULT_VARIABLE INT_RESULT_CODE)
if(NOT ${INT_RESULT_CODE} EQUAL 0)
message(STATUS "!! Failed to execute install_name_tool! Error message was: ${INT_ERROR_OUTPUT}")
endif()
endfunction(set_install_name)
# uses the file command to determine if a file is an executable or a shared library
function(is_executable_or_library OUTPUT_VARIABLE FILE_PATH)
execute_process(COMMAND file ${FILE_PATH}
ERROR_VARIABLE FILE_CMD_ERROR_OUTPUT
OUTPUT_VARIABLE FILE_CMD_OUTPUT
RESULT_VARIABLE FILE_CMD_RESULT_CODE)
if(NOT ${FILE_CMD_RESULT_CODE} EQUAL 0)
message(STATUS "!! Failed to execute file! Error message was: ${FILE_CMD_ERROR_OUTPUT}")
set(${OUTPUT_VARIABLE} FALSE PARENT_SCOPE)
return()
endif()
if("${FILE_PATH}" MATCHES "\\.dSYM")
# ignore debugging symbol libraries
set(${OUTPUT_VARIABLE} FALSE PARENT_SCOPE)
elseif("${FILE_CMD_OUTPUT}" MATCHES "Mach-O universal binary" OR "${FILE_CMD_OUTPUT}" MATCHES "Mach-O .+ executable")
#executables
set(${OUTPUT_VARIABLE} TRUE PARENT_SCOPE)
elseif("${FILE_CMD_OUTPUT}" MATCHES "Mach-O .+ dynamically linked shared library" OR "${FILE_CMD_OUTPUT}" MATCHES "Mach-O .+ bundle")
#shared libraries
set(${OUTPUT_VARIABLE} TRUE PARENT_SCOPE)
else()
#everything else
set(${OUTPUT_VARIABLE} FALSE PARENT_SCOPE)
endif()
endfunction(is_executable_or_library)
message("Bundling OSX dependencies for package rooted at: ${PACKAGE_PREFIX}")
file(GLOB PACKAGE_LIBRARIES "${PACKAGE_PREFIX}/lib/*${CMAKE_SHARED_LIBRARY_SUFFIX}")
file(GLOB PACKAGE_EXECUTABLES "${PACKAGE_PREFIX}/bin/*")
if(NOT "${PREFIX_RELATIVE_PYTHONPATH}" STREQUAL "")
# note: on OS X, python extension modules use ".so", not ".dylib"
file(GLOB_RECURSE PYTHON_EXTENSION_MODULES "${PACKAGE_PREFIX}${PREFIX_RELATIVE_PYTHONPATH}/*.so")
else()
set(PYTHON_EXTENSION_MODULES "")
endif()
# items are taken from, and added to, this stack.
# All files in this list are already in the installation prefix, and already have correct RPATHs
set(ITEMS_TO_PROCESS ${PACKAGE_LIBRARIES} ${PACKAGE_EXECUTABLES} ${PYTHON_EXTENSION_MODULES})
# lists of completed items (can skip if we see a dependency on these)
# This always contains the path inside the prefix
set(PROCESSED_ITEMS_BY_NEW_PATH "")
# List of external libraries which have already been copied to the prefix (by their external paths)
set(COPIED_EXTERNAL_DEPENDENCIES "")
# List that matches each index in the above list with the new path of the library
set(COPIED_EXTERNAL_DEPS_NEW_PATHS "")
if(NOT DEFINED EXTRA_RPATH_SEARCH_DIRS)
set(EXTRA_RPATH_SEARCH_DIRS "")
endif()
# always use the prefix lib folder as the first RPATH search dir
set(RPATH_SEARCH_DIRS "${PACKAGE_PREFIX}/lib" ${EXTRA_RPATH_SEARCH_DIRS})
while(1)
list(LENGTH ITEMS_TO_PROCESS NUM_ITEMS_LEFT)
if(${NUM_ITEMS_LEFT} EQUAL 0)
break()
endif()
list(GET ITEMS_TO_PROCESS 0 CURRENT_ITEM)
message(STATUS "Considering ${CURRENT_ITEM}")
is_executable_or_library(IS_EXEC_OR_LIB "${CURRENT_ITEM}")
if(IS_EXEC_OR_LIB)
set(CURRENT_ITEM_PREREQUISITES "")
get_prerequisites(${CURRENT_ITEM} CURRENT_ITEM_PREREQUISITES 0 0 "" ${PACKAGE_PREFIX}/lib ${PACKAGE_PREFIX}/lib)
foreach(PREREQUISITE_LIB_REFERENCE ${CURRENT_ITEM_PREREQUISITES})
should_ignore_dep_library(${PREREQUISITE_LIB_REFERENCE} SHOULD_IGNORE_PREREQUISITE)
if(SHOULD_IGNORE_PREREQUISITE)
message(STATUS ">> Ignoring dependency: ${PREREQUISITE_LIB_REFERENCE}")
else()
# resolve RPATH references
if("${PREREQUISITE_LIB_REFERENCE}" MATCHES "^@rpath")
string(REPLACE "@rpath" "" RPATH_SUFFIX_PATH "${PREREQUISITE_LIB_REFERENCE}")
# find the first folder in our RPATH search dirs that contains the library
set(PREREQUISITE_LIB "")
foreach(SEARCH_DIR ${RPATH_SEARCH_DIRS})
#message("Checking ${SEARCH_DIR}${RPATH_SUFFIX_PATH}")
if(EXISTS "${SEARCH_DIR}${RPATH_SUFFIX_PATH}")
set(PREREQUISITE_LIB ${SEARCH_DIR}${RPATH_SUFFIX_PATH})
endif()
endforeach()
else()
set(PREREQUISITE_LIB ${PREREQUISITE_LIB_REFERENCE})
endif()
if(NOT EXISTS "${PREREQUISITE_LIB}")
message("!! Unable to resolve library dependency ${PREREQUISITE_LIB_REFERENCE} -- skipping")
else()
# check if we already know about this library, and copy it here if we don't
list(FIND COPIED_EXTERNAL_DEPENDENCIES "${PREREQUISITE_LIB}" INDEX_IN_COPIED_DEPS)
list(FIND PACKAGE_LIBRARIES "${PREREQUISITE_LIB}" INDEX_IN_PACKAGE_LIBRARIES)
if(NOT INDEX_IN_COPIED_DEPS EQUAL -1)
message(STATUS ">> Already copied dependency: ${PREREQUISITE_LIB}")
list(GET COPIED_EXTERNAL_DEPS_NEW_PATHS ${INDEX_IN_COPIED_DEPS} PREREQ_LIB_REALPATH)
elseif(NOT INDEX_IN_PACKAGE_LIBRARIES EQUAL -1)
message(STATUS ">> Dependency is internal: ${PREREQUISITE_LIB}")
set(PREREQ_LIB_REALPATH ${PREREQUISITE_LIB})
else()
# previously unseen library -- copy to the prefix and queue for processing
message(STATUS ">> Copy library dependency: ${PREREQUISITE_LIB}")
# resolve symlinks
get_filename_component(PREREQ_LIB_REALPATH ${PREREQUISITE_LIB} REALPATH)
file(COPY "${PREREQ_LIB_REALPATH}" DESTINATION ${PACKAGE_PREFIX}/lib FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ WORLD_READ)
# find new filename
get_filename_component(PREREQ_LIB_FILENAME "${PREREQ_LIB_REALPATH}" NAME)
set(NEW_PREREQ_PATH "${PACKAGE_PREFIX}/lib/${PREREQ_LIB_FILENAME}")
# add correct RPATH
add_rpath(${NEW_PREREQ_PATH} "@loader_path/../lib")
list(APPEND COPIED_EXTERNAL_DEPENDENCIES ${PREREQUISITE_LIB})
list(APPEND COPIED_EXTERNAL_DEPS_NEW_PATHS ${NEW_PREREQ_PATH})
list(APPEND ITEMS_TO_PROCESS ${NEW_PREREQ_PATH})
endif()
# now, update how CURRENT_ITEM refers to this prerequisite
get_filename_component(PREREQUISITE_FILENAME "${PREREQ_LIB_REALPATH}" NAME)
if(NOT "${PREREQUISITE_LIB_REFERENCE}" STREQUAL "@rpath/${PREREQUISITE_FILENAME}")
change_dependency_instname(${CURRENT_ITEM} ${PREREQUISITE_LIB_REFERENCE} "@rpath/${PREREQUISITE_FILENAME}")
endif()
endif()
endif()
endforeach()
if("${CURRENT_ITEM}" MATCHES "${CMAKE_SHARED_LIBRARY_SUFFIX}$")
# if it's a library, set its install name to refer to it on the RPATH (so anything can link to it as long as it uses the $AMBERHOME/lib RPATH)
get_filename_component(CURRENT_ITEM_FILENAME "${CURRENT_ITEM}" NAME)
set_install_name(${CURRENT_ITEM} "@rpath/${CURRENT_ITEM_FILENAME}")
endif()
list(APPEND PROCESSED_ITEMS_BY_NEW_PATH ${CURRENT_ITEM})
else(IS_EXEC_OR_LIB)
message(STATUS ">> Not an executable or shared library, skipping: ${CURRENT_ITEM}")
endif()
list(REMOVE_AT ITEMS_TO_PROCESS 0)
endwhile()
message(STATUS "Dependency bundling done!")
# Dummy subdirectory to run BundleOSXDependencies at install time.
# Place this file and BundleOSXDependencies.cmake into a subfolder, then add this subfolder as
# a subdirectory at the end of your top-level CMakeLists.txt (insuring it gets installed last).
# This file does nothing if CMAKE_SYSTEM_NAME is not Darwin
if(${CMAKE_SYSTEM_NAME} STREQUAL Darwin)
# Add extra RPATH directories here
set(EXTRA_RPATH_SEARCH_DIRS "")
# create an install rule to invoke BundleOSXDependencies.cmake
install(CODE "
message(STATUS \"Finding and bundling dependencies...\")
execute_process(COMMAND ${CMAKE_COMMAND}
\"-DPACKAGE_PREFIX=\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}\"
-DCMAKE_SHARED_LIBRARY_SUFFIX=${CMAKE_SHARED_LIBRARY_SUFFIX}
-DCMAKE_EXECUTABLE_SUFFIX=${CMAKE_EXECUTABLE_SUFFIX}
\"-DEXTRA_RPATH_SEARCH_DIRS=${EXTRA_RPATH_SEARCH_DIRS}\"
-P ${CMAKE_CURRENT_SOURCE_DIR}/BundleOSXDependencies.cmake)")
endif()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment