Skip to content

Instantly share code, notes, and snippets.

Last active March 1, 2023 18:33
  • Star 10 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
What would you like to do?
Make a macOS executable binary or .dylib portable
#!/usr/bin/env bash
# Licensed by author Alex Birch under CC BY-SA 4.0
# Example input:
# ./ 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
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}"
echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
exit "${code}"
trap 'error ${LINENO}' ERR
BINARYDIR=$(dirname "$BINARY")
# 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=$(echo "$ALL_DYLIBS" | grep -v "$DYLIB_ID")
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"
Copy link

Fixed cp: ./lib/libogg.0.dylib: Permission denied; copying same file multiple times into same place (you would clash with a previous attempt, which was not yet writeable)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment