#!/usr/bin/env bash

# Example input:
# ./make_portable.sh mycoolbinary
# Or:
# USE_RPATH=1 ./make_portable.sh mycoolbinary
#
# where mycoolbinary is a mach-o object file
# (for example an executable binary or a .dylib)
#
# this script rewrites your file's every environment-specific
# dynamic link (recursively!)
# such that they point to local .dylibs.
# these .dylibs are then copied to a folder lib, next to your binary
#
# by "environment-specific" I mean any link to a .dylib under /usr/local

set -o pipefail

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

BINARY="$1"
BINARYDIR=$(dirname "$BINARY")
LIBDIRNAME=${LIBDIRNAME:-lib}
LIBOUTPUTDIR="$BINARYDIR/$LIBDIRNAME"

# assume that our binary is running from:
# CoolBinary.app/Contents/MacOS/coolbinary
# and wants to navigate to:
# CoolBinary.app/Contents/lib/coollibrary.dylib
RPATH_FALLBACK="@loader_path/../$LIBDIRNAME"

if [ -z ${USE_RPATH+x} ]; then
	# don't use rpath
	OBJLOADREL="$RPATH_FALLBACK"
else
	OBJLOADREL="@rpath"
	# define in our binary what it should expand the
	# runtime search path @rpath to
	install_name_tool -add_rpath "$RPATH_FALLBACK" "$BINARY"
fi

# make a lib folder
mkdir -p "$LIBOUTPUTDIR"

# find every LC_LOAD_DYLIB command in the obj file
# filter to just loads under /usr/local
# print the absolute path of each such dylib
get_env_specific_direct_dependencies () {
	# otool -L shows us every LC_LOAD_DYLIB plus LC_ID_DYLIB
	# otool -D shows us just LC_ID_DYLIB
	ALL_DYLIBS=$(otool -L "$1" | awk 'NR>1')
	DYLIB_ID=$(otool -D "$1" | awk 'NR>1')
	if [ -z "$DYLIB_ID" ]; then
		DIRECT_DEPS="$ALL_DYLIBS"
	else
		DIRECT_DEPS=$(echo "$ALL_DYLIBS" | grep -v "$DYLIB_ID")
	fi
	echo "$DIRECT_DEPS" \
	| awk '/\/usr\/local\//,/.dylib/ {print $1}'
}

# lookup LC_LOAD_DYLIB commands in an obj file,
# then follow those loads and ask the same of each
# of its dylibs, recursively
get_env_specific_dependencies_recursive () {
	while read -r obj; do
		[ -z "$obj" ] && continue
		echo "$obj"
		get_env_specific_dependencies_recursive "$obj"
	done < <(get_env_specific_direct_dependencies "$1")
}

DEP_PATHS=$(get_env_specific_dependencies_recursive "$BINARY")

mkdir -p "$LIBOUTPUTDIR"
# copy each distinct dylib in the dependency tree into our lib folder
echo "$DEP_PATHS" \
| xargs -n1 realpath \
| sort \
| uniq \
| xargs -I'{}' cp {} "$LIBOUTPUTDIR/"

chmod +w "$BINARY" "$LIBOUTPUTDIR"/*.dylib

while read -r obj; do
	[ -z "$obj" ] && continue
	OBJ_LEAF_NAME=$(echo "$obj" | awk -F'/' '{print $NF}')
	# rewrite the install name of this obj file. completely optional.
	# provides good default for future people who link to it.
	install_name_tool -id "$OBJLOADREL/$OBJ_LEAF_NAME" "$obj"

	# iterate over every LC_LOAD_DYLIB command in the objfile
	while read -r load; do
		[ -z "$load" ] && continue
		LOAD_LEAF_NAME=$(echo "$load" | awk -F'/' '{print $NF}')
		# rewrite a LC_LOAD_DYLIB command in this obj file
		# to point relative to @rpath
		install_name_tool -change "$load" "$OBJLOADREL/$LOAD_LEAF_NAME" "$obj"
	done < <(get_env_specific_direct_dependencies "$obj")
done < <(cat <(echo "$BINARY") <(echo "$DEP_PATHS" | awk -F'/' -v l="$LIBOUTPUTDIR" -v OFS='/' '{print l,$NF}'))
