Add support for DeviceHub.app on Xcode 27#343
Conversation
Xcode 27 replaced Simulator.app with DeviceHub.app (bundle id com.apple.dt.Devices, located at Xcode.app/Contents/Applications), which broke opening/activating simulators from Orbit: - 'id of app "Simulator"' no longer resolves, so system requirements validation failed with TOOL_CHECK_FAILED - 'open -a Simulator' and the AppleScript calls targeting the "Simulator" app/process no longer worked Resolve the simulator UI app dynamically (preferring DeviceHub.app when the selected Xcode is >= 27, Simulator.app otherwise) and use the resolved name/bundle id everywhere instead of hardcoding Simulator.
Three build failures when building with Xcode 27:
1. Xcode 27 only supports macOS deployment targets >= 12.0, but several
pods still declare 10.x/11.x. Bump them to 12.0 in post_install.
2. Xcode 27's clang rejects fmt 11.0.2's consteval compile-time
format-string checking ('call to consteval function ... is not a
constant expression' in format-inl.h). fmt 11.0.2 ignores externally
defined FMT_USE_CONSTEVAL/FMT_CONSTEVAL (no #ifndef guard), so patch
the vendored header in post_install to disable consteval. Format
strings are validated at runtime instead of compile time.
3. ReactNativeDelegate.customize(_ rootView: RCTRootView) fails with
'method does not override any method from its superclass' because
Xcode 27 resolves RCTRootView to the 'React' module while the
Expo.swiftmodule expects it in 'React_RCTAppDelegate'. Replace the
override by making the root view layer transparent directly in
applicationDidFinishLaunching.
| * the app that matches the active toolchain. | ||
| */ | ||
| export async function getSimulatorAppInfoAsync(): Promise<SimulatorAppInfo | null> { | ||
| if (cachedSimulatorAppInfo === undefined) { |
There was a problem hiding this comment.
[🏗️ design] cachedSimulatorAppInfo is module-level and only populated once. That's fine for the short-lived CLI, but the menu bar process is long-lived — if a user changes the selected Xcode via xcode-select while the app is running (e.g. switching between Xcode 26 and an Xcode 27 beta), this cache won't pick it up until the app is restarted.
It's probably worth either:
- caching on
(xcodeVersion, name) -> bundleId(a Map keyed by Xcode path/version) so a Xcode switch invalidates implicitly, or - exposing a small
resetSimulatorAppCache()for callers that detect a switch, or - at minimum, a comment acknowledging the limitation.
Also a minor nit: the first two concurrent callers will both run xcode.getXcodeVersionAsync() since the cache is set after the awaits — storing the in-flight Promise instead of the resolved value would dedupe.
|
|
||
| async function resolveAppInfoAsync(name: string): Promise<SimulatorAppInfo | null> { | ||
| try { | ||
| const bundleId = (await osascript.execAsync(`id of app "${name}"`)).trim(); |
There was a problem hiding this comment.
[🐧 nit] Nit: id of app "${name}" interpolates name straight into AppleScript. The current call sites only pass the two constants, so this is safe today. But once getSimulatorAppNameAsync returns derived values, this becomes an injection surface. A name.match(/^[A-Za-z0-9 ]+$/) precondition (or just a comment that callers must pass an allowlisted constant) would make the assumption explicit.
| // Transparent background. Previously done in ReactNativeDelegate.customize(_:), | ||
| // but that override no longer type-checks with Xcode 27, which resolves | ||
| // RCTRootView to the 'React' module instead of 'React_RCTAppDelegate'. | ||
| rootView.wantsLayer = true | ||
| rootView.layer?.backgroundColor = NSColor.clear.cgColor |
There was a problem hiding this comment.
We don't won't need to change anything on AppDelegate as long as we don't compile orbit using Xcode 27 for now
gabrieldonadel
left a comment
There was a problem hiding this comment.
Don't we need to change this as well?

Why
Xcode 27 removes
Simulator.appand replaces it with DeviceHub.app, which breaks Orbit's iOS simulator integration on machines running the Xcode 27 beta:id of app "Simulator"no longer resolves, sovalidateSystemRequirementsAsyncfails withTOOL_CHECK_FAILED("Can't determine id of Simulator app")open -a Simulatorno longer works, so booting a simulator never brings up the simulator UISimulatorapp/process (activate window, count processes) silently stop workingDeviceHub facts (verified on Xcode 27.0 beta, build
27A5194q):com.apple.iphonesimulator/com.apple.CoreSimulator.SimulatorTrampolinecom.apple.dt.DevicesXcode.app/Contents/Developer/Applications/Xcode.app/Contents/Applications/SimulatorDeviceHubsimctl(list/boot/install/launch) is unchanged, so only the UI-app handling needs updating.How
eas-shared— newsimulatorApp.tsresolves which simulator UI app is available via Launch Services (id of app ...), preferring DeviceHub when the selected Xcode is >= 27 and Simulator.app otherwise (keeps multi-Xcode setups on the app matching the active toolchain). The resolved name/bundle id is now used by:openSimulatorAppAsync(open -a <app>)activateSimulatorWindowAsync/isSimulatorAppRunningAsync(AppleScript process name)getSimulatorAppIdAsync(bundle id)assertSimulatorAppInstalledAsyncacceptscom.apple.dt.Devicesas a known idmenu-bar— fixes building the macOS app itself with Xcode 27:post_installFMT_USE_CONSTEVALinpost_install: Xcode 27's clang rejects fmt's consteval format-string checking, and fmt 11.0.2 ignores externally definedFMT_USE_CONSTEVAL/FMT_CONSTEVAL(no#ifndefguard). Format strings are validated at runtime instead of compile time.ReactNativeDelegate.customize(_ rootView: RCTRootView)(failed with "method does not override any method from its superclass" because Xcode 27 resolvesRCTRootViewto theReactmodule whileExpo.swiftmoduleexpectsReact_RCTAppDelegate) by making the root view layer transparent directly inapplicationDidFinishLaunching.Test Plan
On macOS with Xcode 27.0 beta (
27A5194q) selected viaxcode-select:validateSystemRequirementsAsyncpasses (previouslyTOOL_CHECK_FAILED)orbit-cli boot-device --platform ios --id <udid>boots the simulator and opens/activates DeviceHub, with DeviceHub quit beforehand (previously timed out waiting for Simulator.app)ensureSimulatorAppOpenedAsync+activateSimulatorWindowAsyncwork against the running DeviceHub processyarn macosand launching simulators from the UI opens DeviceHubyarn lintandyarn typecheckpass; menu-bar jest suite passes (19/19). The 6 failing tests inapps/cli(trustedSourcesValidatorMiddleware) fail onmainas well and are unrelatedSimulatorstill resolves first and all call sites receive the same values as before