#!/bin/sh version=1.5 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 *.[aA][pP][pP]) 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" scan_libs "$@" | sort -u | \ while read lib; do if [ -n "$list" ]; then echo "$lib" else resolved=$(echo "$lib" | fully_resolve_links) resolved_basename=${resolved##*/} if [ ! -f "$frameworks/$resolved_basename" ]; then cp -f "$resolved" "$frameworks" 2>/dev/null fi if [ "$resolved" != "$lib" ]; then lib_basename=${lib##*/} if [ ! -f "$frameworks/$lib_basename" ]; then ln -s "$resolved_basename" "$frameworks/$lib_basename" fi fi 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 # Remove path parts for mark file bin_mark_file=$(echo "$bin" | sed 's,/,_,g') # if binary is already processed, continue [ -d "$scratch_dir/$bin_mark_file" ] && continue # otherwise mark it processed mkdir -p "$scratch_dir/$bin_mark_file" set -- OLDIFS=$IFS IFS=' ' for lib in $(otool -L "$bin" 2>/dev/null \ | sed -E '1d; s/^[[:space:]]*//; \,^(/System|/usr/lib),d; s/[[:space:]]+\([^()]+\)[[:space:]]*$//'); 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}" ;; '@loader_path/../../../../'*) lib='/usr/local/'"${lib#@loader_path/../../../../}" ;; '@loader_path/'*) lib='/usr/local/lib'"${lib#@loader_path}" ;; esac echo "$lib" set -- "$@" "$lib" done IFS=$OLDIFS # recurse [ $# -ne 0 ] && lib_scan "$@" done } fully_resolve_links() { while read -r file; do while [ -h "$file" ]; do file=$(readlink -f "$file") done echo "$file" done } lock() { mkdir -p "$lock_dir/$1" } unlock() { rm -rf "$lock_dir/$1" } wait_lock() { while [ -d "$lock_dir/$1" ]; do /bin/bash -c 'sleep 0.1' done } relink_all() { lock_dir="$tmp/locks" find "$app_bundle/Contents/Frameworks" -name '*.dylib' > "$tmp/libs" for exe in "$@"; do ( # dylib search path for executable wait_lock "$exe" lock "$exe" install_name_tool -add_rpath '@loader_path/../Frameworks' "$exe" unlock "$exe" OLDIFS=$IFS IFS=' ' set -- for lib in $(cat "$tmp/libs"); do set -- "$@" "$lib" done IFS=$OLDIFS for lib in "$@"; do wait_lock "$lib" lock "$lib" # 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" unlock "$lib" # relink executable and all other libs to this lib for target in "$exe" "$@"; do ( relink "$lib" "$target" ) & done wait done ) & done wait rm -rf "$tmp/libs" "$lock_dir" } 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 wait_lock "$lib" lock "$lib" install_name_tool -change "$lib_link_path" "@rpath/$lib_basename" "$target" unlock "$lib" ;; 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 "$@"; then return 1 fi elif ! grep -Eq -i 'would duplicate path' "$out_file"; then cat "$out_file" >&2 return 1 fi fi return 0 } main "$@"