#!/usr/bin/env bash # Recursively copy non-system dylibs next to a Mach-O binary and rewrite load paths. set -euo pipefail find_homebrew_lib() { local base="$1" find "${HOMEBREW_PREFIX:-/opt/homebrew}/Cellar" "${HOMEBREW_PREFIX:-/opt/homebrew}/opt" \ -name "${base}" -path '*/lib/*' 2>/dev/null | head -1 } resolve_dependency_path() { local owner="$1" local dep="$2" local reference="${3:-${owner}}" if [[ "${dep}" != @* ]]; then [[ -f "${dep}" ]] || return 1 printf '%s\n' "${dep}" return 0 fi local owner_dir reference_dir owner_dir="$(cd "$(dirname "${owner}")" && pwd)" reference_dir="$(cd "$(dirname "${reference}")" && pwd)" case "${dep}" in @loader_path/*) local candidate="${owner_dir}/${dep#@loader_path/}" [[ -f "${candidate}" ]] || return 1 printf '%s\n' "${candidate}" ;; @executable_path/*) local rel="${dep#@executable_path/}" local candidate="${owner_dir}/${rel}" if [[ ! -f "${candidate}" ]]; then candidate="${reference_dir}/${rel}" fi if [[ ! -f "${candidate}" ]]; then candidate="$(find_homebrew_lib "$(basename "${rel}")")" fi [[ -f "${candidate}" ]] || return 1 printf '%s\n' "${candidate}" ;; @rpath/*) local rel="${dep#@rpath/}" local rpath candidate while IFS= read -r rpath; do case "${rpath}" in @loader_path/*) candidate="${owner_dir}/${rpath#@loader_path/}/${rel}" ;; @executable_path/*) candidate="${owner_dir}/${rpath#@executable_path/}/${rel}" ;; *) candidate="${rpath}/${rel}" ;; esac if [[ -f "${candidate}" ]]; then printf '%s\n' "${candidate}" return 0 fi done < <( otool -l "${owner}" | awk ' /cmd LC_RPATH/ { want=1; next } want && /path / { sub(/^.*path /, "", $0) sub(/ \(offset.*$/, "", $0) print want=0 } ' ) candidate="${owner_dir}/${rel}" [[ -f "${candidate}" ]] || return 1 printf '%s\n' "${candidate}" ;; *) return 1 ;; esac } list_load_dylibs() { local target="$1" otool -l "${target}" | awk ' /cmd LC_LOAD_DYLIB/ { want=1; next } want && /name / { sub(/^.*name /, "", $0) sub(/ \(offset.*$/, "", $0) print want=0 } ' } bundle_mach_dylibs() { local binary="${1:?binary path required}" local lib_dir="${2:?lib directory required}" local reference_binary="${3:-${binary}}" [[ -f "${binary}" ]] || { echo "bundle_mach_dylibs: missing binary: ${binary}" >&2 return 1 } mkdir -p "${lib_dir}" local -a queue=("${binary}") local seen_list=" " local -a sources=() is_system_lib() { case "$1" in /usr/lib/*|/System/*|/Library/*) return 0 ;; esac return 1 } already_seen() { [[ "${seen_list}" == *" $1 "* ]] } remember() { seen_list+=" $1 " } while ((${#queue[@]} > 0)); do local target="${queue[0]}" queue=("${queue[@]:1}") [[ -f "${target}" ]] || continue local dep resolved while IFS= read -r dep; do [[ -n "${dep}" ]] || continue is_system_lib "${dep}" && continue already_seen "${dep}" && continue remember "${dep}" resolved="$(resolve_dependency_path "${target}" "${dep}" "${reference_binary}")" || { echo "bundle_mach_dylibs: missing dependency ${dep} for ${target}" >&2 return 1 } already_seen "${resolved}" || remember "${resolved}" if [[ "${resolved}" != "${binary}" ]]; then sources+=("${resolved}") queue+=("${resolved}") fi done < <(list_load_dylibs "${target}") done local src base for src in "${sources[@]}"; do base="$(basename "${src}")" cp -f "${src}" "${lib_dir}/${base}" chmod 755 "${lib_dir}/${base}" done rewrite_dep() { local target="$1" local dep="$2" local new_path="$3" install_name_tool -change "${dep}" "${new_path}" "${target}" 2>/dev/null || true } rewrite_targets() { local target="$1" local prefix="$2" local dep base while IFS= read -r dep; do [[ -n "${dep}" ]] || continue is_system_lib "${dep}" && continue [[ "${dep}" == @* ]] && continue base="$(basename "${dep}")" [[ -f "${lib_dir}/${base}" ]] || continue rewrite_dep "${target}" "${dep}" "${prefix}${base}" done < <(list_load_dylibs "${target}") } rewrite_rpath_targets() { local target="$1" local prefix="$2" local dep base while IFS= read -r dep; do [[ -n "${dep}" ]] || continue [[ "${dep}" == @rpath/* ]] || continue base="$(basename "${dep}")" [[ -f "${lib_dir}/${base}" ]] || continue rewrite_dep "${target}" "${dep}" "${prefix}${base}" done < <(list_load_dylibs "${target}") } local changed=1 while ((changed)); do changed=0 rewrite_targets "${binary}" "@executable_path/../lib/" rewrite_rpath_targets "${binary}" "@executable_path/../lib/" for staged in "${lib_dir}"/*.dylib; do [[ -f "${staged}" ]] || continue before="$(list_load_dylibs "${staged}" | { grep -E 'homebrew|^@rpath/' || true; } | wc -l | tr -d ' ')" rewrite_targets "${staged}" "@loader_path/" rewrite_rpath_targets "${staged}" "@loader_path/" after="$(list_load_dylibs "${staged}" | { grep -E 'homebrew|^@rpath/' || true; } | wc -l | tr -d ' ')" [[ "${before}" != "${after}" ]] && changed=1 done done local remaining remaining="$(list_load_dylibs "${binary}" | grep homebrew || true)" if [[ -n "${remaining}" ]]; then echo "bundle_mach_dylibs: unresolved homebrew dependencies remain:" >&2 printf '%s\n' "${remaining}" >&2 return 1 fi install_name_tool -add_rpath "@executable_path/../lib" "${binary}" 2>/dev/null || true local staged for staged in "${lib_dir}"/*.dylib; do [[ -f "${staged}" ]] || continue codesign --force --sign - "${staged}" >/dev/null done codesign --force --sign - "${binary}" >/dev/null } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then bundle_mach_dylibs "$@" fi