# Xcode and tool versions
URL: /docs/ios/xcode-and-tools
LLM index: /llms.txt
Description: Control which Xcode release and which Node, Ruby, and CocoaPods versions your sandbox builds use.

# Xcode and developer tool versions

By default, an Xcode sandbox builds with its node's default Xcode and a set of preinstalled developer tools. Xcode is selected per workspace, other tools through [mise](https://mise.jdx.dev). To carry the workspace itself across sandboxes, use [disk snapshots](/docs/ios/snapshots).

## Choose the Xcode version

A sandbox builds with its node's default Xcode unless you select another installed version. The fleet carries one released (GA) Xcode per major and, while Apple is seeding one, one beta. Two selectors cover them:

- A bare major (`27`) binds the newest GA release of that major on the node, never a beta. It follows Apple's point releases on its own. Use it for App Store builds.
- A major.minor (`27.1`) pins that exact version. This is how you pick a beta.

The preference is stored per workspace, the way a version manager remembers a version. A workspace is your git repository or worktree, or a directory assigned with `lim set-workspace-dir`.

### List the installed versions

Print the versions the sandbox can build with:

```bash
lim xcode version list
```

Each row shows the bound marker (`*` for the one in use), the Select value to type, the channel (`ga` or `beta`), and the full version. `--quiet` prints only the Select values, one per line. For example, on a node with three installed versions the Select values are `26`, `27`, and `27.1`, and the last row reads `27.1 beta (27A9269)`. The versions on your node may differ.

To see which Xcode the sandbox has selected, and the workspace preference when the two differ:

```bash
lim xcode version
```

### Pin a version for the workspace

Set the preference with `lim xcode use` or `lim xcode version set`. Both remember the selector and switch the existing sandbox; with no sandbox, they save the preference for the next one. `lim xcode build`, `test`, `rbe`, and new sandboxes all use it:

```bash
lim xcode use xcode@27        # the newest GA Xcode 27
lim xcode version set 27.1    # or pin the 27.1 beta
```

`lim xcode use xcode@27` is equivalent to `lim xcode version set 27` and does not sync or write mise configuration. You can combine it with tool requests, such as `lim xcode use xcode@27 node@24`; only the other tools go into mise. `--cwd` applies only to mise tools, and `--workspace` chooses the Limrun workspace whose preference is set.

### Override the version for one command

Pass `--xcode-version <major|major.minor>` to `lim xcode build`, `create`, `test`, or `rbe`. It overrides the preference for that command and leaves the preference unchanged:

```bash
lim xcode build . --xcode-version 26
```

### Forget the preference

Remove the preference, and the sandbox goes back to the node default:

```bash
lim xcode version unset
```

### Select the version from the SDK

From the TypeScript SDK, read the installed versions and bind one on the sandbox client:

```ts
const { installed } = await xcode.getXcode();
for (const x of installed) console.log(x.version, x.channel); // for example "27.0 ga", "27.1 beta"

await xcode.setXcode('27');    // GA lane: the newest released Xcode 27
await xcode.setXcode('27.1');  // beta lane: this exact version; pick one lane or the other

const { bound } = await xcode.getXcode();
if (bound.channel !== 'ga') throw new Error('App Store uploads need a released Xcode');
```

### How switching works

The sandbox has one Xcode selected at a time, and the workspace preference wins. When a build finds the sandbox on another Xcode, because a colleague switched it or a fresh sandbox came up on the node default, it says so and switches before building. Every build prints the Xcode it runs with, such as `Building with Xcode 27.1 (27A9269)`.

- Switching Xcode invalidates build state produced with the other version, so the next build starts cold. For [disk snapshots](/docs/ios/snapshots), use a separate key per Xcode lane, such as `myapp-27` and `myapp-27.1`: archives are stored per key, and a restore under a different Xcode is wiped.
- The switch is refused while a build, command, sync, or `lim xcode rbe` stack is running. Stop them first (`lim xcode rbe --stop`). `lim xcode use xcode@<version>` keeps the preference in that case, and the next command retries the switch.
- Asking for a version the node does not have fails with the available list, and `lim xcode use xcode@<version>` records no preference for it.
- A major.minor pin lasts until the fleet retires that version. Then `lim xcode build` fails with the daemon's message plus a hint to run `lim xcode version set 27` or `lim xcode version unset`.
- When a beta becomes GA, the release replaces the beta under the same major.minor selector, with one cold build. The bare major follows the newest released Xcode of its major, so it moves to 27.1 as soon as 27.1 is GA on the node.
- Simulators keep running the fleet's default runtime. `lim xcode test` with a non-default Xcode warns and proceeds: XCTest bundles built by the selected Xcode run against the frameworks of the simulator's default runtime, which works for most suites but is not guaranteed.
- Apple rejects App Store uploads built with a beta Xcode. Keep `--upload-to-appstore` on a bare-major pin (`27`, not `27.1`), which only ever binds a released Xcode. In the SDK, gate the publish on `channel === 'ga'`, not on `betaSeed`: Apple's 27.1 seed ships without a seed number.

## Reuse the workspace across sandboxes

A sandbox's workspace, including dependency installs and build products, is lost when the instance terminates. A disk snapshot saves it under a key and restores it into a later sandbox, so the next build starts warm. Create the sandbox with `--snapshot-key`, and delete it with `--wait-snapshot` after a successful build. [Disk snapshots](/docs/ios/snapshots) covers keys, branch fallbacks, and the SDK. Use a separate key per Xcode version, because a restore under a different Xcode is wiped.

## Select developer tools

Select tool versions in the sandbox after your project has synced, then install them and check the result:

```bash
lim xcode use node@24 pnpm@10 ruby@3.3
lim xcode tools install
lim xcode tools
lim xcode build .
```

The three commands do different jobs:

- `lim xcode use` selects tools in an existing sandbox after a project sync. Mise installs a requested version if needed. Explicit sandbox selections take precedence over project requests and apply to later commands.
- `lim xcode tools install` syncs the project and runs `mise install` in the sandbox, installing the selections from synced project files. `--no-sync` installs the current selections without syncing. It never creates or replaces an instance.
- `lim xcode tools` inspects an existing sandbox without syncing or creating an instance. `--sync` uploads local changes first.

All three take `--cwd apps/mobile` for a nested project and `--id <xcode-instance-id>` to choose a sandbox, and all three need an existing sandbox. `lim xcode use xcode@<version>` is the exception: without a sandbox it saves the Xcode preference for the next one.

Each `lim xcode run` and `lim xcode build` resolves the mise environment (`mise env --json`) once at the start. Dependency installation, project generation, builds, and their child processes all use that environment.

### Project requests and compatibility lines

Limrun imports `[tools]` declarations from synced project mise files. Project requests take precedence over the detected package-manager major and Limrun's image defaults, and deeper project files override parent files within the synced directory. Your personal mise configuration on the client is not read or forwarded.

Limrun guarantees compatibility lines, so image updates can deliver newer releases within a line:

| Tool request | Sandbox compatibility line |
|---|---|
| `node = "24.5.0"` | `24` |
| `pnpm = "10.12.1"` | `10` |
| `ruby = "3.3.7"` | `3.3` |
| `python = "3.13.6"`, `go = "1.24.4"` | `3.13`, `1.24` |
| `flutter = "3.44.6"`, `dart = "3.12.2"` | `3.44`, `3.12` |
| `mint = "0.18.0"` | `0.18` |

Ruby, Python, Go, Flutter, Dart, and tools below version 1 keep `major.minor`; other numeric versions keep their major. Client patch pins and `mise.lock` do not freeze the image's release. `latest` keeps its mise meaning and opts out of a fixed line. For any other version string, run mise in the sandbox directly; see [Pin an exact version](#pin-an-exact-version).

### Preinstalled tools

The image provides these tools:

| Tools | Default line |
|---|---|
| Node.js, npm, npx | Node 22 and 24, default 22; npm and npx ship with Node |
| pnpm | 10; 9 and 11 are also preinstalled |
| Yarn | 1; 4 is also preinstalled |
| Bun, bunx | 1 |
| Ruby, RubyGems | 3.3 |
| Bundler | 4 |
| CocoaPods | 1, including the cocoapods-patch plugin |
| CMake, ctest, cpack | 3 |
| Java | JetBrains Runtime 21; Corretto 21 is available for Gradle vendor detection |
| Flutter and its Dart SDK | Flutter 3.44 |
| Mint | 0.18 |
| XcodeGen | 2 |
| xcbeautify | 3 |
| zsign | 1, with Limrun's signing fixes |

Xcode, Apple SDKs, and simulator runtimes are managed separately from mise. Select Xcode with `lim xcode use xcode@27` as described above. Mise itself and Limrun's runtime helpers are part of the image.

### First installation

The first sandbox operation that reads a project mise file runs `mise install` before exporting its environment. This happens once per sandbox, even when later files or tool selections change. Without a project mise file, commands keep using the image defaults.

A failed or cancelled first installation stops that operation. Retry explicitly with `lim xcode tools install`, which also installs later tool selections. Select another Java vendor with a request such as `java@corretto-17`; upstream support for macOS and the selected runtime still applies. The managed CocoaPods resolver uses the selected CocoaPods tool; a project `Gemfile` does not select its gems.

### Environment the build sees

`NODE_BINARY` points to the Node that mise selected, in both runs and builds. For React Native and Expo builds, Limrun rewrites `ios/.xcode.env.local` with that binary and the selected tool `PATH`, so Xcode build phases use the same tools as dependency installation. Do not copy a client Node path into this file. The selected Java and Flutter installations also set `JAVA_HOME` and `FLUTTER_ROOT`.

### Pin an exact version

For an exact version or an explicit update, call mise inside the sandbox:

```bash
lim xcode run -- mise install node@24.5.0
lim xcode run -- mise use --pin node@24.5.0
lim xcode tools
```

`mise install` adds a version without changing the selection. `mise use` installs and records an explicit sandbox override for later operations in `.limrun-runtime-mise.toml`; it takes precedence over project tool requests. `lim xcode use node@24` replaces that override with a Node 24 selection.

A running operation keeps its environment snapshot. The next run or build picks up a version change. Use `mise exec` when a child command in the same operation needs the new version immediately.

### Tools and disk snapshots

Tools preinstalled in the image are never part of a [disk snapshot](/docs/ios/snapshots). Tools you install with mise live under `.limbuild-sandbox/home/.mise/` and are saved when your `--snapshot-paths` cover that directory (the whole workspace by default); mise then selects among compatible image installations and restored user installations.

The publication rule above still applies: a successful `lim xcode build` is required. Dependency installs rerun when the resolved tool versions change, and switching Xcode still invalidates restored build state.

Mise gem wrappers contain absolute paths. After a snapshot restore moves the sandbox home, reinstall an affected user gem explicitly, for example `lim xcode run -- mise install --force bundler`. Image gem wrappers are packaged to support relocation.

## Next steps

<Columns cols={2}>
  <Card title="Build with Xcode" icon="hammer" href="/docs/ios/build-with-xcode">
    Sync, build, and install with the versions you selected.
  </Card>
  <Card title="Sign and distribute" icon="key-round" href="/docs/ios/sign-and-distribute">
    Ship a release build from a GA Xcode to App Store Connect.
  </Card>
</Columns>