Fix #753: stop cmux launching under Rosetta on Apple Silicon

Merged
AU
5mo ago
+195-0

Issue

Fixes #753 — on Apple Silicon, cmux.app ends up running under Rosetta (translated), so the app and every child process (login, zsh, and every tool launched in a cmux terminal) run as x86_64. macOS 26 (Tahoe) shows a Rosetta deprecation dialog, and x86_64 child shells silently poison dev toolchains (xcodebuild launched from a cmux terminal builds x86_64).

Diagnosis

This is architecture selection at launch, not a stray Intel binary. Reproduced on the affected machine:

  • Running app process has ps -o flags = 40c4 → the P_TRANSLATED bit (0x4000) is set.
  • A shell spawned inside cmux: uname -m = x86_64, sysctl sysctl.proc_translated = 1.
  • lipo -archs /Applications/cmux.app/Contents/MacOS/cmux → x86_64 arm64 — the binary is universal; LaunchServices is selecting the x86_64 slice.
  • A full bundle rescan found no x86_64-only component; every Mach-O is universal.
  • The lsregister -u <app> && lsregister <app> + restart workaround fixes it locally, confirming the root cause is a LaunchServices architecture preference (seeded by the DMG volume copy and inherited by the /Applications copy), not Mach-O contents.

Fix

Two layers, both of which keep the universal binary so Intel support is preserved (no regression of #293 / #1661 — the app stays x86_64 arm64):

  1. LSArchitecturePriority = (arm64) in Resources/Info.plist — LaunchServices prefers the arm64 slice on Apple Silicon for all future launches.
  2. RosettaNativeRelaunch launch-time self-heal (Sources/App/RosettaNativeRelaunch.swift) — corrects an already mis-pinned install without waiting for a reinstall. At cmuxApp.init() (right after CLI forwarding, before AppKit comes up) it reads sysctl.proc_translated; when translated it re-execs the same bundle executable in place via posix_spawn with POSIX_SPAWN_SETEXEC + posix_spawnattr_setbinpref_np(CPU_TYPE_ARM64), guarded by an env var to prevent relaunch loops. No-op on Intel and on native Apple Silicon launches; on spawn failure it logs and continues (degrades to translated rather than crashing).

Child-process spawn paths were audited: the x86_64 strings in Workspace.swift/ContentView.swift are arch-name mapping only (for downloading helper binaries) — no posix_spawn binary-preference forces x86_64, so children correctly inherit the app's (now native) arch. The release pipeline contains no lsregister double-registration; LSArchitecturePriority covers even the system's automatic DMG-volume registration.

Tests

The launch/LaunchServices behavior itself is not cleanly unit-testable (and per repo policy we do not assert Info.plist/pbxproj contents). The unit-testable part is the self-heal decision logic, covered by a real behavioral test (cmuxTests/RosettaNativeRelaunchTests.swift): relaunch iff translated and not already attempted, including the loop-guard case. Per the regression-test commit policy this is split red→green:

  • Commit 1 adds the test with the loop guard omitted → the guard test fails.
  • Commit 2 adds the guard (+ Info.plist key + call-site wiring) → green.

End-to-end verification is by launching the produced app and checking uname -m = arm64 / sysctl sysctl.proc_translated = 0 with no Rosetta dialog.

Localization

No user-facing strings added — the self-heal is silent (os_log only). Nothing to localize.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


[!NOTE]
Medium Risk
Changes early app launch with in-process re-exec via posix_spawn, but scope is narrow, Intel/universal support is preserved, loop guards and fail-open behavior limit blast radius.

Overview
Fixes Apple Silicon installs that were stuck launching cmux’s x86_64 slice under Rosetta (terminals and child tools then run translated).

Info.plist now declares LSArchitecturePriority with arm64 before x86_64, so LaunchServices prefers native on Apple Silicon while Intel still gets x86_64.

A new RosettaNativeRelaunch path runs at the very start of cmuxApp init, before bundled CLI forwarding: if sysctl.proc_translated says the process is translated and a one-shot env guard is absent, it re-execs the same bundle executable in place via posix_spawn with POSIX_SPAWN_SETEXEC and an arm64 binary preference; on failure it logs and continues translated. RosettaNativeRelaunchTests cover the relaunch vs loop-guard decision only.

Reviewed by Cursor Bugbot for commit 3513f8919a0aa2d568a866673f6e1a92bfcfd480. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Stops cmux from launching under Rosetta on Apple Silicon so terminals run native arm64 and avoid the deprecation dialog. The app remains universal to keep Intel support. Fixes #753.

  • Bug Fixes
    • Set LSArchitecturePriority = (arm64, x86_64) in Info.plist so Apple Silicon prefers arm64 while Intel still launches x86_64.
    • Add early self-heal: if translated, re-exec the same binary with POSIX_SPAWN_SETEXEC and an arm64 preference; guarded by an env var; bails if SETEXEC can’t be set to avoid forking; no-op on Intel; logs and continues on failure; uses the repo’s posix_spawnattr_t init pattern.
    • Run the relaunch check before CLI forwarding (and before AppKit) so forwarded CLI inherits the native arch; corrected doc comments to match.
    • Add unit tests for the relaunch decision, including the loop guard.

Summary by CodeRabbit

  • Bug Fixes

    • Detects translated (Rosetta) launches and attempts to relaunch the app as a native (arm64) process on Apple Silicon when appropriate.
  • Tests

    • Added unit tests for the relaunch decision logic and loop-guard behavior.
  • Chores

    • Declares arm64 as preferred architecture in app metadata and updated project build entries for the relaunch feature.