> ## Documentation Index
> Fetch the complete documentation index at: https://docs.run.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Build with Xcode

> Build Xcode, XcodeGen, React Native, and Expo projects on run.cloud.

`runcloud xcode build` uploads a bounded snapshot of your source, builds it on
macOS, streams a sanitized `xcodebuild` log, and keeps the build record and
artifact available through the authenticated API. You do not need an active
simulator session.

## Build a simulator app

Authenticate once, then run the command from a directory that contains an
Xcode project or workspace:

```bash theme={null}
runcloud login

BUILD_KEY="xcode-$(git rev-parse HEAD)-${GITHUB_RUN_ATTEMPT:-local}"
runcloud xcode build . \
  --project MyApp.xcodeproj \
  --scheme MyApp \
  --sdk iphonesimulator \
  --configuration Debug \
  --idempotency-key "$BUILD_KEY" \
  --output MyApp.tar.gz
```

The default SDK is `iphonesimulator`. Native projects default to `Debug`;
detected React Native and Expo projects default to `Release`. If `--scheme` is
omitted, run.cloud uses the first shared scheme reported by the selected
workspace or project.

The output for an iOS simulator build is a gzip-compressed `.app` bundle. An
unsigned `iphoneos` build returns an app bundle, signed iOS builds return an
IPA, and watchOS builds return a compressed watch app.

## Observe a detached build

Every accepted build has a durable ID. A detached client can reconnect, inspect
state, resume reading the sanitized log, download an artifact, or cancel work:

```bash theme={null}
BUILD_ID="$(runcloud xcode build . --no-wait --json | jq -r '.id')"
runcloud xcode get "$BUILD_ID" --json
runcloud xcode logs "$BUILD_ID" --follow
runcloud xcode artifact "$BUILD_ID" --output app.tar.gz
```

Lifecycle states are `uploading`, `queued`, `running`, `succeeded`, `failed`,
and `cancelled`. Terminal records include an exit code. Failed records also
include the stage, a specific reason extracted from the retained log, a failure
category, and a suggested next action. The artifact endpoint requires the same
organization access as the build; any storage redirect it returns is short
lived.

Use `--idempotency-key` in CI. Repeating the same key with the same source and
options returns the original build. Reusing it with different input fails with
`409` instead of starting ambiguous duplicate work.

## Reuse the output as an Asset

Every verified build output has an immutable, organization-scoped Asset ID in
`artifact.assetId`. The same output can be installed into several sessions
without downloading and uploading it again:

```bash theme={null}
BUILD_ID="$(runcloud xcode build . --no-wait --json | jq -r '.id')"
runcloud xcode logs "$BUILD_ID" --follow
ASSET_ID="$(runcloud xcode get "$BUILD_ID" --json | jq -r '.artifact.assetId')"

LAUNCH_KEY="launch-${BUILD_ID}-iphone16"
runcloud mobile launch \
  --asset "$ASSET_ID" \
  --idempotency-key "$LAUNCH_KEY" \
  --model iphone \
  --json
```

`mobile launch` is one retry-safe operation: it selects the output, creates a
session, installs the app, and returns the launch ID, session ID, current state,
or retained failure. Repeating the same key and request returns the same
operation and never allocates a second simulator. A changed request with the
same key returns `409`.

The Asset ID is the authorization boundary, not a storage URL. run.cloud checks
that the build output and target session belong to the same organization, then
creates a short-lived, read-only download for the selected simulator host. The
Mac that built the app and the host running the simulator do not exchange
credentials and do not presign for each other.

## Rebuild only JavaScript

An Expo simulator build records a fingerprint of its native shell. When only
JavaScript and bundled assets change, reuse that shell:

```bash theme={null}
BASIS_BUILD_ID="xcb_replace_with_a_successful_build"
REBUNDLE_KEY="rebundle-${BASIS_BUILD_ID}-$(git rev-parse HEAD)"

runcloud xcode rebundle . \
  --basis-build "$BASIS_BUILD_ID" \
  --expo-app-dir . \
  --idempotency-key "$REBUNDLE_KEY" \
  --output MyApp-rebundled.tar.gz
```

Rebundles are Release `iphonesimulator` outputs. run.cloud installs dependencies,
calculates the current native fingerprint, and stops with a `compatibility`
failure when native inputs differ from the basis. That failure includes the
action to run a full build and use the new build as the next basis. The original
Asset is never modified; the rebundle creates a new Asset whose
`parentAssetId` identifies its native-shell basis.

## Replay build events

Organization event sequences are ordered and replayable. Save `nextCursor`
only after processing the returned page:

```bash theme={null}
CURSOR=0
runcloud xcode events --after "$CURSOR" --json > build-events.json
CURSOR="$(jq -r '.nextCursor' build-events.json)"
```

Use `--build-id` to select one build. Events contain product-visible state,
stage, output Asset metadata, and sanitized failure details without requiring
access to the build worker.

## Delegate one launch safely

An organization member can mint an opaque capability for a worker that should
read and launch one output, without sharing the organization API key:

```bash theme={null}
LAUNCH_KEY="launch-${BUILD_ID}-worker-1"
CAPABILITY_JSON="$(runcloud capability create \
  --scope mobile.launch \
  --scope xcode.build.read \
  --build "$BUILD_ID" \
  --idempotency-key "$LAUNCH_KEY" \
  --expires-in 900 \
  --json)"

RUN_CLOUD_API_KEY="$(jq -r '.token' <<<"$CAPABILITY_JSON")" \
  runcloud mobile launch \
    --build "$BUILD_ID" \
    --idempotency-key "$LAUNCH_KEY" \
    --json
```

Capability tokens expire after 60 to 3,600 seconds, can be revoked sooner, and
are bound to one organization, explicit resource IDs or idempotency keys, and
named scopes. The secret is returned only by `capability create`; list commands
show metadata, never the token.

## Project preparation

The builder supports these source preparation modes:

* Existing `.xcworkspace` or `.xcodeproj`: select one with `--workspace` or
  `--project`. They are mutually exclusive.
* XcodeGen: pass `--xcodegen-spec`, plus optional `--xcodegen-project` and
  `--xcodegen-project-root` paths.
* React Native or Expo: package installation and native project generation are
  detected from `package.json`; select a monorepo app with `--expo-app-dir`.
* Git-dependent generation: pass `--git-init` to initialize the uploaded
  snapshot before project preparation.

For a Debug React Native build, `--dev-server-url` preserves the launch URL in
the build request. Because this service does not pair a simulator, it does not
open the URL automatically. It is rejected for Release builds.

Dependency preparation recognizes npm, pnpm, Yarn, CocoaPods, Swift Package
Manager through Xcode, and Carthage project files. Source packaging omits VCS
data and common Xcode/dependency output directories. The root `.gitignore` is
honored, while `.xcconfig` files are preserved.

Use repeatable source controls when the defaults do not fit:

```bash theme={null}
runcloud xcode build . \
  --ignore '^Fixtures/' \
  --include '^Generated/Required\.json$' \
  --additional-file "$RUNNER_TEMP/ci.json=Config/ci.json" \
  --additional-file "$RUNNER_TEMP/netrc=~/.netrc"
```

`--ignore` and `--include` accept regular expressions. Additional destinations
must stay inside the uploaded workspace or `~/`; symbolic links are rejected.
The compressed source limit is 100 MiB.

## Build settings

Use a repeatable `--build-setting KEY=VALUE` for
`SWIFT_ACTIVE_COMPILATION_CONDITIONS` or an `APP_CONFIG_` key:

```bash theme={null}
runcloud xcode build . \
  --build-setting SWIFT_ACTIVE_COMPILATION_CONDITIONS=CI \
  --build-setting APP_CONFIG_API_ORIGIN=https://api.example.test
```

Values for `APP_CONFIG_` keys are stored and logged as redacted secret inputs.
Arbitrary Xcode settings are rejected so a request cannot alter the runner or
signing environment.

## Signing and App Store upload

For manual signing, provide one PKCS#12 certificate and one or more provisioning
profiles. Device builds are selected automatically when signing is configured:

```bash theme={null}
runcloud xcode build . \
  --scheme MyApp \
  --certificate-p12 "$RUNNER_TEMP/distribution.p12" \
  --certificate-password "$P12_PASSWORD" \
  --provisioning-profile "$RUNNER_TEMP/MyApp.mobileprovision" \
  --output MyApp.ipa
```

Cloud-managed signing accepts `app-store-connect`, `release-testing`, or
`debugging` as its export method:

```bash theme={null}
runcloud xcode build . \
  --scheme MyApp \
  --signing-method app-store-connect \
  --team-id "$APPLE_TEAM_ID" \
  --asc-key-id "$ASC_KEY_ID" \
  --asc-issuer-id "$ASC_ISSUER_ID" \
  --asc-key "$RUNNER_TEMP/AuthKey.p8" \
  --upload-to-appstore \
  --auto-build-number \
  --output MyApp.ipa
```

`--auto-build-number` finds the highest numeric build number for the app in App
Store Connect and uses the next value. An App Store upload happens after the
IPA is retained by run.cloud, so a failed upload can still leave a downloadable
artifact. Certificate material, private keys, passwords, profiles, and signed
upload URLs are not returned in build records.

Use `--upload <filename>` to choose the retained artifact filename, or
`--signed-upload-url <https-url>` to also upload the output to your own
short-lived destination. Those options are mutually exclusive.

## TypeScript

The public SDK packages the same source snapshot and follows the same lifecycle:

```ts theme={null}
import { writeFile } from "node:fs/promises";
import { Client } from "@run-cloud/sdk";

const cloud = new Client();
const submitted = await cloud.xcode.build(process.cwd(), {
  project: "MyApp.xcodeproj",
  scheme: "MyApp",
  sdk: "iphonesimulator",
  configuration: "Debug",
  idempotencyKey: `xcode-${process.env.GITHUB_SHA ?? "local"}`,
});

const completed = await cloud.xcode.wait(submitted.id, {
  onLog: (text) => process.stdout.write(text),
});

if (completed.status !== "succeeded") {
  throw new Error(`${completed.failure?.reason}\n${completed.failure?.action}`);
}

await writeFile("MyApp.tar.gz", await cloud.xcode.downloadArtifact(completed.id));

const launch = await cloud.mobile.launch(
  { buildId: completed.id },
  { idempotencyKey: `launch-${completed.id}-smoke`, session: { model: "iphone" } },
);
console.log(launch.id, launch.session?.url);
```

For a JavaScript-only change, call
`cloud.xcode.rebundle(process.cwd(), completed.id, options)`. Consume ordered
events with `cloud.xcode.events({ after: cursor })` and persist the returned
`nextCursor` after handling the page.

See the [TypeScript SDK reference](/sdk/latest#xcodebuildsclient) for all option
and response types.

## Intentional differences

The service intentionally keeps the following boundaries around remote builds:

* Each request uses an immutable uploaded snapshot. It does not expose a
  persistent Mac instance, incremental source sync, DerivedData reuse, or a
  customer-managed cache toggle.
* Native-shell reuse is limited to compatible Expo Release simulator builds.
  Native changes require a full build; device signing and App Store uploads do
  not use the rebundle path.
* A successful build remains independent of simulator capacity. Use the
  explicit, idempotent `mobile launch` operation when a simulator should be
  created and the output installed.
* `--no-wait` and `--detach` use the durable build ID for reconnection. There is
  no per-build callback URL option. Consumers replay ordered organization events
  instead of registering an outbound webhook.
* Live command, stdout, and stderr events are combined into one ordered,
  sanitized log that can be resumed by byte offset.
* Every verified output is an immutable organization Asset. `--upload` chooses
  its filename; it does not return a durable bearer download URL or make a build
  output independently deletable.
* Source symlinks are rejected instead of dereferenced or recreated.
* `--asc-wait-timeout` is accepted for command compatibility, but run.cloud
  reports successful upload acceptance and leaves App Store processing
  asynchronous.

These differences do not change the terminal Xcode result: build state, logs,
artifact metadata, output bytes, and actionable failures remain available
outside the client that submitted the request.
