From 956807c5b9ca5f58b81419aad47bc09f79a07306 Mon Sep 17 00:00:00 2001 From: Abdelmalek Ihdene Date: Wed, 16 Sep 2026 14:40:14 -0400 Subject: [PATCH] fix(ios): open Device Hub when Simulator.app is unavailable Xcode 27 removes Simulator.app and replaces it with Device Hub, which lives at Xcode.app/Contents/Applications rather than under the developer directory returned by `xcode-select -p`. `runOnSimulator` hardcodes `${xCodePath}/Applications/Simulator.app`, so `native-run ios` fails on Xcode 27 with: ERR_UNKNOWN: There was an error opening simulator: The file /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app does not exist. Resolve the app at run time instead: keep using Simulator.app when it is present, so behavior is unchanged on Xcode 26 and earlier, and otherwise open Device Hub by its bundle id (com.apple.dt.Devices). The -CurrentDeviceUDID argument is accepted by both. --- src/ios/utils/simulator.ts | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/ios/utils/simulator.ts b/src/ios/utils/simulator.ts index ebe8a4c..a71d254 100644 --- a/src/ios/utils/simulator.ts +++ b/src/ios/utils/simulator.ts @@ -1,3 +1,4 @@ +import { pathExists } from '@ionic/utils-fs'; import { spawnSync } from 'child_process'; // TODO: need cross-spawn for windows? import * as Debug from 'debug'; @@ -9,6 +10,12 @@ import { getXCodePath } from './xcode'; const debug = Debug('native-run:ios:utils:simulator'); +/** + * Xcode 27 removed Simulator.app and replaced it with Device Hub, which lives + * outside the developer directory and so can only be located by bundle id. + */ +const DEVICE_HUB_BUNDLE_ID = 'com.apple.dt.Devices'; + export interface Simulator { availability: '(available)' | '(unavailable)'; isAvailable: boolean; @@ -67,6 +74,25 @@ export async function getSimulators(): Promise { } } +/** + * Returns the `open` arguments used to bring up the simulator UI. + * + * Prefers Simulator.app when it is present so behavior is unchanged on Xcode 26 + * and earlier, and falls back to opening Device Hub by bundle id on Xcode 27+, + * where Simulator.app no longer exists. + */ +async function getSimulatorOpenArgs(): Promise { + const xCodePath = await getXCodePath(); + const simulatorAppPath = `${xCodePath}/Applications/Simulator.app`; + + if (await pathExists(simulatorAppPath)) { + return [simulatorAppPath]; + } + + debug(`Simulator.app not found at ${simulatorAppPath}, falling back to Device Hub`); + return ['-b', DEVICE_HUB_BUNDLE_ID]; +} + export async function runOnSimulator(udid: string, appPath: string, bundleId: string, waitForApp: boolean) { debug(`Booting simulator ${udid}`); const bootResult = spawnSync('xcrun', ['simctl', 'boot', udid], { @@ -83,13 +109,10 @@ export async function runOnSimulator(udid: string, appPath: string, bundleId: st throw new Exception(`There was an error installing app on simulator: ${installResult.stderr}`); } - const xCodePath = await getXCodePath(); debug(`Running simulator ${udid}`); - const openResult = spawnSync( - 'open', - [`${xCodePath}/Applications/Simulator.app`, '--args', '-CurrentDeviceUDID', udid], - { encoding: 'utf8' }, - ); + const openResult = spawnSync('open', [...(await getSimulatorOpenArgs()), '--args', '-CurrentDeviceUDID', udid], { + encoding: 'utf8', + }); if (openResult.status) { throw new Exception(`There was an error opening simulator: ${openResult.stderr}`); }