Repair iOS Simulator Boot Failures on a Cloud Mac

Repair iOS Simulator Boot Failures on a Cloud Mac

Remote CI can occasionally fail in ways that are difficult to reproduce: Xcode finishes compiling, but the iOS Simulator remains stuck in Booting; or the device appears as Shutdown, yet the installation command still reports a service connection failure. Restarting the cloud Mac often provides a temporary fix, but it also destroys the evidence and does not explain why the problem returns. A more reliable approach is to investigate the runtime, device state, and job concurrency separately.

Identify Which Layer Is Failing

Simulator failures usually occur at one of three layers: the Xcode runtime is unavailable, the device directory is corrupted, or multiple jobs are manipulating the same device. Start by recording the Xcode path and device inventory instead of immediately running erase all.

set -euo pipefail

xcode-select -p
xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices
xcrun simctl list devices unavailable

If a runtime is marked unavailable, first verify that the selected Xcode installation includes the iOS Runtime required by the project. When multiple Xcode versions are installed, explicitly set the developer directory and run the checks again:

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcrun simctl list runtimes

Do not infer a Runtime identifier from the Xcode application name. Automation should read the actual available entries from simctl list -j and reject any runtime with isAvailable=false.

Collect Evidence Before Destructive Cleanup

When a device is stuck, save the JSON inventory and recent logs. Even if the device is rebuilt later, these artifacts help distinguish a CoreSimulator service failure from a terminated boot process or a crash in the app itself.

mkdir -p artifacts/simulator

xcrun simctl list -j > artifacts/simulator/list.json
xcrun simctl diagnose \
  > artifacts/simulator/diagnose.txt 2>&1 || true

xcrun simctl spawn booted log show \
  --last 5m \
  --style compact \
  --predicate 'process == "SpringBoard" OR process == "launchd_sim"' \
  > artifacts/simulator/boot.log 2>&1 || true

If no device is currently booted, it is normal for the final command to fail, but the script should still retain the first two outputs. Sanitize logs before archiving them so that working directories, token arguments, or paths to signing materials do not end up in long-lived CI artifacts.

erase, delete, and directly removing device directories are all destructive operations. Running them before collecting evidence usually produces only “it worked after retrying,” not a root cause that can be fixed.

Create an Isolated Device Set for Every Job

The default device set is stored in the user directory and is accessible to every concurrent job. If one job runs shutdown all, another can lose its device in the middle of testing. The solution is not more retries, but a separate directory for each job.

JOB_KEY="${CI_JOB_ID:-local}-$$"
DEVICE_SET="$PWD/.simulators/$JOB_KEY"
mkdir -p "$DEVICE_SET"

DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"
RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-18-0"

UDID="$(
  xcrun simctl --set "$DEVICE_SET" create \
    "ci-$JOB_KEY" "$DEVICE_TYPE" "$RUNTIME_ID"
)"

xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcrun simctl --set "$DEVICE_SET" bootstatus "$UDID" -b

The device type and Runtime in this example only demonstrate the parameter format. The actual values must be selected from the JSON inventory collected earlier. If device creation fails, do not silently fall back to the default device set, because that would disable isolation precisely when it is most needed.

Clean Up Only the Job’s Own Directory

When the job finishes, shut down its own devices first, then remove the corresponding directory. Never run a global delete all on a shared runner.

xcrun simctl --set "$DEVICE_SET" shutdown all || true
rm -rf "$DEVICE_SET"

Extend Boot Validation to the App Layer

bootstatus -b only confirms that the operating system has finished booting. It does not prove that the app under test can be installed and launched. A complete smoke check should include at least four items:

Check Command or signal Preserve on failure
Device boot bootstatus -b diagnose output and boot logs
App installation simctl install App path and exit code
App launch simctl launch bundle identifier and process output
Test readiness Status file or health probe Deadline and last known state
APP_PATH="$PWD/build/Sample.app"
BUNDLE_ID="com.example.Sample"

xcrun simctl --set "$DEVICE_SET" install "$UDID" "$APP_PATH"
xcrun simctl --set "$DEVICE_SET" launch \
  --console-pty "$UDID" "$BUNDLE_ID"

The pipeline should enforce a clear timeout for the launch phase. If that timeout is reached, collect logs before shutting down the device. A fixed number of unconditional retries only turns a deterministic Runtime error into a longer wait.

Define a Tiered Recovery Order

Recovery should begin with the least disruptive action:

  1. Verify again that DEVELOPER_DIR, the Runtime, and the device type are compatible.
  2. Shut down and restart the device owned by the current job.
  3. Delete the failed device from the same isolated device set and recreate it.
  4. Remove confirmed invalid entries reported by simctl list devices unavailable.
  5. Restart CoreSimulator-related services or the entire machine only when no other jobs are running.

If every incident reaches step five, check whether the runner is still using the shared default device set, whether disk usage is close to capacity, and whether simulator child processes remain after jobs are canceled. Remote jobs on RunnerVM should follow the same principles: confirm the currently available configurations in the console, then make the device-set path part of the job lifecycle instead of treating simulator state as a permanent machine-level resource.

Make Failures Comparable

Finally, record the Xcode version, Runtime identifier, device type, UDID, boot duration, and failure stage in a structured result. Aggregating consecutive failures by these fields reveals whether the issue is concentrated in a particular Runtime, device class, or concurrent job.

A reliable simulator recovery does not end when the interface finally appears. It ends when the commands can be repeated, the app can be installed and launched, failure evidence has been archived, and job-private state has been reclaimed. If the problem returns, investigation can then resume from a known stage instead of starting over with guesswork.

Frequently asked questions

Should I erase or recreate a simulator that is stuck in Booting?

Capture the device list and relevant logs first, then recreate the device inside an isolated device set. Use erase only when the old test state is no longer needed.

Can concurrent CI jobs share the default simulator device set?

They can, but they should not. One job may shut down, erase, or delete a device used by another job. A device set per job provides predictable ownership and cleanup.

Does a successful bootstatus check prove the app is ready?

No. A useful health check also installs the built app, launches its bundle identifier, verifies the exit status, and archives diagnostics when any step fails.

Runner M4

Run your next build on a dedicated physical Mac

Choose a cloud Mac node for each task duration, then manage orders, connection details, and support tickets from the console.

Choose a plan and order