fix(build): pin the macOS deployment floor for the whole native payload, and enforce it at pack time - #536
Open
EtienneLescot wants to merge 3 commits into
Conversation
Neither build set one, so clang and CMake defaulted to the BUILD MACHINE's SDK
and the shipped binaries inherited whatever macOS compiled them. Measured on
the installed, notarized v1.10.0 arm64 payload: every ffmpeg dylib, every
ggml/whisper/parakeet dylib and whisper-stt-server stamped minos 26.0, inside
an app whose Info.plist declares LSMinimumSystemVersion 12.0.
The minos number is NOT itself the bug. dyld does not refuse a binary — or a
dylib — whose minos exceeds the running OS; both were verified to load here
(a dylib stamped 27.0 loads fine on 26.5, with only a link-time warning). What
the deployment target actually controls is which symbols the toolchain is
willing to import from the OS, and that is where the damage is.
Measured by rebuilding at 12.0 and diffing imports against the shipped
binaries:
ffmpeg identical import sets, 0 symbols either way. The claim that the
compositor addon cannot load on macOS 12 is NOT supported; these
would very likely have loaded.
whisper 9 STRONG (non-weak) undefined refs to libc++ symbols that the
12.0 build does not reference at all:
__ZTVNSt3__117bad_function_callE and friends
__ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE4openEPKcj
vtable/VTT for basic_ifstream / basic_ofstream
Those are version-gated by libc++ itself. The SDK's availability header
declares the bad_function_call key function as
`availability(macos, strict, introduced = 15.4)`, and the cutovers reproduce
exactly on a three-line test program: the fstream symbols start being imported
at a 13.0 target, the bad_function_call ones at 15.4. Below those the
toolchain emits local definitions instead — which is precisely what it does
now.
So the shipped STT helper carries strong references to symbols the toolchain
says do not exist before macOS 15.4, well above the Monterey case that
prompted this. Not observed on an old macOS — no such machine here — but that
annotation is Apple's own statement about where the symbol ships.
After the pin, every rebuilt Mach-O reports minos 12.0, whisper-stt-server
carries 0 of those 9 refs, and it still loads and runs.
This is the macOS twin of the ubuntu-22.04 pin in build-whisper-stt.yml: a
shipped binary's floor decided by the runner rather than by the project.
Refs #515
…floor before-pack.cjs already refuses an incomplete macOS payload. "Complete" is not the same property as "runnable on the macOS we claim", and #515 was the second kind: the payload was whole, and one helper in it was built for macOS 13 while the app advertised 12. Nothing in the pipeline looked. Walks electron/native/bin/darwin-* and fails the pack if any Mach-O declares a minimum macOS above MAC_MIN_OS_FLOOR. Verified both directions against real binaries rather than only fixtures — on this branch, which still carries the original .macOS(.v13): $ node scripts/build-macos-screencapturekit-helper.mjs && node scripts/before-pack.cjs Refusing to package binaries that demand a newer macOS than the 12.0 floor - openscreen-macos-cursor-helper is built for macOS 13.0.0 (floor 12.0) - openscreen-screencapturekit-helper is built for macOS 13.0.0 (floor 12.0) and exit 0 once Package.swift is at .v12. Pointed at the installed, notarized v1.10.0 payload it names all 25 ffmpeg/whisper dylibs at 26.0. Parses LC_BUILD_VERSION (and LC_VERSION_MIN_MACOSX) out of the file rather than shelling out to `vtool`. Same reason neededSymbolVersions() does not use readelf and importedDlls() does not use dumpbin, plus one specific to this hook: it runs for the Windows and Linux packs too, and vtool exists on neither — so a subprocess would have to be skipped on exactly the hosts where skipping is silent. Parsing makes the guard host-independent instead of conditionally absent. Cross-checked against `vtool -show-build` on all 44 Mach-O files across two real payloads: 0 mismatches. Universal binaries take the highest slice, since an x86_64 half built on a newer machine strands Intel users just as thoroughly. The message names the file, its measured floor, the constant, and #515, and states the mechanism — dyld does NOT gate on the minos number; the deployment target decides which symbols get resolved against the OS, and a too-high floor leaves strong references to symbols the target macOS never had. Without that, the obvious "fix" is to raise the constant until it passes. Carries the same parser-sanity assertion as its Linux sibling: reading no deployment target from any Mach-O means the parser broke, not that the payload is unusually clean. Tests synthesise Mach-O headers instead of invoking clang, so they run on the Linux and Windows CI legs as well, and tie MAC_MIN_OS_FLOOR to README.md. That assertion is one-directional on purpose — building for older than advertised is harmless, building for newer is the bug — so it holds both before and after the README correction in the #515 branch. Refs #515
…port
Follows the decision in the parent branch to declare macOS 13 rather than
accommodate 12. The pins and the pack-time guard move with it: 13.0 in
fetch-ffmpeg-macos.mjs, build-whisper-stt.sh and MAC_MIN_OS_FLOOR, all now
described as tracking `mac.minimumSystemVersion` in electron-builder.json5,
which is the number the .app actually tells LaunchServices.
This does NOT weaken the fix — it is the whole point of it. The defect was never
Monterey specifically: the shipped v1.10.0 binaries carried 9 strong undefined
references to libc++ symbols that the toolchain dates to macOS 15.4
(`availability(macos, strict, introduced = 15.4)` on the bad_function_call key
function), so STT was expected to fail to load on Ventura and Sonoma too — the
versions this project still supports, one of which the README recommends.
Rebuilt at 13.0 and re-measured rather than assumed:
whisper-stt-server 15.4-gated (bad_function_call) 9 -> 0
13.0-gated (fstream/filebuf) 7 (correct at this floor)
The second row is the point of pinning rather than merely lowering: at a 13.0
target the toolchain still imports the fstream symbols, which exist on 13.0, and
stops importing the 15.4 ones. Both halves are the deployment target doing its
job.
Every shipped Mach-O now reports 13.0 (compositor_view.node stays at rustc's
11.0, below the floor), whisper-stt-server still loads and runs, the compositor
addon links the rebuilt ffmpeg, and `node scripts/before-pack.cjs` exits 0 on the
complete payload.
before-pack.test.mjs now asserts MAC_MIN_OS_FLOOR EQUALS the declared
minimumSystemVersion, not merely that it is no higher: a pack-time guard looser
than the app's own declaration would wave through exactly the binaries
LaunchServices then refuses to run. Its fixtures are derived from the floor
instead of hardcoding versions — the previous literals silently turned from
offenders into compliant binaries when the floor moved, so the guard's own tests
stopped testing it.
Refs #515
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.
Stacked on #527 — base is that PR's branch, not
main, because the floor it declares (mac.minimumSystemVersion: "13.0") is the number this PR pins everything else to. Review or merge #527 first; GitHub will retarget this tomainautomatically once it lands.What was wrong
scripts/fetch-ffmpeg-macos.mjsandscripts/build-whisper-stt.shset no macOS deployment target, so clang and CMake defaulted to the build machine's SDK. Measured on the installed, notarized v1.10.0 arm64 payload:minoscompositor_view.node(rustc default)whisper-stt-serverNot a stable mistake — a floor that moves whenever GitHub rolls
macos-latest(CI measured ~15.x, this machine 26.0).The mechanism is not the number
dyld does not refuse a binary, or a dylib, whose
minosexceeds the running OS. Both were verified here: a binary stampedminos 99.0execs fine, and a dylib stamped 27.0 loads on 26.5 with only a link-time warning. Anyone "fixing" this by reasoning about the version number will fix the wrong thing.The deployment target decides which symbols the linker resolves against the OS instead of emitting locally. Rebuilding at a pinned floor and diffing imports against the shipped binaries:
__ZTVNSt3__117bad_function_callEand friends,basic_filebuf::open, vtables/VTT forbasic_ifstream/basic_ofstream.Those are version-gated by libc++ itself. The SDK's availability header declares the
bad_function_callkey function asavailability(macos, strict, introduced = 15.4), and the cutovers reproduce exactly on a three-line test program:bad_function_callvtablefilebuf::openSo the shipped STT helper strongly references symbols the toolchain says do not exist before macOS 15.4 — well above the Monterey report that started this, and covering Ventura and Sonoma, one of which the README recommends.
After pinning to 13.0
The second row is why this is a pin and not just a lower number: at a 13.0 target the toolchain still imports the fstream symbols, which exist on 13.0, and stops importing the 15.4 ones. Both halves are the deployment target doing its job.
Every shipped Mach-O now reports 13.0 (
compositor_view.nodestays at 11.0, below the floor),whisper-stt-serverloads and runs, and the compositor addon links the rebuilt ffmpeg.The pack-time guard
before-pack.cjsalready refuses an incomplete macOS payload. "Complete" is not the same property as "runnable on the macOS we claim", and #515 was the second kind. It now also walkselectron/native/bin/darwin-*and fails the pack on any Mach-O aboveMAC_MIN_OS_FLOOR.Verified in both directions against real binaries, not only fixtures — on the parent branch before its
Package.swiftfix:and exit 0 on the corrected payload. Pointed at the installed v1.10.0 payload it names all 25 dylibs at 26.0.
Parses
LC_BUILD_VERSIONout of the file rather than shelling out tovtool. Same reasonneededSymbolVersions()avoidsreadelfandimportedDlls()avoidsdumpbin, plus one specific to this hook: it runs for the Windows and Linux packs too, andvtoolexists on neither — a subprocess would have to be skipped on exactly the hosts where skipping is silent. Cross-checked againstvtool -show-buildon all 44 Mach-O files across two real payloads: 0 mismatches. Universal binaries take the highest slice, since an x86_64 half built on a newer machine strands Intel users just as thoroughly.The failure message names the file, its measured floor, the constant, and #515, and states the mechanism above — without it, the obvious "fix" is to raise the constant until it passes.
Notes for review
MAC_MIN_OS_FLOORequals the declaredminimumSystemVersion— a pack-time guard looser than the app's own declaration would wave through exactly the binaries LaunchServices then refuses to run.introduced = 15.4annotation plus the references being strong rather than weak. Strong inference, not an observation — worth one run on a Ventura or Sonoma box before this is described as user-facing in a changelog.