#!/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: [32mthird_party_libs_tool [1;34m[[1;35mOPTION[1;34m][0m [1;35mBUNDLE.app[0m
Bundle third party dylibs into [1;35mBUNDLE.app[0m and fix up linkages.

Binaries are searched for in [1;35mBUNDLE.app[0m/Contents/MacOS .

The dylibs are copied into [1;35mBUNDLE.app[0m/Contents/Frameworks .

  [1m-h, --help, --usage[0m                Show this help screen and exit.
  [1m-v, --version[0m                      Show version information and exit.
  [1m-l, --list[0m                         Only list dylibs used by binaries, do not copy or link.

Examples:
  [32mthird_party_libs_tool [1;35m./MyApp.app[0m         # bundle and link [1;35m./MyApp.app[0m
  [32mthird_party_libs_tool [1m--list[0m [1;35m./MyApp.app[0m  # list third party libs used by [1;35m./MyApp.app[0m

Project homepage and documentation: <[1;34mhttp://github.com/rkitover/mac-third-party-libs-tool[0m>
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 "$@"
