Skip to content

Add support for DeviceHub.app on Xcode 27#343

Merged
gabrieldonadel merged 8 commits into
expo:mainfrom
GersonRocha9:gersonrocha/support-devicehub-xcode27
Jun 12, 2026
Merged

Add support for DeviceHub.app on Xcode 27#343
gabrieldonadel merged 8 commits into
expo:mainfrom
GersonRocha9:gersonrocha/support-devicehub-xcode27

Conversation

@GersonRocha9

Copy link
Copy Markdown
Contributor

Why

Xcode 27 removes Simulator.app and 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, so validateSystemRequirementsAsync fails with TOOL_CHECK_FAILED ("Can't determine id of Simulator app")
  • open -a Simulator no longer works, so booting a simulator never brings up the simulator UI
  • The AppleScript calls targeting the Simulator app/process (activate window, count processes) silently stop working

DeviceHub facts (verified on Xcode 27.0 beta, build 27A5194q):

Simulator.app (<= Xcode 26) DeviceHub.app (Xcode 27+)
Bundle id com.apple.iphonesimulator / com.apple.CoreSimulator.SimulatorTrampoline com.apple.dt.Devices
Location Xcode.app/Contents/Developer/Applications/ Xcode.app/Contents/Applications/
Process name Simulator DeviceHub

simctl (list/boot/install/launch) is unchanged, so only the UI-app handling needs updating.

How

eas-shared — new simulatorApp.ts resolves 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)
  • assertSimulatorAppInstalledAsync accepts com.apple.dt.Devices as a known id

menu-bar — fixes building the macOS app itself with Xcode 27:

  • Bump pod deployment targets below 12.0 (Xcode 27 minimum) in post_install
  • Patch fmt 11.0.2's FMT_USE_CONSTEVAL in post_install: Xcode 27's clang rejects fmt's consteval format-string checking, and fmt 11.0.2 ignores externally defined FMT_USE_CONSTEVAL/FMT_CONSTEVAL (no #ifndef guard). Format strings are validated at runtime instead of compile time.
  • Replace ReactNativeDelegate.customize(_ rootView: RCTRootView) (failed with "method does not override any method from its superclass" because Xcode 27 resolves RCTRootView to the React module while Expo.swiftmodule expects React_RCTAppDelegate) by making the root view layer transparent directly in applicationDidFinishLaunching.

Test Plan

On macOS with Xcode 27.0 beta (27A5194q) selected via xcode-select:

  • validateSystemRequirementsAsync passes (previously TOOL_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 + activateSimulatorWindowAsync work against the running DeviceHub process
  • Menu bar app builds with yarn macos and launching simulators from the UI opens DeviceHub
  • yarn lint and yarn typecheck pass; menu-bar jest suite passes (19/19). The 6 failing tests in apps/cli (trustedSourcesValidatorMiddleware) fail on main as well and are unrelated
  • Behavior on Xcode <= 26 is unchanged: Simulator still resolves first and all call sites receive the same values as before

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.
Comment thread apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.swift
* the app that matches the active toolchain.
*/
export async function getSimulatorAppInfoAsync(): Promise<SimulatorAppInfo | null> {
if (cachedSimulatorAppInfo === undefined) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🏗️ 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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🐧 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.

Comment on lines +35 to +39
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem to work

Image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't won't need to change anything on AppDelegate as long as we don't compile orbit using Xcode 27 for now

Comment thread CHANGELOG.md Outdated

@gabrieldonadel gabrieldonadel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gabrieldonadel gabrieldonadel merged commit fc712cd into expo:main Jun 12, 2026
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.

2 participants