Skip to content

Instantly share code, notes, and snippets.

@arbruijn
Forked from Birch-san/make_portable.sh
Created October 12, 2022 13:22
Show Gist options
  • Save arbruijn/7db5c556bbef0f86ce515f112196e6f3 to your computer and use it in GitHub Desktop.
Save arbruijn/7db5c556bbef0f86ce515f112196e6f3 to your computer and use it in GitHub Desktop.
Make a macOS executable binary or .dylib portable
#!/usr/bin/env bash
# Licensed by author Alex Birch under CC BY-SA 4.0
# https://creativecommons.org/licenses/by-sa/4.0/
# Example input:
# ./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 handler by Charles Duffy
# https://stackoverflow.com/q/64786
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")
LIBREL="lib"
LIB="$BINARYDIR/$LIBREL"
# make a lib folder
mkdir -p "$LIB"
# 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 "$LIB"
# copy each distinct dylib in the dependency tree into our lib folder
echo "$DEP_PATHS" \
| xargs -n1 realpath \
| sort \
| uniq \
| xargs -I'{}' cp {} "$LIB/"
chmod +w "$LIB"/*.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 "@rpath/$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" "@rpath/$LOAD_LEAF_NAME" "$obj"
done < <(get_env_specific_direct_dependencies "$obj")
done < <(cat <(echo "$BINARY") <(echo "$DEP_PATHS" | awk -F'/' -v l="$LIB" -v OFS='/' '{print l,$NF}'))
# define in our binary what it should expand the
# runtime search path @rpath to
install_name_tool -add_rpath "@loader_path/$LIBREL" "$BINARY"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment