Skip to content

fix(mac): target macOS 14 so the menubar app runs on Sonoma#577

Open
YoungYo wants to merge 2 commits into
getagentseal:mainfrom
YoungYo:fix/menubar-sonoma-compat
Open

fix(mac): target macOS 14 so the menubar app runs on Sonoma#577
YoungYo wants to merge 2 commits into
getagentseal:mainfrom
YoungYo:fix/menubar-sonoma-compat

Conversation

@YoungYo

@YoungYo YoungYo commented Jun 30, 2026

Copy link
Copy Markdown

Problem

codeburn menubar fails on macOS 14.x (Sonoma) with:

kLSIncompatibleSystemVersionErr: The app cannot run on the current OS version
error=-10825

The released .app's Mach-O has minos = 15.0, so LaunchServices rejects it on Sonoma — even though Info.plist (LSMinimumSystemVersion = 14.0), the CLI install guard (MIN_MACOS_MAJOR = 14), and mac/README all advertise macOS 14+.

Root cause

mac/Package.swift declared .macOS(.v15). That commit's rationale was NSAttributedString(attachment:), but that initializer is AppKit since macOS 10.0 — the bump was a misdiagnosis. Package.swift was the only place claiming 15; everything else says 14.

There's also a deeper layer: a stock macOS-15-SDK build hard-links /usr/lib/swift/libswift_errno.dylib, which only ships in macOS 15. Even after patching minos, dyld can't resolve it on Sonoma. Building against the macOS 14 SDK avoids that dependency entirely.

Changes

  • Package.swift: .macOS(.v15).macOS(.v14) — the actual fix. Binary minos becomes 14.0, matching every other declaration. No macOS 15-only API is used.
  • Scripts/build-local.sh (new): build on a Sonoma machine. The macOS 14 SDK's SwiftUI lacks the @MainActor annotation the macOS 15 SDK adds to the View protocol, so a plain build hits ~80 actor-isolation errors. The script uses a standalone swift.org Swift 6.x toolchain against the local 14 SDK and adds explicit @MainActor to views in a scratch copy (repo sources stay clean), producing a minos = 14.0 bundle in ~/Applications.
  • README.md: document the Sonoma local-build path.

Verification

Built locally on macOS 14.3 (arm64) via build-local.sh:

app: minos 14.0 / sdk 14.2 / libswift_errno refs: 0
codeburn menubar  →  Ready. ~/Applications/CodeBurnMenubar.app   (exit 0, app running)

The -10825 error is gone and the menubar app launches normally.

🤖 Generated with Claude Code

The packaged app set Package.swift to .macOS(.v15), producing a binary with
minos 15.0 that LaunchServices refuses on macOS 14.x with
kLSIncompatibleSystemVersionErr (-10825) — even though Info.plist, the CLI
install guard (MIN_MACOS_MAJOR=14), and the README all advertise macOS 14+.
The .v15 bump was attributed to NSAttributedString(attachment:), which is
actually AppKit since macOS 10.0, so the floor must not exclude Sonoma.

Separately, a stock macOS-15-SDK CI build hard-links libswift_errno.dylib
(macOS 15 only), which dyld cannot resolve on Sonoma even once minos passes.
Building against the macOS 14 SDK avoids that dependency.

- Package.swift: .macOS(.v15) -> .macOS(.v14)
- Scripts/build-local.sh: build on a Sonoma machine (which only has the
  macOS 14 SDK, lacking the SwiftUI @mainactor inference the macOS 15 SDK
  adds to the View protocol) using a standalone swift.org Swift 6.x toolchain,
  patching @mainactor onto views in a scratch copy so repo sources stay clean.
- README: document the Sonoma local-build path.

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

Copy link
Copy Markdown
Collaborator

Reviewed this in depth, including reproducing the Mach-O layout locally and checking the linker mechanism against Apple's ld64. Net: the diagnosis is correct, and the one-line manifest change is the right fix and is sufficient for the CI-distributed release too, not just local builds. Notes below are about the wording of the description (the mechanism is the deployment target, not the SDK) plus an optional guard to keep this from regressing.

1. Root cause confirmed

  • NSAttributedString(attachment:) (the menubar glyph in CodeBurnApp.swift) is AppKit since macOS 10.0, so the .macOS(.v15) bump in ac35746 ("Bump Package.swift platform to macOS 15 to match NSAttributedString(attachment:) usage") really was a misdiagnosis. There is no macOS 15-only API and no @available(macOS 15) anywhere in the sources.
  • The shipped mac-v0.9.14 binary: vtool -show-build -> minos 15.0 / sdk 15.5; otool -L -> a non-weak (hard) dependency on /usr/lib/swift/libswift_errno.dylib, which only ships on macOS 15+. So the release is genuinely unlaunchable on Sonoma and fails on the two layers described. This is not a one-off: other Swift 6 tools built with minos 15 hit the same Library not loaded: /usr/lib/swift/libswift_errno.dylib on Sonoma (e.g. openai/codex #19641, #18755).

2. The manifest change is sufficient, including for CI (one correction to the description)

The "deeper layer" note says a stock macOS-15-SDK build hard-links libswift_errno.dylib and that you must "build against the macOS 14 SDK" to avoid it. That part is not quite accurate: the determining variable is the deployment target, not the SDK.

I built the target two ways against a newer SDK (macOS 26.5 SDK, Swift 6.3.2, no source patches), the same "newer SDK than the floor" situation CI is in:

.macOS(.v15):  minos 15.0   otool -L -> hard link to libswift_errno.dylib
                            (+ libswift_math/_signal/_stdio/_time/_sys_time/_unistd)
.macOS(.v14):  minos 14.0   otool -L -> that whole errno/math/signal/stdio/time
                            cluster is GONE. Only libswift_Concurrency remains
                            (which back-deploys fine).

The mechanism is $ld$previous: the SDK's .tbd stubs for these split dylibs carry $ld$previous metadata, so for a lower deployment-target range ld64 redirects the symbols to the previous dylib and omits the load command entirely (not a weak link, an omission). That is why lowering minos to 14 drops the dependency even against a 15/26 SDK.

CI runs on macos-latest, which is currently mid-migration from macOS 15 to macOS 26, so a run lands on Xcode 16.4 / SDK 15.5 or Xcode 26.5 / SDK 26.5. Both are SDK >= 15, where SwiftUI's View is already @MainActor, so the StrictConcurrency upcoming feature stays satisfied without any patch and the build stays green. Result: after this PR the CI universal binary comes out minos = 14.0 with no libswift_errno dependency and launches on Sonoma. The Package.swift change fixes the downloaded release, not only a local build.

Worth noting the inverse: pinning runs-on: macos-14 would not be a valid alternative. That runner defaults to Xcode 15.4 / Swift 5.10 (incompatible with swift-tools-version: 6.0 + StrictConcurrency), and its Xcode 16 variants ship the macOS 15 SDK anyway. Leaving CI untouched, as this PR does, is the correct call.

3. Implication for build-local.sh

Since the manifest change already fixes the CI artifact, the premise behind build-local.sh ("a stock CI build links the macOS-15-only libswift_errno.dylib") is weaker than stated. The script is still genuinely useful for the narrow case of building on a Sonoma machine with only the Command Line Tools (the macOS 14 SDK), where the @MainActor injection is required because the 14 SDK's View protocol is not annotated. Worth reframing the README and the script header so they do not imply the macOS 14 SDK is what drops libswift_errno. The deployment target does that; the 14 SDK is only what forces the @MainActor workaround.

4. Minor notes on build-local.sh (dev-only, low severity)

  • It builds arm64 only, while CI ships universal. An Intel Sonoma user is not served by this path.
  • The perl that injects @MainActor matches struct ... : ... View ... { on a single line. A multi-line struct declaration, or a conformance added via extension X: View, would be missed and would reintroduce the actor-isolation errors. Fine against the current sources, brittle as they evolve.
  • SDKROOT comes from xcrun --sdk macosx --show-sdk-path, which returns the macOS 15 SDK if an Xcode 16 is selected, defeating the 14-SDK intent. An explicit assert on xcrun --show-sdk-version would make that fail loudly instead of silently.
  • pkill -f "CodeBurnMenubar" can match unrelated processes; matching the bundle id or an exact path is safer.

5. Suggested guard (optional, high value)

This class of failure is a binary-layout regression that is invisible in code review, which is exactly how ac35746 slipped in. A cheap post-build assertion in package-app.sh would lock the fix in:

  • fail if vtool -show-build reports minos other than 14.0
  • fail if otool -L still references libswift_errno.dylib

Any future accidental deployment-target bump is then caught in CI rather than in a user's crash report.

Summary

The fix is correct and, contrary to what the description implies, the Package.swift change alone also fixes the CI-distributed release on Sonoma (deployment target, via $ld$previous, is what drops libswift_errno, not the SDK). The build-local.sh rationale and README wording could be tightened, and a minos / libswift_errno assertion in package-app.sh would prevent a recurrence. Nice catch on the misdiagnosed version bump.

…eployment-target wording, universal build-local.sh, CI minos guard

- README.md / build-local.sh: the -10825 fix is the Package.swift deployment
  target, not the SDK used to build (ld64's $ld$previous drops
  libswift_errno.dylib based on minos, so it already applies to the
  CI-distributed release too). build-local.sh exists only for building on a
  Sonoma machine with just the Command Line Tools.
- build-local.sh: build arm64 and x86_64 separately and lipo them into a
  universal binary (`--arch arm64 --arch x86_64` together needs xcbuild,
  which CLT doesn't ship); assert the active SDK is actually 14.x instead of
  silently trusting `xcrun`; use `pkill -x` instead of `-f` to avoid matching
  unrelated processes; broaden the @mainactor patch regex to slurp mode so it
  also covers multi-line struct headers and `extension X: View`.
- package-app.sh: fail the build if the packaged binary's minos isn't 14.0
  for every arch slice, or if it links libswift_errno.dylib, so a future
  deployment-target regression is caught in CI instead of a user's crash report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@YoungYo

YoungYo commented Jul 1, 2026

Copy link
Copy Markdown
Author

Thanks for the deep dive — the $ld$previous explanation is genuinely useful. Addressed all of it in afe582e:

1. Wording fix — you're right, README.md and build-local.sh's header comment both conflated SDK-vs-deployment-target. Rewrote both to say the -10825/libswift_errno fix comes from Package.swift's .macOS(.v14) deployment target and already covers the CI-distributed release; build-local.sh now documented as existing only for the narrower "Sonoma machine with just Command Line Tools" case, where it's needed for the @MainActor patch, not for dropping libswift_errno.

2. build-local.sh low-severity notes — all four:

  • Universal binary: builds arm64 and x86_64 separately (each stays off xcbuild, confirmed --arch arm64 --arch x86_64 together requires it and CLT doesn't ship it) and merges with lipo -create. Verified locally: both slices come out minos 14.0, no libswift_errno.
  • SDKROOT now asserted to be 14.x via xcrun --show-sdk-version, fails loudly with a fix-it hint instead of silently building against a newer SDK if Xcode is selected.
  • pkill -f "${EXE}"pkill -x "${EXE}" (exact process-name match instead of command-line substring).
  • Perl patch switched to slurp mode so [^{]* spans newlines — now also catches multi-line generic struct headers and extension X: View, not just the single-line shape the current sources happen to use. Verified against synthetic multi-line/extension/App cases.

3. package-app.sh guard — added: fails the build if the packaged binary's minos isn't 14.0 for every arch slice, or if it still links libswift_errno.dylib. Locks the fix in against exactly the kind of regression ac35746 was.

Ran the full build-local.sh pipeline end-to-end on a real Sonoma + Command-Line-Tools-only machine to verify rather than just reasoning about it — universal .app came out clean (minos 14.0 both slices, no libswift_errno, codesign --verify --deep --strict passes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants