Skip to content

iOS: make the device set configurable so Xcode tools can target a leased simulator #962

Closed
1 of 1 issue completed

Description

@V3RON

Request: #946

Problem

Simlock's iOS simulators live in their own CoreSimulator device set. On Xcode 26 and older, no Xcode tool can target a simulator there by its UDID: xcodebuild -destination "platform=iOS Simulator,id=<udid>", react-native run-ios --udid, Expo run:ios, Detox and Maestro all read Xcode's own set. The only switch is a per-user Xcode default, which leaks into every xcodebuild run of that user and hides their own simulators while set. Xcode 27 takes the set as a per-command argument (-DVTSimulatorSetLocation=<set>), but only tools that pass extra xcodebuild arguments can use it; Expo run:ios cannot. The docs mention none of this. The only workaround they hint at is to build for generic/platform=iOS Simulator and install with simlock simctl install.

Android does not have this problem: adb, Gradle and the React Native CLI honour ANDROID_ADB_SERVER_PORT, which the lease exports.

Who it is for

Coding agents (agent-device first, then the React Native CLI, Expo, Detox and plain xcodebuild) that hold an iOS lease and need to build and run an app on that simulator. Also the operator who sets up the worker and chooses how much isolation to keep.

Outcome

  • An operator can set drivers.ios.deviceSet to "default". A worker set that way keeps its simulators in Xcode's own set. Any Xcode tool then targets a leased simulator by its UDID, with no extra argument and no detour.
  • With either setting, Simlock's own work (cleanup, the warm pool, nuke, doctor --purge-orphans, reclaim) acts only on simulators this Simlock created. A user's own simulators in Xcode's set, and another Simlock's, are never listed as Simlock's, wiped or deleted by it.
  • An agent's simlock simctl still refuses the commands that change a device's lifecycle (create, erase, delete, shutdown all, runtime delete) under both settings. Under "default" it can list or install on any simulator in Xcode's set by UDID, as it can reach another agent's device today; scoping it to the lease is simlock simctl and simlock adb on a worker run only against the caller's lease, and fill the device in #948.
  • simlock instructions tells an agent which build route its lease needs.
  • The default stays "simlock", so nothing changes for a worker that does not opt in.
  • A new docs page, "Build and run your app", says how to build onto a leased device for each setting and each platform, and what a careless xcrun simctl … all does to leased devices under "default".

How it works for the user

flowchart TD
  A[Operator sets drivers.ios.deviceSet] --> B{Value}
  B -->|simlock, or not set| C[Simulators in Simlock's own set, as today]
  B -->|default| D[Simulators in Xcode's set]
  B -->|anything else| E[iOS off; doctor says invalid-device-set]
  D --> F{Simulators left from the other setting?}
  C --> F
  F -->|yes| G[iOS off; doctor says device-set-changed and the steps]
  F -->|no| H[iOS starts]
  H --> I[Agent leases a simulator]
  I --> J[default: xcodebuild / run-ios --udid works directly]
  I --> K[simlock: Xcode 27 argument, or generic build + simctl install]
Loading

The operator's session, switching to default:

$ simlock config set drivers.ios.deviceSet default
Updated drivers.ios.deviceSet in /Users/alex/.simlock/config.json; takes effect on daemon restart.
$ simlock daemon stop && simlock daemon start
$ simlock doctor
...
Unavailable [ios] device-set-changed: Refusing the ios device set: Simlock's own set at /Users/alex/.simlock/devices/ios still holds 2 simulators (6F1D2A8C-…, 0B7E…). Set drivers.ios.deviceSet back to simlock, run `simlock nuke --delete-devices` and `simlock doctor --purge-orphans`, then switch again. Delete any that remain with `xcrun simctl --set /Users/alex/.simlock/devices/ios delete <udid>`.

After the steps, an agent's session:

$ simlock lease --platform ios --detach
...
$ UDID=6F1D2A8C-3B4E-4F7A-9C2D-8E5B1A7F3C90   # the device id the lease printed
$ xcodebuild -scheme MyApp -destination "platform=iOS Simulator,id=$UDID" build
** BUILD SUCCEEDED **

Today (and under simlock on Xcode 26) the same xcodebuild prints Unable to find a device matching the provided destination specifier.

Examples

drivers.ios.deviceSet drivers.ios.deviceRoot Today After
not set not set own set at ~/.simlock/devices/ios the same
"simlock" not set key ignored, own set own set
"simlock" /Volumes/sims key ignored, own set at /Volumes/sims own set at /Volumes/sims
"default" not set key ignored, own set Xcode's set
"default" /Volumes/sims key ignored iOS off, invalid-device-set
"Default" not set key ignored iOS off, invalid-device-set
"", true, 1 not set key ignored iOS off, invalid-device-set
A simulator in Xcode's set under "default" Simlock lists it? doctor --purge-orphans may delete it?
this Simlock created it, it has a lease yes no (registered)
this Simlock created it, no registry record yes yes (an orphan)
another Simlock (another SIMLOCK_HOME) created it no no
the user's own, any name no no
the user's own, named simlock-1234 no no

What could go wrong

  • A user's own simulator, or another Simlock's, is treated as this Simlock's and wiped or deleted. Unlikely: Simlock claims only devices carrying the mark it writes, with its own instance id, when it creates one. It would be the worst failure.
  • Under "default", a user runs xcrun simctl shutdown all. Leased simulators stop; Simlock reboots them under their leases and agents lose running apps. One erased while stopped ends its lease. The docs say so.
  • An operator flips the key with simulators still around and iOS stays off until they follow the steps doctor prints.

Words used

  • device set: a folder of simulators that simctl and Xcode read, e.g. ~/Library/Developer/CoreSimulator/Devices (Xcode's set) or ~/.simlock/devices/ios (Simlock's own set).
  • Xcode's set: the default device set every Xcode tool reads when given no other.
  • durable mark: the small file Simlock writes into a simulator's folder when it creates the simulator. It will name the Simlock instance; under "default" it is what makes a simulator this Simlock's.
  • instance: one Simlock data folder (SIMLOCK_HOME, ~/.simlock by default) with its own id. One OS user can run more than one, e.g. the slow-lane tests.
  • generic build: xcodebuild -destination "generic/platform=iOS Simulator", a build for no particular simulator, installed afterwards.

Non-goals

Completion conditions

  • With drivers.ios.deviceSet: "default", a leased simulator is listed by a plain xcrun simctl list devices and agent-device drives it (snapshot), on a real Mac.
  • With the key unset, every iOS behaviour is as today.
  • Under "default", a simulator this Simlock did not create is never in its device list, so cleanup, the warm pool, nuke and doctor --purge-orphans never touch it.
  • A bad value, or a switch with simulators left from the other setting, turns iOS off with a reason simlock doctor shows; Android keeps running. (Today a refused driver makes simlock doctor fail with an internal error; iOS simulators can live in Xcode's device set (drivers.ios.deviceSet) #968 fixes that.)
  • The website has a "Build and run your app" page, the configuration, cleanup, device and how-it-works pages describe both settings, and simlock instructions gives the build route for each.

Open questions

Decisions

Shared values

none (one task)

Behaviours

Behaviour (invoker, action, what it sees) Seam Cases Task
The iOS driver, given a deviceSet value, runs every simctl call with or without --set IosSimctlDriver (src/drivers/ios/index.ts), fake process runner key unset; "simlock"; "default" #968
An operator with a bad value or a conflicting deviceRoot gets iOS off with reason invalid-device-set IosSimctlDriver.create "Default"; ""; true; "default" with deviceRoot set #968
An operator who switches with simulators left gets iOS off with reason device-set-changed IosSimctlDriver.create default with devices in Simlock's set; default with Simlock's set empty or absent; simlock with a device in Xcode's set marked by this instance; simlock with only unmarked or other-instance devices there; the message names each UDID and the delete command #968
A new or reclaimed simulator's mark names this instance, under both settings IosSimctlDriver.provision, reclaim "simlock"; "default" #968
Under "default", listManaged reports only simulators whose durable mark names this instance IosSimctlDriver.listManaged this instance's mark; erasable mark gone (still listed, reads erased); no mark; a mark with no instance; another instance's mark; unmarked named simlock-… #968
Under "default", component.list's foreignDevices counts only simulators this instance's mark does not name IosSimctlDriver.listComponents one of this instance's, one unmarked, one another instance's, on the same runtime #968
A lease holder under "default" gets no SIMLOCK_IOS_DEVICE_SET, and the screen-capture program gets none IosSimctlDriver.leaseEnvironment, screen-capture env "simlock"; "default" #968
An agent using simlock simctl under "default" gets plain xcrun simctl and the same refusals IosSimctlDriver.passthrough install <udid> app; erase <udid>; --set /tmp list #968
The agent-device simulator daemon under "default" gets no AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET AgentDeviceSupervisor lease environment with and without SIMLOCK_IOS_DEVICE_SET #968
An agent reading simlock instructions learns the build route: no SIMLOCK_IOS_DEVICE_SET in the grant means target the UDID directly; with it, add -DVTSimulatorSetLocation on Xcode 27+, else the generic build and install src/instructions/index.ts text test the section's required phrases #968
An operator running simlock doctor while iOS is refused sees driver-unavailable with the reason, not an internal error src/daemon/dispatcher.ts doctor.run invalid-device-set; device-set-changed #968
A maintainer who stops a slow-lane run under "default" is left no simulator in their Xcode set scripts/slow-e2e.sh stop a run stopped mid-case #968
An agent on a real Mac under "default" finds its leased simulator in plain xcrun simctl list and agent-device snapshots it slow lane, e2e/slow-agent-device.test.ts iOS #968

Tasks

One task.

Completion conditions: all delivered by #968.

Open work in the same area:

Written by an agent.

Activity

  1. added
    feature:specSpec in progress, business and technical together.
    on Oct 23, 2026
  2. V3RON commented on Oct 23, 2026

    @V3RON
    ContributorAuthor

    Spec updated: business sections, Decisions (ADR 0048, PR #964), Behaviours and the task list.

    Written by an agent.

  3. V3RON commented on Oct 23, 2026

    @V3RON
    ContributorAuthor

    Spec updated: round-0 check fixes (instance-named marks, switch steps, overlaps #147 and #957), and task #968.

    Written by an agent.

  4. added
    feature:plannedSplit into tasks. Never picked up itself.
    and removed
    feature:specSpec in progress, business and technical together.
    on Oct 24, 2026
  5. V3RON commented on Oct 24, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: none
    Parked: none
    Waiting: #968 — on #966 (open, no PR yet)

    Written by an agent.

  6. V3RON commented on Oct 24, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: none
    Parked: #968 — PR #976 needs-hardware: slow lane unavailable (14.8 GiB free, needs 15); stack #977 (#964 → #976) reviewed, not gated
    Decided: #968 — switch steps for a custom deviceRoot → keep it set during cleanup, no registry check (pending the maintainer)

    Written by an agent.

  7. V3RON commented on Oct 26, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: #964 (ADR 0048), #976 (#968)

    Written by an agent.

  8. github-actions commented on Oct 26, 2026

    @github-actions

    Closing: all 1 sub-issues are closed. The PR that closed the last one walks the completion conditions (delivery rule 9); reopen this if one does not hold on main. Flip its ADRs to Accepted if delivery has not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature:plannedSplit into tasks. Never picked up itself.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions