Skip to content

fix(ios): open Device Hub when Simulator.app is unavailable - #405

Closed
aihdene-genetec wants to merge 1 commit into
ionic-team:developfrom
aihdene-genetec:fix/xcode-27-device-hub
Closed

aihdene-genetec wants to merge 1 commit into
ionic-team:developfrom
aihdene-genetec:fix/xcode-27-device-hub

Conversation

@aihdene-genetec

Copy link
Copy Markdown

Xcode 27 removes Simulator.app and replaces it with Device Hub. The new app lives at Xcode.app/Contents/Applications/DeviceHub.app, which is outside the developer directory returned by xcode-select -p, and the old Contents/Developer/Applications/ directory is gone entirely.

runOnSimulator() hardcodes ${xCodePath}/Applications/Simulator.app, so native-run ios (and therefore npx cap run ios) fails on Xcode 27:

✖ Deploying App.app to 96D5F2F5-8067-4D50-84B7-BD2E2CA80DE5 - failed!
[error] ERR_UNKNOWN: There was an error opening simulator: The file
        /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app does not exist.

This is distinct from #401, which was the same error message caused by xcode-select pointing at CommandLineTools. Here xcode-select -p is correctly set to /Applications/Xcode.app/Contents/Developer — the target simply no longer exists under any Xcode path, so fixing the Locations setting can't help.

Changes

Resolve the app at run time rather than assuming a fixed path:

  • If Simulator.app exists at the current location, keep using it, so behavior is unchanged on Xcode 26 and earlier.
  • Otherwise open Device Hub by bundle id (com.apple.dt.Devices).

--args -CurrentDeviceUDID <udid> is accepted by both apps, so device targeting is preserved either way. A debug line records which path was taken.

Testing

Tested against a Capacitor project on Xcode 27.0 (27A266a) / macOS 26.6.2, booting an iPad (A16) simulator on the iOS 27.0 runtime:

  • Before: fails with the error above (exit 1).
  • After: full boot → install → open → launch pipeline completes (exit 0), Device Hub opens, and the app installs and launches on the target simulator.
native-run:ios:utils:simulator Running simulator 96D5F2F5-...
native-run:ios:utils:simulator Simulator.app not found at /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app, falling back to Device Hub
native-run:ios:utils:simulator Launching .../App.app on 96D5F2F5-...

npm run build, npm test (40/40), and npm run lint all pass, with no new eslint or Prettier findings.

I don't have an Xcode 26 install available to exercise the Simulator.app branch directly, but that path is byte-for-byte the previous behavior and is gated purely on the file existing.

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.
@pjaudiomv

Copy link
Copy Markdown

Thanks for this! One suggestion on how Device Hub is located. The doc comment says it "lives outside the developer directory and so can only be located by bundle id", but it sits at a fixed position relative to that directory:

$(xcode-select -p)                       → …/Xcode.app/Contents/Developer
$(xcode-select -p)/../Applications/DeviceHub.app → …/Xcode.app/Contents/Applications/DeviceHub.app

Resolving it from getXCodePath() keeps the existing guarantee that the UI opened belongs to the selected Xcode. open -b com.apple.dt.Devices asks Launch Services instead, and with two Xcode 27 installs side by side (stable + beta, which is a common setup) it may open the other one's Device Hub. I only have one Xcode 27 installed so I haven't reproduced that, but the old code was deliberately xcode-select-relative and it seems worth preserving.

The bundle id still makes a good last resort, so something like:

async function getSimulatorOpenArgs(): Promise<string[]> {
  const xCodePath = await getXCodePath();
  const simulatorAppPath = `${xCodePath}/Applications/Simulator.app`;
  if (await pathExists(simulatorAppPath)) {
    return [simulatorAppPath];
  }

  // Xcode 27+: Device Hub sits beside Developer/, not inside it.
  const deviceHubPath = `${xCodePath}/../Applications/DeviceHub.app`;
  if (await pathExists(deviceHubPath)) {
    debug(`Simulator.app not found, opening Device Hub at ${deviceHubPath}`);
    return [deviceHubPath];
  }

  debug(`Device Hub not found at ${deviceHubPath}, falling back to bundle id`);
  return ['-b', DEVICE_HUB_BUNDLE_ID];
}

Tested on macOS 27.0 / Xcode 27.1 beta (27A9269): open "$(xcode-select -p)/../Applications/DeviceHub.app" --args -CurrentDeviceUDID <udid> exits 0 and the freshly launched process receives the argument; open -b com.apple.dt.Devices --args -CurrentDeviceUDID <udid> also exits 0, so the PR as written does fix the problem on a single-Xcode machine. One nuance on "device targeting is preserved": the argument is accepted, but launching Device Hub with it did not boot a shut-down device, so targeting rests on the preceding simctl boot/install/launch calls rather than on the argument. That's fine for this pipeline since simctl boot always runs first.

@luisbytes luisbytes mentioned this pull request Sep 22, 2026
2 of 9 tasks
@luisbytes

Copy link
Copy Markdown
Contributor

@pjaudiomv I tested the approach you suggested, resolving DeviceHub.app relative to getXCodePath() and passing -CurrentDeviceUDID.

With Xcode 27, Device Hub opens successfully, but it does not select the simulator specified by the UDID. It opens the last simulator used instead, and in the expanded Device Hub view.

I also tested devices://device/open?id=, which does select the requested simulator and opens it in the compact simulator view, making it much closer to the previous Simulator.app behavior.

I ended up using this approach in #408.

@pjaudiomv

Copy link
Copy Markdown

@pjaudiomv I tested the approach you suggested, resolving DeviceHub.app relative to getXCodePath() and passing -CurrentDeviceUDID.

With Xcode 27, Device Hub opens successfully, but it does not select the simulator specified by the UDID. It opens the last simulator used instead, and in the expanded Device Hub view.

I also tested devices://device/open?id=, which does select the requested simulator and opens it in the compact simulator view, making it much closer to the previous Simulator.app behavior.

I ended up using this approach in #408.

Nice catch, can confirm — opening DeviceHub.app with -CurrentDeviceUDID ignores it and just brings up the last-used device in the expanded view.

devices://device/open?id=<UDID> works for me on Xcode 27.1 beta — selects the right sim and opens the compact view. #408 looks like the way to go 👍

@aihdene-genetec

Copy link
Copy Markdown
Author

Cool, if there's another PR open that does this better then I shall close mine. Thank you.

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