From 492d208c0c90822fe182014a47cefcb9a2a2744e Mon Sep 17 00:00:00 2001 From: Dave Page Date: Thu, 18 Jun 2026 13:56:24 +0100 Subject: [PATCH] fix(mac): prune dangling symlinks before codesign so Gatekeeper accepts the bundle A signed, notarised and stapled pgAdmin 4.app was rejected by Gatekeeper with "invalid destination for symbolic link in bundle". Gatekeeper walks every symlink in the bundle and rejects the whole app if any link does not resolve to a real file inside it; notarisation does not catch this, so a broken link slips through stapling and only surfaces as a Gatekeeper failure on the end user's machine. The embedded Python.framework ships such links: an arm64-only build still carries a bin/python3-intel64 launcher symlink (whose target _strip_architecture deletes when it removes the foreign-arch files), and the bundled Tcl/Tk frameworks carry PrivateHeaders links pointing at a Versions/Current that has none. Add a _prune_dangling_symlinks step that removes every dangling symlink in the bundle after architecture stripping and before signing, then fails the build if any remain, so this cannot slip past notarisation again. --- pkg/mac/build-functions.sh | 30 ++++++++++++++++++++++++++++++ pkg/mac/build.sh | 1 + 2 files changed, 31 insertions(+) diff --git a/pkg/mac/build-functions.sh b/pkg/mac/build-functions.sh index 63eb90e1df5..b45b9dc3819 100644 --- a/pkg/mac/build-functions.sh +++ b/pkg/mac/build-functions.sh @@ -427,6 +427,36 @@ _strip_architecture() { done < <(find "${BUNDLE_DIR}" -type f) } +_prune_dangling_symlinks() { + # Gatekeeper walks every symlink in the bundle and rejects the whole app + # with "invalid destination for symbolic link in bundle" if any link does + # not resolve to a real file inside the app. Notarisation does NOT catch + # this, so a broken link slips through stapling and only surfaces as a + # Gatekeeper failure on the end user's machine. + # + # The embedded Python.framework ships such links: an arm64-only build still + # carries a bin/python*-intel64 launcher symlink (whose target we delete in + # _strip_architecture), and the bundled Tcl/Tk frameworks carry + # PrivateHeaders links pointing at a Versions/Current that has none. Rather + # than hunt individual offenders, prune EVERY dangling symlink so a future + # stray link cannot reintroduce the bug. + # + # NB: this MUST run after _strip_architecture (which orphans links by + # deleting their targets) and before _codesign_binaries / _codesign_bundle. + + echo "Pruning dangling symlinks before signing..." + # -type l selects symlinks; "! -exec test -e {} \;" keeps only those whose + # target does not exist (test -e follows the link). BSD find on macOS. + find "${BUNDLE_DIR}" -type l ! -exec test -e {} \; -print -delete + + # Belt and braces: fail the build if anything still dangles, so this can + # never silently slip past notarisation into a shipped bundle again. + if find "${BUNDLE_DIR}" -type l ! -exec test -e {} \; -print | grep -q .; then + echo "ERROR: bundle still contains dangling symlinks (Gatekeeper will reject)" >&2 + exit 1 + fi +} + _generate_sbom() { echo "Generating SBOM..." syft "${BUNDLE_DIR}/Contents/" -o cyclonedx-json > "${BUNDLE_DIR}/Contents/sbom.json" diff --git a/pkg/mac/build.sh b/pkg/mac/build.sh index 564333769ee..dae3a987df5 100755 --- a/pkg/mac/build.sh +++ b/pkg/mac/build.sh @@ -93,6 +93,7 @@ _create_python_env _build_docs _complete_bundle _strip_architecture +_prune_dangling_symlinks _generate_sbom _codesign_binaries _codesign_bundle