Skip to content

macos: strip non-extern symbols at link time (CMake + mrbind)#6058

Merged
Grantim merged 4 commits into
masterfrom
wheel/strip-cmake-macos
May 7, 2026
Merged

macos: strip non-extern symbols at link time (CMake + mrbind)#6058
Grantim merged 4 commits into
masterfrom
wheel/strip-cmake-macos

Conversation

@Grantim
Copy link
Copy Markdown
Contributor

@Grantim Grantim commented May 6, 2026

Summary

The wheel-size investigation found that on macOS our shipped Mach-O binaries carry their full local symbol table in __LINKEDIT. On mrmeshpy.so that's 86 MB out of 119 MB (72% of the file), with similar ratios on libMRMesh/libMRViewer/libMRVoxels/libMRMcp/libMRIOExtras/etc. Linux strips by default; only macOS doesn't.

This PR adds -Wl,-x (the linker analog of strip -x — drops local symbols, keeps externs that dyld binds) on the macOS link line for non-Debug configs, in two places:

  1. cmake/Modules/CompilerOptions.cmakeadd_link_options($<$<NOT:$<CONFIG:Debug>>:-Wl,-x>) in the existing Apple branch. Covers every CMake target (our libs, libMeshLibC2, in-tree thirdparty wrapped via add_subdirectory).
  2. scripts/mrbind/generate.mk:243 — replaces $(if $(IS_MACOS),,-s) (which skipped strip on macOS because Apple's ld rejects -s) with $(if $(IS_MACOS),-Wl$(comma)-x,-s). Covers mrmeshpy.so and the other mrbind-generated Python bindings, which are linked outside the CMake tree.

This benefits every macOS distribution — wheels, the .pkg installer's MeshLib.framework, the runtimes/osx-*/native/ slice of NuGet, the zip distribution. Replaces #6057 (a wheel-only attempt where the strip placement was ineffective).

Verified savings — mac wheels (both archs)

arm64 x86_64
baseline .whl 87.87 MB 91.25 MB
this PR .whl 77.86 MB 82.85 MB
compressed Δ −10.01 MB (−11.4%) −8.40 MB (−9.2%)

mrmeshpy.so (the dominant binary) before/after:

arm64 baseline arm64 PR x86_64 baseline x86_64 PR
File size 119.52 MB 34.88 124.59 MB 47.29
__TEXT 31.70 31.70 40.09 40.09
__LINKEDIT 86.08 1.44 82.80 5.49
Symbol count 528,811 2,958 394,251 23,858

__TEXT is byte-identical on both archs (sanity check: strip removed no code). Most of __LINKEDIT is mangled symbol names — DEFLATE crushes them ~10×, so the ~80 MB uncompressed save lands as ~9 MB compressed on the wheel.

Per-binary __LINKEDIT (arm64; x86_64 deltas are within ±10% of these):

Binary before MB after MB Δ MB
mrmeshpy.so 86.08 1.44 −84.64
libMRViewer.dylib 3.31 0.63 −2.68
libMRMesh.dylib 3.04 0.60 −2.44
libMRVoxels.dylib 2.76 0.24 −2.53
libMRMcp.dylib 1.01 0.16 −0.85
mrviewerpy.so 0.50 0.06 −0.44
libMRIOExtras.dylib 0.37 0.10 −0.28
Others (mrcuda/mrmeshnumpy/MRPython/MRSymbolMesh) 0.42 0.16 −0.26

Equivalent verification on the .pkg from macos-build-test: every bundled dylib in MeshLib.framework is also stripped (e.g., libMRMesh.dylib __LINKEDIT 3.04 → 0.58 MB). mrmeshpy.so isn't in the .pkg (only the framework C++ libs + companion Python modules are), but the same lever benefits anyone consuming the framework or NuGet.

Test plan

CI under full-ci + test-pip-build produces all relevant artifacts:

  • macos-build-test (arm64, Release) green
  • macos-build-test (x64, Release) green
  • macos-build-test (arm64, Debug) green (Debug skips strip — confirmed via generator expression)
  • test-pip-build / macos-pip-build (arm64) green
  • test-pip-build / macos-pip-build (x86) green
  • all test-pip-build / macos-pip-test (arm64, ...) green (6 Python versions)
  • all test-pip-build / macos-pip-test (x86, ...) green (6 Python versions)
  • mrmeshpy.so __LINKEDIT shrinks ~98% on both archs
  • __TEXT byte-identical on every binary
  • Distributives_macos-arm .pkg framework dylibs also stripped

Risk

Low. -Wl,-x is the standard release-build linker option used by Apple's own SDK and most C++ projects shipping macOS dylibs. Doesn't affect dyld linkage, exception unwinding (CFI tables live in __TEXT's __unwind_info/__eh_frame), __cxa_* lookups (in __DATA_CONST), or pybind11 entry points (PyInit_* is extern). All 12 macos-pip-test jobs (2 archs × 6 Python versions) pass — import meshlib.mrmeshpy and friends all work after dyld linkage. Rollback is a one-line revert per file.

Follow-up

PR-B (separate) will strip third-party deps that we consume (vcpkg-installed libs on Linux+Windows via triplet linker flag, brew-installed dylibs on macOS via CI post-install strip).

🤖 Generated with Claude Code

`-Wl,-x` is the linker's equivalent of `strip -x` — drops local symbols
(most of __LINKEDIT, ~67% of mrmeshpy.so) while keeping externs that
dyld binds. Skipped in Debug. Affects every macOS distribution that
consumes our libs (.whl, .pkg, NuGet, zip), not just wheels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grantim and others added 2 commits May 7, 2026 00:32
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous `-s` skip-on-macos left mrmeshpy.so's 86 MB __LINKEDIT
intact (about 70% of the wheel's macOS bloat). Apple's ld doesn't
accept `-s`, but it does accept `-Wl,-x` (drops local symbols, keeps
externs that dyld binds), the linker analog of `strip -x`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Grantim Grantim added the test-pip-build Build Python wheels (and discard them) label May 7, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Grantim Grantim changed the title cmake: strip non-extern symbols on macOS at link time macos: strip non-extern symbols at link time (CMake + mrbind) May 7, 2026
@Grantim Grantim merged commit 5d5308c into master May 7, 2026
104 checks passed
@Grantim Grantim deleted the wheel/strip-cmake-macos branch May 7, 2026 11:01
Grantim added a commit that referenced this pull request May 7, 2026
Companion to PR #6058. PR-A stripped our own libs at link; this strips
the third-party deps that we bundle into wheels/.pkg/NuGet/etc.

- Linux: VCPKG_LINKER_FLAGS_RELEASE -Wl,-s in x64+arm64 triplets so vcpkg
  ports drop .symtab/.strtab/.debug_* during their own release link
  (~15 MB across the bundled chain). Strip-at-link sidesteps the patchelf
  alignment bug that broke #6057's auditwheel --strip attempt.
- macOS: chmod +w + strip -x every brew dylib in install_brew_requirements.sh
  so delocate-wheel/install_name_tool/.pkg copy already-trimmed binaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grantim added a commit that referenced this pull request May 7, 2026
Companion to PR #6058. PR-A stripped our own libs at link; this strips
the third-party deps that we bundle into wheels/.pkg/NuGet/etc.

- Linux: VCPKG_LINKER_FLAGS_RELEASE -Wl,-s in x64+arm64 triplets so vcpkg
  ports drop .symtab/.strtab/.debug_* during their own release link
  (~15 MB across the bundled chain). Strip-at-link sidesteps the patchelf
  alignment bug that broke #6057's auditwheel --strip attempt.
- macOS: chmod +w + strip -x every brew dylib in install_brew_requirements.sh
  so delocate-wheel/install_name_tool/.pkg copy already-trimmed binaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grantim added a commit that referenced this pull request May 7, 2026
Drops ~43 MB of __LINKEDIT (mostly local symbol names) from each
bundled brew dylib that delocate copies into the wheel — verified
~−2.5 MB compressed / ~−44 MB uncompressed per macOS pip wheel.
libopenvdb.dylib alone shrinks 61.7 MB → 20.7 MB.

Per-file find loop, not `-exec ... +`: strip warns + exits 1 on
signed dylibs, which would abort a batched invocation and leave
later files unstripped. codesign re-signs ad-hoc so dyld doesn't
SIGKILL on load.

Companion to #6058 (our libs) and #6063 (linux third-party).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grantim added a commit that referenced this pull request May 7, 2026
* investigate: brew strip diagnostic on macOS Cellar

PR #6063 v1 ran strip+codesign on brew dylibs but the resulting wheel's
libopenvdb.dylib was byte-identical. Instrument the script to: count and
total-size all Cellar dylibs before/after, sha256 a sample (libopenvdb),
print codesign details, and run strip per-file with stderr captured.
Goal: confirm whether bytes actually change on disk and what strip is
saying when we can't see its output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deps: strip + re-sign brew dylibs after install on macOS

Drops ~43 MB of __LINKEDIT (mostly local symbol names) from each
bundled brew dylib that delocate copies into the wheel — verified
~−2.5 MB compressed / ~−44 MB uncompressed per macOS pip wheel.
libopenvdb.dylib alone shrinks 61.7 MB → 20.7 MB.

Per-file find loop, not `-exec ... +`: strip warns + exits 1 on
signed dylibs, which would abort a batched invocation and leave
later files unstripped. codesign re-signs ad-hoc so dyld doesn't
SIGKILL on load.

Companion to #6058 (our libs) and #6063 (linux third-party).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grantim added a commit that referenced this pull request May 7, 2026
…6063)

* deps: strip third-party libs at install time (vcpkg + brew)

Companion to PR #6058. PR-A stripped our own libs at link; this strips
the third-party deps that we bundle into wheels/.pkg/NuGet/etc.

- Linux: VCPKG_LINKER_FLAGS_RELEASE -Wl,-s in x64+arm64 triplets so vcpkg
  ports drop .symtab/.strtab/.debug_* during their own release link
  (~15 MB across the bundled chain). Strip-at-link sidesteps the patchelf
  alignment bug that broke #6057's auditwheel --strip attempt.
- macOS: chmod +w + strip -x every brew dylib in install_brew_requirements.sh
  so delocate-wheel/install_name_tool/.pkg copy already-trimmed binaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(brew strip): walk Cellar/ instead of lib/

\$BREW_PREFIX/lib contains only symlinks into Cellar/, so the previous
find -type f matched nothing. Walking Cellar reaches the real .dylib
files. Verified separately: linux vcpkg side stripped libopenvdb.so
.strtab from 2.07 MB to 0; mac libopenvdb.dylib __LINKEDIT was unchanged
because of this bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: retrigger CI (prepare-image was cancelled mid-flight)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(brew strip): re-sign dylibs with ad-hoc signature after strip

strip invalidates the existing ad-hoc signature, and dyld then SIGKILLs
any process trying to load the dylib (Apple Silicon enforces this hard).
Symptom on the previous run: python3.10 -m ensurepip got Killed: 9.
Fix: codesign --force --sign - immediately after strip in the same find
chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deps: drop macOS brew strip, scope PR to Linux only

The macOS brew strip ran in CI but produced byte-identical libopenvdb.dylib
in the wheel — strip+codesign on brew bottles appears to be a silent no-op,
likely due to hardened-runtime signature handling. Linux savings (~3 MB
compressed via vcpkg triplet -Wl,-s) are real and worth shipping; macOS
investigation moves to a separate branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants