#!/bin/sh version=1.0 main() { # parse options list= while [ $# -gt 0 ]; do case "$1" in -h|--help|--usage) usage quit 0 ;; -v|--version) echo "third_party_libs_tool $version" quit 0 ;; -l|--list) list=1 shift ;; *) break ;; esac done if [ $# -ne 1 ]; then usage quit 1 fi mktmp app_bundle=$(echo "$1" | fully_resolve_links) case "$app_bundle" in *.app|*.APP) if [ ! -d "$app_bundle" ]; then usage quit 1 fi ;; *) usage quit 1 ;; esac set -- OLDIFS=$IFS IFS=' ' for file in $(find "$app_bundle/Contents/MacOS" -type f); do case "$file" in *.dylib) set -- "$@" "$file" ;; *) [ -x "$file" ] && set -- "$@" "$file" ;; esac done IFS=$OLDIFS frameworks="$app_bundle/Contents/Frameworks" mkdir -p "$frameworks" 2>/dev/null scan_libs "$@" | fully_resolve_links | sort -u | \ while read lib; do if [ -n "$list" ]; then echo "$lib" else cp -f "$lib" "$frameworks" 2>/dev/null fi done # fix dynamic link info in executables and just copied libs [ -z "$list" ] && relink_all "$@" quit 0 } usage() { cat <<'EOF' Usage: third_party_libs_tool [OPTION] BUNDLE.app Bundle third party dylibs into BUNDLE.app and fix up linkages. Binaries are searched for in BUNDLE.app/Contents/MacOS . The dylibs are copied into BUNDLE.app/Contents/Frameworks . -h, --help, --usage Show this help screen and exit. -v, --version Show version information and exit. -l, --list Only list dylibs used by binaries, do not copy or link. Examples: third_party_libs_tool ./MyApp.app # bundle and link ./MyApp.app third_party_libs_tool --list ./MyApp.app # list third party libs used by ./MyApp.app Project homepage and documentation: <http://github.com/rkitover/mac-third-party-libs-tool> EOF } mktmp() { tmp="/tmp/third_party_libs_tool_$$" mkdir "$tmp" || quit 1 chmod 700 "$tmp" 2>/dev/null trap "quit 1" PIPE HUP INT QUIT ILL TRAP KILL BUS TERM } quit() { [ -n "$tmp" ] && rm -rf "$tmp" 2>/dev/null exit ${1:-0} } scan_libs() { scratch_dir="$tmp/lib_scan" mkdir -p "$scratch_dir" lib_scan "$@" rm -rf "$scratch_dir" } lib_scan() { for bin in "$@"; do case "$bin" in *.dylib) ;; *) [ ! -x "$bin" ] && continue ;; esac # if binary is already processed, continue [ -d "$scratch_dir/$bin" ] && continue # otherwise mark it processed mkdir -p "$scratch_dir/$bin" set -- OLDIFS=$IFS IFS=' ' for lib in $(otool -L "$bin" 2>/dev/null \ | awk '/^([^ \t]|([ \t]*\/(System|usr\/lib)\/))/ { next } \ { sub("^[ \t]*", ""); sub("[ \t]*\\(.*\\)[ \t]*$", ""); print }'); do [ "$lib" = "$bin" ] && continue # check for libs already linked as @rpath/ which usually means /usr/local/lib/ case "$lib" in '@rpath/'*) lib='/usr/local/lib'"${lib#@rpath}" ;; esac echo "$lib" set -- "$@" "$lib" done IFS=$OLDIFS # recurse [ $# -ne 0 ] && lib_scan "$@" done } fully_resolve_links() { while read -r file; do # get initial part for non-absolute path, or blank for absolute path=${file%%/*} # and set $file to the rest file=${file#*/} OLDIFS=$IFS IFS='/' for part in $file; do [ ! -z "$part" ] && path=$(resolve_link "$path/$part") done IFS=$OLDIFS # remove 'foo/..' path parts while :; do case "$path" in */../*|*/..) path=$(echo "$path" | sed 's,//*[^/][^/]*//*\.\./*,/,g') ;; *) break ;; esac done # remove trailing /s while [ "$path" != "${path%/}" ]; do path=${path%/} done echo "$path" done } resolve_link() { file=$1 while [ -h "$file" ]; do ls0=$(ls -ld "$file") new_link=$(expr "$ls0" : '.* -> \(.*\)$') if expr "$new_link" : '/.*' > /dev/null; then file="$new_link" else file="${file%/*}"/"$new_link" fi done echo "$file" } relink_all() { for exe in "$@"; do # dylib search path for executable install_name_tool -add_rpath '@loader_path/../Frameworks' "$exe" OLDIFS=$IFS IFS=' ' set -- for lib in $(find "$app_bundle/Contents/Frameworks" -name '*.dylib'); do set -- "$@" "$lib" done IFS=$OLDIFS for lib in "$@"; do # make lib writable chmod u+w "$lib" # change id of lib install_name_tool -id "@rpath/${lib##*/}" "$lib" # set search path of lib install_name_tool -add_rpath '@loader_path/../Frameworks' "$lib" # relink executable and all other libs to this lib for target in "$exe" "$@"; do relink "$lib" "$target" done done done } relink() { lib=$1 target=$2 lib_basename=${lib##*/} lib_basename_unversioned_re=$(echo "$lib_basename" | sed 's/[0-9.-]*\.dylib$//; s/\./\\./g') # remove full path and version of lib in executable lib_link_path=$( otool -l "$target" 2>/dev/null | \ sed -n 's,^ *name \(.*/*'"$lib_basename_unversioned_re"'[0-9.-]*\.dylib\) (offset .*,\1,p' | \ head -1 ) [ -z "$lib_link_path" ] && return 0 # check that the shorter basename is the prefix of the longer basename # that is, the lib versions match lib1=${lib_basename%.dylib} lib2=${lib_link_path##*/} lib2=${lib2%.dylib} if [ "${#lib1}" -le "${#lib2}" ]; then shorter=$lib1 longer=$lib2 else shorter=$lib2 longer=$lib1 fi case "$longer" in "$shorter"*) # and if so, relink target to the lib install_name_tool -change "$lib_link_path" "@rpath/$lib_basename" "$target" ;; esac } # try with sudo in case it fails, # also suppress duplicate path errors install_name_tool() { out_file="$tmp/install_name_tool.out" if ! command install_name_tool "$@" >"$out_file" 2>&1; then if grep -Eq -i 'permission denied|bad file descriptor' "$out_file"; then if ! command sudo install_name_tool "$@" >"$out_file" 2>&1; then cat "$out_file" >&2 return 1 fi elif ! grep -Eq -i 'would duplicate path' "$out_file"; then cat "$out_file" >&2 return 1 fi fi return 0 } main "$@"