"The production build bundles the pinned libmpv dylib and its required dependency closure, fixes runtime paths, includes license notices, and signs every nested binary before the app."
libmpv Doesn't Come Alone
When you bundle libmpv, you're not bundling one file. libmpv depends on other dylibs, which depend on others, all the way down — the dependency closure. Ship libmpv but miss one thing it transitively needs, and the app dies on launch just as surely as if you'd bundled nothing. Self-contained means the entire closure travels inside the app, not just the library you thought of first.
Getting there is a specific four-step dance, and every step matters:
- Pin exact builds of every dylib, with recorded checksums and license notices — reproducibility and attribution are not optional.
- Copy the whole closure into the bundle's Frameworks directory.
- Rewrite each install name so binaries load their dependencies from inside the bundle (
@rpath), not from a Homebrew path.
- Sign every nested binary first, then the app last, because signatures nest outward.
The whole transitive closure travels, path-fixed and signed inner-to-outer. A dependency you didn't bundle is a launch failure you didn't test. Walk the closure completely, relocate every install name into the bundle, and sign from the inside out.
Here's the dance in shell, including the step most people forget — the proof:
Signing order is not a detail — inner binaries must be signed before the app. Code signatures nest: the app's signature covers its contents, so if you sign the app first and then re-sign a nested dylib, you've invalidated the outer signature. Always sign the deepest binaries first and work outward to the app bundle last. Get the order wrong and Gatekeeper rejects the whole thing.
Prove It by Hiding Your Crutch
The final step is the one that separates hope from proof: temporarily hide the development Homebrew prefix and launch the bundle. If it still runs, the closure genuinely travels; if it dies, you missed a dylib and just found out on your own machine instead of a user's. This 'kick away the crutch and see if it stands' test is the whole point of the track — the installed app must not depend on a developer's cellar, a writable source checkout, or a single unsigned, unbundled library.
The first time I thought Ashen Reel was self-contained, I hid Homebrew to prove it and the app died instantly — a transitive dylib two levels down that I'd never consciously depended on. Humbling. Dad just nodded: "그래서 숨겨보는 거야. 안 숨겨보면 사용자가 대신 숨겨줘." — that's why you hide it; if you don't, the user hides it for you. The proof step isn't ceremony. It's the difference between believing you shipped the whole thing and knowing it.
Exercise
If you ship anything that bundles a native dependency, walk its full transitive closure — not just the top-level library, but everything IT needs. Then find (or build) your version of the 'hide the crutch' test: temporarily remove the development source of that dependency and confirm your artifact still runs. If you can't easily do that, you don't yet know whether your bundle is self-contained or just lucky.
Hint
Two things trip people up: the closure is transitive (the library you bundled pulls in libraries you didn't think about), and signing must go inner-to-outer (sign the deepest binary first, the container last). The 'hide the dev copy and launch' test is the only way to be sure the closure is complete — belief isn't proof here.