fix(pkg): retry otool/install_name_tool with absolute path before giving up

pgadmin4-appbundle-build #116 (macos-arm64) failed even with
otool-classic invoked directly via its real resolved path
(669c2f244's fix) - identical error, byte-for-byte, to calling plain
`otool -L`:

  can't open file: .../pgAdmin 4 Helper  (No such file or directory)

That rules out PATH resolution and otool's internal re-exec dispatch
as the cause too. Both otool -L and otool-classic -L, invoked directly
with correct argv quoting and a verified-correct, verified-present
path, fail identically on this one binary. Whatever's actually broken
is inside these tools' own handling of "(" in a path they're given.

Untested variable so far: this script pushd's into the bundle dir and
passes relative paths ("./Contents/...") throughout. Try a plain
absolute path with each tool before falling back to the diagnostic
dump - cheap to test, and applies equally to the two
`install_name_tool -change` calls further down that edit this same
file in place, since they could hit the identical issue.

Still not proven - if this also fails, the diagnostic dump (retained
as final fallback) will at least confirm parens themselves are the
blocker regardless of path form, which would mean sidestepping via a
temporary parens-free symlink is the next thing to try.
This commit is contained in:
Ashesh Vashi committed 2026-07-30 12:10:59 +05:30
1 parent 669c2f2442
commit 6891b5e12f
1 file changed
+50 -20
+50 -20
View File
@@ -169,7 +169,7 @@ _build_docs() {
} }
_fixup_imports() { _fixup_imports() {
local TODO TODO_OLD TODO_PYTHON FW_RELPATH LIB LIB_BN OTOOL_OUTPUT OTOOL_CLASSIC local TODO TODO_OLD TODO_PYTHON FW_RELPATH LIB LIB_BN OTOOL_OUTPUT OTOOL_CLASSIC ABS_TODO_OBJ
echo "Fixing imports on the core appbundle..." echo "Fixing imports on the core appbundle..."
pushd "$1" > /dev/null || exit pushd "$1" > /dev/null || exit
@@ -227,6 +227,12 @@ _fixup_imports() {
printf '%s' "${TODO_OBJ}" | od -c printf '%s' "${TODO_OBJ}" | od -c
fi fi
# Computed once up front - both the otool and install_name_tool
# fallbacks below need it, in case the relative "./Contents/..."
# path (this script pushd's into the bundle dir) combined with
# "(" is what these tools choke on.
ABS_TODO_OBJ="$(pwd)/${TODO_OBJ#./}"
# Figure out the relative path from ${TODO_OBJ} to Contents/Frameworks # Figure out the relative path from ${TODO_OBJ} to Contents/Frameworks
FW_RELPATH=$(echo "${TODO_OBJ}" | \ FW_RELPATH=$(echo "${TODO_OBJ}" | \
sed -n 's|^\(\.//*\)\(\([^/][^/]*/\)*\)[^/][^/]*$|\2|gp' | \ sed -n 's|^\(\.//*\)\(\([^/][^/]*/\)*\)[^/][^/]*$|\2|gp' | \
@@ -235,16 +241,16 @@ _fixup_imports() {
# Find all libraries ${TODO_OBJ} depends on, but skip system libraries. # Find all libraries ${TODO_OBJ} depends on, but skip system libraries.
# #
# otool sometimes re-execs the legacy otool-classic internally # Both `otool -L` and (invoked directly, with correct argv
# for certain Mach-O binaries (seen consistently on # quoting, real resolved path) `otool-classic -L` fail to
# "pgAdmin 4 Helper (GPU)"), and that internal re-exec fails # open "pgAdmin 4 Helper (GPU)" - a file confirmed present,
# to open a file confirmed present, correctly-permissioned, # correctly-permissioned, and byte-for-byte correctly-pathed
# and byte-for-byte correctly-pathed at that exact moment # at that exact moment (verified via ls -la + od -c on prior
# (verified via ls -la + od -c on a prior failing build) - # failing builds). Neither this script's quoting nor PATH
# i.e. otool's own internal dispatch is broken here, not # resolution is the cause; whatever's wrong is inside these
# anything this script passes it. Retry via otool-classic # tools' own handling of "(" in a path. Try a plain absolute
# directly (our own quoting, not otool's internal re-exec) # path with each tool before falling back to a raw
# before giving up. # diagnostic dump.
# #
# NOTE: the assignment must be the condition of the `if` # NOTE: the assignment must be the condition of the `if`
# itself, not a separate statement followed by checking $? - # itself, not a separate statement followed by checking $? -
@@ -254,8 +260,15 @@ _fixup_imports() {
# inspected. Bash exempts the tested command of an `if` from # inspected. Bash exempts the tested command of an `if` from
# errexit. # errexit.
if ! OTOOL_OUTPUT=$(otool -L "${TODO_OBJ}" 2>&1); then if ! OTOOL_OUTPUT=$(otool -L "${TODO_OBJ}" 2>&1); then
echo "WARNING: otool -L failed on: ${TODO_OBJ}" echo "WARNING: otool -L failed on relative path: ${TODO_OBJ}"
echo "otool output: ${OTOOL_OUTPUT}" echo "otool output: ${OTOOL_OUTPUT}"
echo "Retrying via otool -L with an absolute path (${ABS_TODO_OBJ})..."
if OTOOL_OUTPUT=$(otool -L "${ABS_TODO_OBJ}" 2>&1); then
echo "Absolute path worked."
fi
fi
if [ -z "${OTOOL_OUTPUT}" ] || [[ "${OTOOL_OUTPUT}" == *"can't open file"* ]]; then
# otool-classic isn't on PATH - it only lives inside the # otool-classic isn't on PATH - it only lives inside the
# active Xcode toolchain (confirmed by a prior failed # active Xcode toolchain (confirmed by a prior failed
# attempt at calling the bare name: "command not found"). # attempt at calling the bare name: "command not found").
@@ -264,9 +277,9 @@ _fixup_imports() {
if [ -z "${OTOOL_CLASSIC}" ]; then if [ -z "${OTOOL_CLASSIC}" ]; then
OTOOL_CLASSIC="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool-classic" OTOOL_CLASSIC="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool-classic"
fi fi
echo "Retrying via otool-classic directly (${OTOOL_CLASSIC})..." echo "Retrying via otool-classic directly (${OTOOL_CLASSIC}), absolute path..."
if ! OTOOL_OUTPUT=$("${OTOOL_CLASSIC}" -L "${TODO_OBJ}" 2>&1); then if ! OTOOL_OUTPUT=$("${OTOOL_CLASSIC}" -L "${ABS_TODO_OBJ}" 2>&1); then
echo "ERROR: otool-classic -L also failed on: ${TODO_OBJ}" echo "ERROR: otool-classic -L also failed on: ${ABS_TODO_OBJ}"
echo "otool-classic output: ${OTOOL_OUTPUT}" echo "otool-classic output: ${OTOOL_OUTPUT}"
echo "Raw bytes of the path (od -c):" echo "Raw bytes of the path (od -c):"
printf '%s' "${TODO_OBJ}" | od -c printf '%s' "${TODO_OBJ}" | od -c
@@ -305,16 +318,33 @@ _fixup_imports() {
TODO="${TODO}"$'\n'"./Contents/Frameworks/${LIB_BN}" TODO="${TODO}"$'\n'"./Contents/Frameworks/${LIB_BN}"
fi fi
# Rewrite the dependency paths # Rewrite the dependency paths.
#
# Same relative-path fallback as the otool -L call above:
# install_name_tool edits ${TODO_OBJ} in place, so if it
# hits the same "(" handling issue, retry with the
# absolute path before giving up.
echo "Rewriting library ${LIB} to @loader_path/${FW_RELPATH}/${LIB_BN} in ${TODO_OBJ}" echo "Rewriting library ${LIB} to @loader_path/${FW_RELPATH}/${LIB_BN} in ${TODO_OBJ}"
install_name_tool -change \ if ! install_name_tool -change \
"${LIB}" \ "${LIB}" \
"@loader_path/${FW_RELPATH}/${LIB_BN}" \ "@loader_path/${FW_RELPATH}/${LIB_BN}" \
"${TODO_OBJ}" || exit 1 "${TODO_OBJ}" 2>/dev/null; then
install_name_tool -change \ echo "Retrying install_name_tool -change with absolute path (${ABS_TODO_OBJ})..."
install_name_tool -change \
"${LIB}" \
"@loader_path/${FW_RELPATH}/${LIB_BN}" \
"${ABS_TODO_OBJ}" || exit 1
fi
if ! install_name_tool -change \
"${TARGET_PATH}" \ "${TARGET_PATH}" \
"@loader_path/${FW_RELPATH}/${TARGET_FILE}" \ "@loader_path/${FW_RELPATH}/${TARGET_FILE}" \
"${TODO_OBJ}" || exit 1 "${TODO_OBJ}" 2>/dev/null; then
echo "Retrying install_name_tool -change with absolute path (${ABS_TODO_OBJ})..."
install_name_tool -change \
"${TARGET_PATH}" \
"@loader_path/${FW_RELPATH}/${TARGET_FILE}" \
"${ABS_TODO_OBJ}" || exit 1
fi
done done
done <<< "${TODO_OLD}" done <<< "${TODO_OLD}"
done done