From 45abd1e9adee74c62b34bbcd4d8ba9bb407cc524 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 10 Jun 2026 11:50:16 +0100 Subject: [PATCH] Strip foreign-architecture code from the macOS bundle (#10063) The macOS app is built for a single architecture (matching the build machine, via ${ARCH}), but relocatable-python pulls the python.org universal2 installer, so the entire Python.framework ships both arm64 and x86_64 slices. PostgreSQL-sourced dylibs may be universal too. The foreign slice is dead weight that bloats the bundle and DMG. Add a _strip_architecture step, run after _complete_bundle and before code-signing (lipo invalidates signatures, so the existing sign passes re-sign the thinned binaries). It removes the universal2 stragglers (python*-intel64 launcher, config-*-darwin/python.o) and lipo-thins every fat Mach-O in the bundle to the build arch, preserving file modes and warning on anything lacking the target slice. Already single-arch inputs (Electron and its helpers) are skipped. --- docs/en_US/release_notes_9_16.rst | 1 + pkg/mac/build-functions.sh | 63 +++++++++++++++++++++++++++++++ pkg/mac/build.sh | 1 + 3 files changed, 65 insertions(+) diff --git a/docs/en_US/release_notes_9_16.rst b/docs/en_US/release_notes_9_16.rst index 4f0740cdff0..879db74a38f 100644 --- a/docs/en_US/release_notes_9_16.rst +++ b/docs/en_US/release_notes_9_16.rst @@ -35,6 +35,7 @@ Housekeeping | `Issue #9981 `_ - Clarify the SSH tunnel "Prompt for identity file password?" switch label and help text to indicate it applies only to identity-file authentication. | `Issue #10018 `_ - Remove the EDB BigAnimal cloud deployment support. + | `Issue #10063 `_ - Strip the foreign-architecture slice from the macOS bundle so single-arch builds no longer ship the universal2 Python framework's unused arm64/x86_64 code. Bug fixes ********* diff --git a/pkg/mac/build-functions.sh b/pkg/mac/build-functions.sh index d1fb198a5a0..63eb90e1df5 100644 --- a/pkg/mac/build-functions.sh +++ b/pkg/mac/build-functions.sh @@ -364,6 +364,69 @@ _complete_bundle() { chmod -R og-w "${BUNDLE_DIR}" } +_strip_architecture() { + # We only ship a single architecture (matching the build machine, via + # ${ARCH}), but some inputs arrive as fat/universal2 Mach-O binaries. + # In particular relocatable-python pulls the python.org *universal2* + # installer, so the entire Python.framework carries both arm64 and + # x86_64 slices; PostgreSQL-sourced dylibs (libpq, libssl, ...) may be + # universal too. Strip the foreign slice from every fat Mach-O so the + # bundle ships lean. Electron and its helpers are downloaded + # single-arch already, so the loop simply skips them. + # + # NB: lipo invalidates code signatures, so this MUST run before + # _codesign_binaries / _codesign_bundle. + + # Map the build ARCH ("arm64"/"x64") to the lipo/Mach-O arch name. + local LIPO_ARCH="arm64" + if [ "${ARCH}" == "x64" ]; then + LIPO_ARCH="x86_64" + fi + + echo "Stripping foreign architectures, keeping ${LIPO_ARCH}..." + + # Remove arch-specific stragglers shipped by the universal2 Python + # installer: a pure-x86_64 launcher and a stray, never-executed build + # object file. Globs keep this independent of the Python version. + find "${BUNDLE_DIR}/Contents/Frameworks/Python.framework" \ + -name 'python*-intel64' -type f -delete + find "${BUNDLE_DIR}/Contents/Frameworks/Python.framework" \ + -path '*/config-*-darwin/python.o' -type f -delete + + # Thin every fat Mach-O in the bundle in place. -type f skips symlinks, + # so versioned dylib aliases are left alone and only the real file is + # thinned once. + local f archs perms + while IFS= read -r f; do + archs=$(lipo -archs "${f}" 2>/dev/null) || continue # not Mach-O + case " ${archs} " in + *" ${LIPO_ARCH} "*) ;; # has our slice + *) + # No slice for our target arch — thinning can't help; this + # would need a rebuild from the right arch. Warn loudly. + echo "WARNING: ${f} lacks a ${LIPO_ARCH} slice (${archs}); leaving as-is" >&2 + continue + ;; + esac + # Already single-arch (our arch) — nothing to strip. + if [ "$(echo "${archs}" | wc -w)" -le 1 ]; then + continue + fi + echo "Thinning ${f} (${archs} -> ${LIPO_ARCH})" + # lipo writes to a separate file, which loses the original mode + # (notably the +x bit the signing pass relies on), so capture and + # restore the permissions across the swap. + perms=$(stat -f '%Lp' "${f}") + if lipo -thin "${LIPO_ARCH}" "${f}" -output "${f}.thin"; then + chmod "${perms}" "${f}.thin" + mv -f "${f}.thin" "${f}" + else + rm -f "${f}.thin" + echo "WARNING: failed to thin ${f}" >&2 + fi + done < <(find "${BUNDLE_DIR}" -type f) +} + _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 f19ef1431af..564333769ee 100755 --- a/pkg/mac/build.sh +++ b/pkg/mac/build.sh @@ -92,6 +92,7 @@ _build_runtime _create_python_env _build_docs _complete_bundle +_strip_architecture _generate_sbom _codesign_binaries _codesign_bundle