#!/bin/sh
## 
## Copyright 2018-2022, 2026 The Regents of the University of California
## All rights reserved.
## 
## This file is part of Spoofer.
## 
## Spoofer is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
## 
## Spoofer is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
## 
## You should have received a copy of the GNU General Public License
## along with Spoofer.  If not, see <http://www.gnu.org/licenses/>.
## 


usage() {
    cat <<EOF
usage: $0 <bundle> ...

Finds unportable shared library references within app <bundle>, copies those
shared libraries to bundle's Frameworks, and modifies the binaries to refer to
the bundled copies.  Repeats this process recursively for each dependent
library.  (macdeployqt already does this for Qt libaries, but not necessarily
other libaries.)
EOF
}


debug() {
    # echo "$@"
    :
}

enqueue_newlib() {
    f="$1"
    if fgrep -q "$f" "$MDTMP/processed"; then
        debug "          already processed: $f"
    elif fgrep -q "$f" "$MDTMP/newlibs"; then
        debug "          already queued: $f"
    else
        debug "          append to newlibs: $f"
        echo "$f" >>"$MDTMP/newlibs"
    fi
}

dump_newlibs() {
    local f
    debug "        newlibs: $*"
    test -f "$MDTMP/newlibs" || return
    cat "$MDTMP/newlibs" | while IFS= read -r line; do
        # Note: pipe into `while` created a new variable scope
        debug "          $line"
    done
}

fixfile() {
    # dump_newlibs

    local bundle prefix f oldname dep exe_path loader_path rpath
    bundle=$(readlink -f "$1")
    prefix="$2"
    f=$(readlink -f "$3")
    case "$f" in
        *.framework)
            f="$f/$(basename $f .framework)"
            ;;
    esac
    if fgrep -q "$f" "$MDTMP/processed"; then
        debug "        already processed"
        return
    fi
    exe_path="$bundle/Contents/MacOS"
    loader_path=$(dirname $(readlink -f "$f"))
    debug "      $f"
    # For each dependency needed by $f, excluding ubiquitous external ones...
    $(dirname $0)/mac-list-dylibs "$f" | \
        egrep -v '^(/usr/lib/|/System/Library/)' | \
        while IFS= read -r dep
    do
        # Note: pipe into `while` created a new variable scope
        oldname="$dep"
        echo "        dep:  $dep"
        rpath=$(readlink -f "$loader_path/../../../")
        oldname=$(echo "$oldname" | sed -E -e "s;@rpath;$rpath;")
        debug "        oldname: $oldname"
        dep=$(echo "$dep" | sed -E -e "s;@rpath;$rpath;" -e "s;@executable_path;$exe_path;" -e "s;@loader_path;$loader_path;")
        debug "        dep@: $dep"
        dep=$(readlink -f "$(dirname $dep)")/$(basename $dep)
        debug "        depL: $dep"
        base=$(basename "$dep")
        case "$dep" in
            /System/Library/*|/usr/lib/*)
                debug "          skip standard lib"
                ;;
            *.dylib)
                debug "          dylib"
                newname="$prefix/"$(perl -le 'use File::Spec; print File::Spec->abs2rel(@ARGV)' "$dep" "$loader_path")
                newfile="$bundle/Contents/Frameworks/$base"
                echo "$newfile" >>"$MDTMP/keep"
                enqueue_newlib "$newfile"
                # dump_newlibs
                if test -f "$newfile"; then
                    debug "          already copied"
                else
                    echo "          copy to: $newfile"
                    cp "$dep" "$newfile"
                    if [ "$newname" = "$oldname" ]; then
                        debug "          already changed"
                    else
                        echo "          change to: $newname"
                        install_name_tool -change "$dep" "$newname" "$f"
                    fi
                fi
                ;;
            $bundle/*.framework/*)
                debug "          bundled framework"
                fw=$(echo $dep | sed -E -e 's/.framework.*/.framework/')
                echo "$fw" >>"$MDTMP/keep"
                enqueue_newlib "$fw"
                # dump_newlibs
                ;;
            *.framework/*)
                echo "ERROR: framework outside of bundle" >&2
                exit 1
                ;;
            *)
                echo "ERROR: unexpected file" >&2
                exit 1
                ;;
        esac
    done
    debug "      processed: $f"
    echo "$f" >>"$MDTMP/processed"
}

trap -- 'echo "$0 exit status: $?"' ERR
set -e
if test "$#" = 0 || test "$1" = "-?" || test $1 = "--help"; then
    usage
    exit 0
fi

for bundle in "$@"; do
    echo "bundle: $bundle"
    test -d "$bundle/Contents/MacOS" || { echo >&2 "$bundle does not appear to be an app bundle."; exit 1; }

    MDTMP=.macdeploy
    mkdir "$MDTMP"

    :>"$MDTMP/processed"
    :>"$MDTMP/newlibs"
    :>"$MDTMP/keep"

    # fix binaries
    for f in "$bundle/Contents/MacOS/"*; do
	if file "$f" | egrep executable | egrep -v -q script; then
            echo "    binary: $f"
            # dump_newlibs "before binary fixfile"
	    fixfile "$bundle" "@executable_path/../Frameworks" "$f"
            dump_newlibs "after binary fixfile"
	fi
    done

    # dump_newlibs "after all binaries"

    # scan and fix libraries
    while test -s "$MDTMP/newlibs"; do
        echo "    ############################ iterate over newlibs"
	mv "$MDTMP/newlibs" "$MDTMP/currentlibs"
        :>"$MDTMP/newlibs"
        sort "$MDTMP/currentlibs" | while IFS= read -r f; do
            # Note: pipe into `while` created a new variable scope
            echo "    library: $f"
	    fixfile "$bundle" "@loader_path" "$f"
	done
        rm "$MDTMP/currentlibs"
        dump_newlibs "after lib fixfile"
    done

    sort <"$MDTMP/keep" | uniq | sed -E -e "s;^$(pwd)/;;" > "$MDTMP/keeplocal"
    debug "#### Keep:"
    cat "$MDTMP/keeplocal" | while IFS= read -r f; do
        # Note: pipe into `while` created a new variable scope
        debug "$f"
    done

    set $(du -s -k $bundle)
    oldsize=$1
    echo "#### Purge unreferenced libs:"
    /bin/ls -1d "$bundle/Contents/Frameworks/"* | \
        fgrep -v -f "$MDTMP/keeplocal" | \
        while IFS= read -r f
    do
        # Note: pipe into `while` created a new variable scope
        echo $f;
        rm -r $f;
    done
    set $(du -s -k $bundle)
    newsize=$1
    echo "Purged $[ $oldsize - $newsize ] / $oldsize kiB"

    echo "removing $MDTMP"; rm -r "$MDTMP"
done
echo "$0 done"
