#!/usr/bin/env bash
# Make Binaries/colmap fully self-contained: copy every non-system dylib it
# (transitively) needs next to the binary, rewrite all load commands to
# @loader_path, strip absolute rpaths, and re-sign.
#
# Why: the shipped app runs on machines with no Homebrew and no
# /Users/<dev>/... checkout. Any absolute or @rpath dependency that leaks
# into the binary kills SfM with dyld "Library not loaded" (first seen:
# tester crash on libfaiss.dylib, 2026-07-10). Xcode Cloud also builds from
# the committed Binaries/, so the dylibs produced here are committed too.
#
# Idempotent — safe to re-run, and runs standalone on an already-built
# Binaries/colmap (no COLMAP rebuild needed). Called automatically at the
# end of Scripts/build_dependencies.sh.
#
# Signing: install_name_tool invalidates code signatures, and arm64 kills
# binaries with invalid signatures at exec — everything touched here gets an
# ad-hoc re-sign. The Xcode post-compile "Sign Bundled Binaries" phase
# replaces these with the real identity inside the app bundle (required:
# hardened-runtime library validation only loads dylibs signed with the
# same Team ID).
set -euo pipefail

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BINARIES_DIR="$PROJECT_ROOT/Binaries"
FAISS_PREFIX="$BINARIES_DIR/faiss-prefix"
COLMAP_BIN="$BINARIES_DIR/colmap"

[ -f "$COLMAP_BIN" ] || { echo "error: $COLMAP_BIN not found" >&2; exit 1; }

# Load-command paths that must not ship: Homebrew, /usr/local, dev checkout,
# and unresolved @rpath references.
BAD_REF_RE='^(/opt/homebrew|/usr/local|/Users/|@rpath/)'

# Print the dependency load commands of a Mach-O that need rewriting.
# (tail -n +2 skips the "file:" header; for dylibs the LC_ID_DYLIB line may
# remain — install_name_tool -change ignores it, and -id is set at copy time.)
bad_deps() {
    otool -L "$1" | tail -n +2 | awk '{print $1}' | grep -E "$BAD_REF_RE" || true
}

# Find a copyable source for a dependency referenced as @rpath/<name>.
resolve_rpath_dep() {
    local name="$1" c
    for c in "$BINARIES_DIR/$name" "$FAISS_PREFIX/lib/$name" \
             "/opt/homebrew/lib/$name" "/usr/local/lib/$name"; do
        [ -e "$c" ] && { echo "$c"; return 0; }
    done
    # depth 5 reaches nested layouts like gcc/lib/gcc/current/libgcc_s.1.1.dylib
    c="$(find -L /opt/homebrew/opt -maxdepth 5 -name "$name" -type f 2>/dev/null | head -1)" || true
    [ -n "$c" ] && { echo "$c"; return 0; }
    return 1
}

# Remove every absolute LC_RPATH (path may contain spaces — parse with sed).
strip_abs_rpaths() {
    local f="$1" rp
    while :; do
        rp="$(otool -l "$f" | grep -A2 'cmd LC_RPATH' | grep ' path /' \
              | sed 's/^ *path //; s/ (offset [0-9]*)$//' | head -1)" || true
        [ -n "$rp" ] || break
        install_name_tool -delete_rpath "$rp" "$f" 2>/dev/null
    done
}

echo "── Making colmap self-contained ──"

# Fixpoint loop: rewriting colmap's deps copies dylibs in; those dylibs have
# their own Homebrew deps (libceres → libglog, libOpenImageIO → libtiff, …),
# which copy more dylibs in. Repeat until a full pass changes nothing.
PASS=0
CHANGED=1
while [ "$CHANGED" = "1" ]; do
    CHANGED=0
    PASS=$((PASS + 1))
    for MACHO in "$COLMAP_BIN" "$BINARIES_DIR"/lib*.dylib; do
        [ -e "$MACHO" ] || continue
        while IFS= read -r DEP; do
            [ -n "$DEP" ] || continue
            NAME="$(basename "$DEP")"
            # Skip a dylib's own LC_ID_DYLIB line (not a load command).
            [ "$NAME" = "$(basename "$MACHO")" ] && continue
            if [ ! -e "$BINARIES_DIR/$NAME" ]; then
                case "$DEP" in
                    @rpath/*) SRC="$(resolve_rpath_dep "$NAME")" \
                        || { echo "error: cannot resolve $DEP (needed by $MACHO)" >&2; exit 1; } ;;
                    *) SRC="$DEP" ;;
                esac
                cp -L "$SRC" "$BINARIES_DIR/$NAME"
                chmod u+w "$BINARIES_DIR/$NAME"
                install_name_tool -id "@loader_path/$NAME" "$BINARIES_DIR/$NAME" 2>/dev/null
                echo "  [pass $PASS] bundled $NAME  (from $SRC)"
            fi
            install_name_tool -change "$DEP" "@loader_path/$NAME" "$MACHO" 2>/dev/null
            CHANGED=1
        done < <(bad_deps "$MACHO")
    done
done

# Drop dylibs colmap no longer reaches (e.g. after a rebuild without ONNX):
# the app copies every Binaries/lib*.dylib, so leftovers would ship.
REACHED=" "
QUEUE="colmap"   # basenames in Binaries/ (the project path has spaces)
while [ -n "$QUEUE" ]; do
    set -- $QUEUE; CUR="$1"; shift; QUEUE="$*"
    # A failed inspection must stop here: an empty list would prune everything.
    [ -f "$BINARIES_DIR/$CUR" ] || { echo "error: colmap needs $CUR, not in Binaries/" >&2; exit 1; }
    LOADS="$(otool -L "$BINARIES_DIR/$CUR")" || { echo "error: otool failed on $CUR" >&2; exit 1; }
    for DEP in $(printf '%s\n' "$LOADS" | tail -n +2 | awk '{print $1}' | sed -n 's|^@loader_path/||p'); do
        case "$REACHED" in *" $DEP "*) continue ;; esac
        REACHED="$REACHED$DEP "
        QUEUE="$QUEUE $DEP"
    done
done
for DYLIB in "$BINARIES_DIR"/lib*.dylib; do
    [ -e "$DYLIB" ] || continue
    case "$REACHED" in *" $(basename "$DYLIB") "*) ;; *) echo "  unused, removed: $(basename "$DYLIB")"; rm -f "$DYLIB" ;; esac
done

# All references are @loader_path now — absolute rpaths are dead weight that
# leaks dev-machine paths (and caused the original crash).
for MACHO in "$COLMAP_BIN" "$BINARIES_DIR"/lib*.dylib; do
    [ -e "$MACHO" ] || continue
    strip_abs_rpaths "$MACHO"
done

# Ad-hoc re-sign everything (install_name_tool invalidated the signatures).
for MACHO in "$COLMAP_BIN" "$BINARIES_DIR"/lib*.dylib; do
    [ -e "$MACHO" ] || continue
    codesign --force -s - "$MACHO" 2>/dev/null
done

# ── Audit: fail loudly if anything machine-specific survived ──
FAIL=0
for MACHO in "$COLMAP_BIN" "$BINARIES_DIR"/lib*.dylib; do
    [ -e "$MACHO" ] || continue
    LEFT="$(bad_deps "$MACHO" | grep -v "^@rpath/$(basename "$MACHO")$" || true)"
    if [ -n "$LEFT" ]; then
        echo "error: $MACHO still references:" >&2
        echo "$LEFT" >&2
        FAIL=1
    fi
    RPATHS="$(otool -l "$MACHO" | grep -A2 'cmd LC_RPATH' | grep ' path /' || true)"
    if [ -n "$RPATHS" ]; then
        echo "error: $MACHO still has absolute LC_RPATH:" >&2
        echo "$RPATHS" >&2
        FAIL=1
    fi
    codesign --verify "$MACHO" || { echo "error: invalid signature on $MACHO" >&2; FAIL=1; }
done
[ "$FAIL" = "0" ] || exit 1

# Prove the result actually loads (dyld resolves every @loader_path dep).
"$COLMAP_BIN" help >/dev/null 2>&1 || { echo "error: self-contained colmap failed to run" >&2; exit 1; }

N_DYLIBS=$(ls "$BINARIES_DIR"/lib*.dylib 2>/dev/null | wc -l | tr -d ' ')
TOTAL_SIZE=$(du -ch "$COLMAP_BIN" "$BINARIES_DIR"/lib*.dylib 2>/dev/null | tail -1 | awk '{print $1}')
echo "── colmap self-contained: $N_DYLIBS bundled dylibs, $TOTAL_SIZE total — audit clean, binary runs ──"
