macos_bundle_dylibs: add an .app layout beside the framework one - #6821
Merged
Merged
Conversation
The same bundling problem exists for MeshInspector's .app, and the two tools available for it are both worse than this script. dylibbundler drops absolute LC_RPATH entries while resolving @rpath references, which is where its "can't get path for" warnings come from, and hardcodes two Homebrew prefixes. fixup_bundle copies every prerequisite it can resolve with no way to exempt one, and keys them by file name, so a bundle that references Python through more than one path ends up shipping a second interpreter. This script calls brew --prefix, and SKIP_BASENAME_RE already leaves Python alone. bundle() now takes seed_dirs, dest_dir and exe_dirs, with thin wrappers naming the two layouts, and the rpath is computed relative to where each binary sits rather than picked from two fixed strings - which is what lets Contents/Frameworks/meshlib/*.so get @loader_path/.. . --search-dir covers libraries outside any Homebrew prefix, needed because a .app is assembled from a build tree rather than an install tree. The framework layout is unchanged: it is the default, it passes no search dirs, and _rpath_for reproduces both of its previous rpaths.
codesign on a bundle's main executable signs the enclosing bundle and seals its resources, which fails on a nested item it does not consider code: "code object is not signed at all / In subcomponent: Contents/Frameworks/meshlib/__init__.py". McpGateway signs fine because Info.plist does not name it, so only the main executable trips this. The .app callers already sign the bundle as a unit afterwards, which covers the executables, so skip them here. The framework layout keeps signing its bin/ executables: those are standalone, not bundle members.
A .app is assembled from a build tree, so every binary in it arrives carrying rpaths into that tree - dylibbundler deleted them on the files it was given, which is why the one file it was never given, the pybind11 shim, was the only one left with them. This script only ever added rpaths, so MeshInspectorCode's load command check failed on eleven of them across the executables and the python modules. Delete anything not @-relative in that layout: dyld searches those paths on the user's machine, and they leak build paths. The framework layout is unaffected, both because it is off by default and because an install tree has none to drop.
The mechanics behind them - copyflag, IGNORE_ITEM, get_item_key, which tree a layout is assembled from - are in the PR. What stays is the fact each line exists to prevent: why the rpaths are dropped, why the .app callers sign the bundle themselves, and the layout table, which has no other home.
oitel
approved these changes
Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Teaches
scripts/macos_bundle_dylibs.pya second layout, so the same script can bundle a.appas well asMeshLib.framework.Note
Nothing in this repo uses the new layout. MeshLib builds no
.app— noMACOSX_BUNDLEtarget, noInfo.plist, noContents/MacOS; it installs into a framework whereMeshVieweris a plain executable inbin/. The consumer is MeshInspectorCode, whose.appis bundled bydylibbundlertoday. This lands here because the script lives here, and it is verified byMeshInspector/MeshInspectorCode#7789, which calls it.Why
dylibbundlerdrops absoluteLC_RPATHentries while resolving@rpathreferences, so it warnscan't get path for '@rpath/...'37 times per macOS build on well-formed binaries (auriamg/macdylibbundler#88), and upstream is unmaintained. It also hardcodes/usr/localand/opt/homebrewas its only search prefixes, so it silently bundles nothing on a runner with a custom Homebrew prefix — the reason this script callsbrew --prefixin the first place.The other candidate, CMake's
fixup_bundle, copies every prerequisite it can resolve and keys them by file name, so a bundle referencing Python through more than one path ships a second interpreter that then has to be deleted again. This script'sSKIP_BASENAME_RE = ^(libpython|Python$)already refuses to bundle Python, so that cannot happen.Adopting it downstream also means one bundling tool for the framework and the
.appinstead of two.What changed
_rpath_for(p, dest_dir, exe_dirs)replaces the hardcoded@executable_path/../lib/@loader_path/.choice with a path computed from where the binary sits. This is what makes a nested directory work:Contents/Frameworks/meshlib/*.soneeds@loader_path/.., which neither fixed string could express.bundle()takesseed_dirs,dest_dirandexe_dirs;bundle_framework()andbundle_app()name the two layouts.--search-dir(repeatable) adds directories whose libraries are bundled like Homebrew ones, and are searched during basename fallback. A.appis assembled from a build tree, so its own dylibs and prebuilt thirdparty libraries live outside any Homebrew prefix.sign_exes=Falsefor the.app. Signing a bundle's main executable makescodesignsign the enclosing bundle and seal its resources, which fails on a nested item it does not consider code:MeshInspector: code object is not signed at all / In subcomponent: Contents/Frameworks/meshlib/__init__.py.McpGatewaysigns fine one line earlier, becauseInfo.plistdoes not name it. Those callers sign the bundle as a unit afterwards, which covers the executables anyway.drop_absolute_rpaths=Truefor the.app. The script only ever added rpaths, so binaries arriving from a build tree kept their rpaths into it — eleven of them across the executables and Python modules, which MeshInspectorCode's load-command check failed on.dylibbundlerdeleted these on the files it was handed, which is why the one file it was never handed, the pybind11 shim, was the only one still carrying them._resolve_homebrew_basename→_resolve_by_basename, since it now looks in--search-dirtoo, preferring those over Homebrew.Compatibility
The framework path is unchanged by construction.
--layoutdefaults toframework; both new behaviours are off by default; with no--search-dir,SEARCH_DIRSis empty and every new branch short-circuits.scripts/distribution_apple.sh:34calls the script with a single positional argument, so that invocation is untouched, and macOS CI here runs it._rpath_forreproduces the old strings exactly, which is worth checking rather than asserting:bin/MeshViewer@executable_path/../liblib/libMRMesh.dylib@loader_path/.Contents/MacOS/MeshInspector@executable_path/../FrameworksContents/Frameworks/libMRMesh.dylib@loader_path/.Contents/Frameworks/meshlib/mrmeshpy.so@loader_path/..The first two are the pre-change behaviour; all five verified locally against the function as it appears in this diff.
Measured downstream
From
#7789, which uses this against MeshInspector's.app, against the same.appbuilt withdylibbundlerand withfixup_bundle:dylibbundlerfixup_bundlecan't get path forwarningsSame payload in all three; the libraries differ only in name, since this script copies each under the name its referrer asked for (
libTKBO.7.9.dylib) rather than the realpath, which is its SONAME-drift protection and needs no alias symlinks.Test plan
.pkgjob pass:scripts/distribution_apple.shruns this script, so the framework layout is covered here.applayout is exercised end to end byMeshInspector/MeshInspectorCode#7789— green on both arches, load-command check clean, plugins load and unload in the packaged bundle.Other platforms are disabled; the script only runs during macOS packaging.