From 5ffb349434daf52ad25921efed188faae4f35512 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Sat, 8 Aug 2026 20:57:17 -0700 Subject: [PATCH] macos: relocate libpq to @rpath for Homebrew layout portability The pg_client extension is linked in CI against the keg-only standalone 'libpq' Homebrew formula, which records an absolute install name of /opt/homebrew/opt/libpq/lib/libpq.5.dylib in the released binary. End users who install libpq via 'brew install postgresql@18' only (the common layout on Apple Silicon) have libpq at a different path, so dyld cannot resolve the dependency and the extension fails to load. Add scripts/relocate-macos-libpq.sh (mirroring the existing scripts/relocate-macos-openssl.sh pattern) to rewrite the install name to @rpath/libpq.5.dylib and add the common Homebrew locations as LC_RPATH fallbacks, plus scripts/verify-macos-libpq-rpaths.sh to assert the relocation in CI. Fixes LadybugDB/extensions#48. --- scripts/relocate-macos-libpq.sh | 67 ++++++++++++++++++++++++++++ scripts/verify-macos-libpq-rpaths.sh | 47 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100755 scripts/relocate-macos-libpq.sh create mode 100755 scripts/verify-macos-libpq-rpaths.sh diff --git a/scripts/relocate-macos-libpq.sh b/scripts/relocate-macos-libpq.sh new file mode 100755 index 00000000..dadf3f2a --- /dev/null +++ b/scripts/relocate-macos-libpq.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Relocates the libpq dependency of a macOS Mach-O binary from the +# package-manager-specific absolute path recorded at build time +# (e.g. /opt/homebrew/opt/libpq/lib/libpq.5.dylib) to @rpath/libpq.5.dylib, +# then adds the common Homebrew locations that provide libpq as LC_RPATH +# fallbacks. +# +# This mirrors scripts/relocate-macos-openssl.sh in the ladybug repo and +# fixes LadybugDB/extensions#48: the pg_client extension is linked against +# the keg-only standalone `libpq` Homebrew formula during CI, but end users +# often have libpq only via `brew install postgresql@18` (which installs it +# to a different location). Without relocation the released binary cannot be +# loaded on such machines. +# +# Note: LC_RPATH entries pointing at directories that do not exist on the +# target machine are silently skipped by dyld, so adding every known +# Homebrew layout is safe. When a new PostgreSQL major version becomes a +# common Homebrew formula, append its locations to the list below. + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +binary="$1" +if [ ! -f "$binary" ]; then + echo "Mach-O binary not found: $binary" >&2 + exit 1 +fi + +LIBPQ="libpq.5.dylib" + +dependency_for() { + local library="$1" + otool -L "$binary" | awk -v library="$library" \ + 'index($1, library) && substr($1, length($1) - length(library) + 1) == library { print $1; exit }' +} + +dependency="$(dependency_for "$LIBPQ")" +if [ -z "$dependency" ]; then + echo "libpq dependency not found in $binary" >&2 + exit 1 +fi +if [ "$dependency" != "@rpath/$LIBPQ" ]; then + install_name_tool -change "$dependency" "@rpath/$LIBPQ" "$binary" +fi + +for rpath in \ + /opt/homebrew/opt/libpq/lib \ + /usr/local/opt/libpq/lib \ + /opt/homebrew/lib/postgresql@18 \ + /usr/local/lib/postgresql@18 \ + /opt/homebrew/opt/postgresql@18/lib \ + /usr/local/opt/postgresql@18/lib; do + existing_rpaths="$(otool -l "$binary" | awk '/cmd LC_RPATH/{getline; getline; print $2}')" + if ! grep -Fxq "$rpath" <<<"$existing_rpaths"; then + install_name_tool -add_rpath "$rpath" "$binary" + fi +done + +# install_name_tool invalidates the existing signature. +# Ad-hoc signing ("-") is sufficient for development and CI. If this binary is +# distributed to end-users (e.g., via npm), a proper Developer ID certificate +# should be used instead for notarization compatibility. +codesign --force --sign - "$binary" \ No newline at end of file diff --git a/scripts/verify-macos-libpq-rpaths.sh b/scripts/verify-macos-libpq-rpaths.sh new file mode 100755 index 00000000..3aa08128 --- /dev/null +++ b/scripts/verify-macos-libpq-rpaths.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verifies that a macOS Mach-O binary's libpq dependency has been relocated +# by scripts/relocate-macos-libpq.sh: it must reference @rpath/libpq.5.dylib, +# must not reference any package-manager-specific absolute libpq path, and +# must carry every known Homebrew libpq location as an LC_RPATH entry. + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +binary="$1" +if [ ! -f "$binary" ]; then + echo "Mach-O binary not found: $binary" >&2 + exit 1 +fi + +dependencies="$(otool -L "$binary")" + +if ! grep -Fq "@rpath/libpq.5.dylib" <<<"$dependencies"; then + echo "missing @rpath dependency for libpq in $binary" >&2 + exit 1 +fi + +# The leading '/' is matched by the '^[[:space:]]+/' portion of the regex, so the +# alternatives within the group omit it: 'opt/homebrew', 'usr/local', 'opt/local'. +if grep -Eq '^[[:space:]]+/(opt/homebrew|usr/local|opt/local)/.*libpq\.5\.dylib' \ + <<<"$dependencies"; then + echo "package-manager-specific libpq dependency remains in $binary" >&2 + exit 1 +fi + +rpaths="$(otool -l "$binary" | awk '/cmd LC_RPATH/{getline; getline; print $2}')" +for required in \ + /opt/homebrew/opt/libpq/lib \ + /usr/local/opt/libpq/lib \ + /opt/homebrew/lib/postgresql@18 \ + /usr/local/lib/postgresql@18 \ + /opt/homebrew/opt/postgresql@18/lib \ + /usr/local/opt/postgresql@18/lib; do + if ! grep -Fxq "$required" <<<"$rpaths"; then + echo "missing libpq rpath $required in $binary" >&2 + exit 1 + fi +done \ No newline at end of file