Retour aux skills
limrun-inc/skillsVérifier avant exécution

SKILL DETAIL

limrun-xcode-bazel

limrun-inc/skills/limrun-xcode-bazel

This skill enables building Bazel Apple projects on Limrun's remote Mac workers from any environment (Linux, Windows, macOS, VM, container) without local Xcode. Use `lim xcode rbe` to bring up a remote RBE stack, tunnel it to a local port, and generate a `.limrun/` config so that `bazelisk build --config=limrun` runs Apple actions remotely. To build, run `lim xcode rbe` from the Bazel workspace root; it sets up the instance and config and prints the exact build command. Run that command to build. To run on a simulator, attach one with `lim ios create --attach`; subsequent successful builds automatically reinstall and relaunch the app. To upload builds as assets, use `--auto-upload` or `rbe upload`. To tear down, use `lim xcode rbe --stop` and delete the instance.

Installations · 133Voir la source

Installation

npx skills add https://github.com/limrun-inc/skills --skill limrun-xcode-bazel

Fichiers du skill

SKILL.md

Dernière synchronisation · 29 août 2026

references/project-compatibility.md
# Making an arbitrary Bazel Apple project build on Limrun RBE

The generated `--config=limrun` is the whole happy path for an idiomatic
rules_apple / rules_swift project. When a build doesn't go green, it's almost
always one of the patterns below — each is a property of *the project's* Bazel
setup, not a Limrun bug. Symptom → cause → what to do.

## What the generated config / fleet already handle (don't re-derive these)

- **apple_support rule `load`s on Bazel 9** — the `.limrun/BUILD` loads
  `xcode_version` / `available_xcodes` / `xcode_config` from apple_support on
  Bazel 9 (where they're no longer native globals) and omits them on Bazel 8
  (where loading them fails). Driven off the workspace `.bazelversion`.
- **Xcode pinned to the fleet's, both remote and local** — the `.limrun/BUILD`
  declares the fleet's Xcode as both the remote AND local version and sets
  `--xcode_version` to it, so the build uses only the fleet's Xcode and resolves
  cleanly (no apple_support "…not available locally" notice). This is the
  "everything runs remotely" stance — see the local-Apple-actions pattern below
  if a project genuinely needs a Mac-local Apple action.
- **`--strategy=SwiftCompile=remote` / `--strategy=Genrule=remote`** — overrides
  the common local pins (rules_swift's worker, standalone genrules).
- **rules_apple `no-remote` / `no-remote-exec` stripped**
  (`--modify_execution_info=.*=-no-remote,.*=-no-remote-exec`): bundling,
  linking, and signing actions that rules_apple (or a repo's recommended
  `+no-remote` bazelrc) pins local are stripped so they run on the worker.
  Without this they fail on a thin (e.g. Linux) client with `cannot be executed
  with any of the available strategies: [remote]`.
- **A workspace `--remote_cache` is cleared** (`--remote_cache=`): a repo
  pointing `--remote_cache` at a separate backend (BuildBuddy, EngFlow) would
  split the CAS against Limrun's executor and fail with `Lost inputs no longer
  available remotely`. The generated config empties it under `--config=limrun`,
  and the repo's non-limrun builds keep their own cache.
- **Darwin exec platform on non-mac clients**, and `PATH` including `/usr/sbin`.
- **CoreSimulator IB utility devices** (actool/ibtool) — provisioned by the fleet.

## Patterns that require a project-side change

### A pinned Xcode version via a Starlark transition
A repo may lock the Xcode version for a sub-build with a `transition` outputting
`//command_line_option:xcode_version`. **A transition output overrides
`--xcode_version`** — no flag or bazelrc can win against it. If the pinned
version isn't on the fleet, analysis fails in `host_xcodes`. Fix: edit the
transition to drop the `xcode_version` output (or set it to the fleet's).

### Custom `remote_default_exec_properties`
Limrun's worker matches actions by an **exact** platform-property set
(`OSFamily=Darwin`). A project bazelrc that adds extra/mismatched properties
(a lowercase `OSFamily=darwin`, `Arch=arm64`, a `cache_bust=…`) makes the action
platform no longer match any worker → the build stalls or reports no usable
worker. Fix: neutralize those base `build --remote_default_exec_properties=…`
lines for the limrun path (the generated config sets the one the worker needs).

### Per-mnemonic strategy pins beyond Swift/Genrule
If a repo pins other mnemonics local (e.g. `--strategy=ObjcCompile=local`), those
actions run on the client and then need a local Xcode / can't run on a thin
Linux client. Symptom: an action runs `local`/`worker` and fails resolving a
local toolchain. Fix: override the offending mnemonic to `remote`.

### Foreign-build genrules (a host tool compiled from source)
A genrule that downloads a C/C++ tool's source and shells out to `cmake`/`make`
(probing `sysctl` for `-j`, etc.) is **not** sandbox/RBE-friendly: the cage
lacks those tools. Symptom: `sysctl: command not found` / `Exit 127` in a
`[for tool]` genrule once forced remote. Idiomatic host tools built as a normal
`cc_binary` compile fine remotely. Treat the genrule form as a project smell.

### codesign digest algorithm
A target that sets `codesignopts = ["--digest-algorithm=sha1"]` fails under
recent Xcode's codesign: `signing with only SHA1 not allowed`. Use `sha256`.

### A Mac-local Apple action that must use the client's own Xcode
The generated config pins Xcode to the fleet's version for **both** the remote
and local sets, and forces every action remote (`--spawn_strategy=remote`,
`--noremote_local_fallback`). That's deliberate: it makes the build identical on
a Mac and a Linux client and silences the "Xcode not available locally" notice.
The trade-off: if a project *forces* an Apple/Swift action to run on the Mac
client (e.g. a `--strategy=…=local` pin, or a rule that runs `xcode-locator`
against the host), it will look for the fleet's Xcode build locally and fail
(`xcode-locator … not available`) instead of using the Mac's own Xcode. This is
intended — under Limrun RBE everything is meant to run remotely. If you genuinely
need that local action: either drop the local strategy pin so it runs remote
like everything else (preferred), or, if you must keep it local, hand-edit
`.limrun/BUILD` to declare the client's *real* local Xcode as a distinct
`local_xcode` (a different major.minor from the fleet's, so its alias doesn't
collide) under `local_versions` — accepting the "not available locally" notice
back. Re-running `lim xcode rbe` regenerates `.limrun/`, so make the edit durable
elsewhere if you keep it.

## Expected, benign

- **A local sub-build before the remote build** — some example repos consume the
  ruleset via a *built release archive*, so the first thing you see is a local
  build of that archive, then the real target builds remote. Not an error.
- `compatibility_level` and `bazel_features` version mismatch **warnings**.

---
Append new patterns here as they're hit — keep them at the pattern level
(symptom → cause → fix), not as one-off patches for a specific app.
references/verify-remote.md
# Verifying that builds actually run remotely (vs. cache)

A successful `--config=limrun` build is **not** proof that anything ran on the
remote workers. Bazel has two cache layers that make builds green with zero
remote work:

- **`remote cache hit`** — the action's output was fetched from Limrun's cache
  (over the tunnel). The action did **not** re-execute. The cache is shared
  across instances, so the *first* build of an unchanged target often hits cache
  for everything.
- **`action cache hit`** (note: no "remote") — Bazel's **local** action cache.
  Once outputs are materialized locally, repeat builds do nothing and never
  contact the tunnel. This is why a build can "succeed" after the Xcode instance
  and tunnel are gone — there was simply no work to do.

Read the `INFO: N processes:` summary line:
- `… M remote` → M actions **executed** on the workers (this is real RBE).
- `… remote cache hit` → fetched from the remote cache, not executed.
- `… action cache hit` / `internal` → local no-op.

A tell that work did run remotely: action stdout shows worker paths under
`/Users/<worker-user>/.../.rbe-<id>/runner/build/…/root/…`. Local execution
would show your own paths.

## Force and prove remote execution

**Positive — make a cache miss with the tunnel up, then look for `remote`:**
```
# lim xcode rbe running (background by default)
echo '// poke' >> path/to/Some.swift            # or any source edit
bazelisk --digest_function=sha256 build --config=limrun //App
# INFO: N processes: … M remote   ← M > 0 means it executed on the worker
```
Or skip the edit and ignore cached results for one run:
```
bazelisk --digest_function=sha256 build --config=limrun //App --noremote_accept_cached
```

**Decisive negative — make a cache miss with the tunnel DOWN; it must fail:**
```
lim xcode rbe --stop                            # close the tunnel + stop the stack
echo '// poke again' >> path/to/Some.swift
bazelisk --digest_function=sha256 build --config=limrun //App
# expect: connection error to 127.0.0.1:<port> / Remote Execution Failure
```
`--config=limrun` sets `--noremote_local_fallback`, so a cache miss with no
remote cannot silently fall back to local. If this fails, the build genuinely
depends on the remote. If it succeeds, it was a cache/no-op.
SKILL.md
---
name: limrun-xcode-bazel
description: "Build a Bazel-based iOS / macOS / Apple app on Limrun's remote build execution (RBE) instead of a local Mac, and install it on a remote iOS simulator. Use when the project is a Bazel workspace (MODULE.bazel / WORKSPACE) building rules_apple / rules_swift targets and the user wants to `bazel build` it or run it on a simulator, or when a `--config=limrun` build or install misbehaves. To then tap, type, screenshot, or otherwise interact with the running app, use limrun-ios-simulator. For non-Bazel (plain xcodebuild) projects use limrun-xcode instead."
user-invocable: true
effort: high
---

# Bazel iOS builds on Limrun RBE

Build Bazel Apple projects on Limrun's remote Mac workers — from any environment
(Linux, Windows, macOS, VM, container), no local Xcode. `lim xcode rbe` brings up
a remote RBE stack, tunnels it to a local port, and writes a `.limrun/` config so
`bazelisk build --config=limrun` runs Apple actions remotely. Never fall back to
local Xcode or build tools.

## Auth and CLI

Install if needed: `npm install --global lim`. Auth is `lim login` or
`LIM_API_KEY` (may be set outside the project — don't ask for it just because
it's absent). The CLI is the source of truth: the commands in this skill are
verified, but if a flag errors or you need one not shown here, check
`lim xcode rbe --help` instead of guessing.

## Build

1. From the **Bazel workspace root** (has `MODULE.bazel` / `WORKSPACE`), run
   `lim xcode rbe`. It sets up the instance + `.limrun/` config and **prints the
   exact build command**. The tunnel runs in the background (prints a PID);
   `--no-daemon` keeps it foreground.
2. Run the printed command, e.g.
   `bazelisk --digest_function=sha256 build --config=limrun //App`.

Don't hand-write `.limrun/` or the flags — the CLI generates them for the fleet's
Xcode and your OS. Re-run `lim xcode rbe` (after `--stop`) to refresh after a
fleet Xcode upgrade.

To add your own Bazel flags to the limrun path without editing the generated
config, put them in **`user.limrun.bazelrc`** at the workspace root. The
generated config try-imports it last, so your `build:limrun --…` lines win, and
it survives `lim xcode rbe` regeneration (`.limrun/` does not).

## Run on a simulator

`lim xcode rbe` is build-only; attach a simulator when the user wants to see or
run the app. Check or attach (it installs the last build immediately, so no
rebuild is needed):

```bash
lim xcode get             # is a simulator already attached?
lim ios create --attach   # attach one
```

Add `--no-open` when you have no browser to show the user; it skips opening
the stream URL locally and still prints it for sharing.

If the attach output includes a signed stream URL, share it with the user as a
Markdown link, such as `[Live simulator](<signed-stream-url>)`.

With a simulator attached, every successful `--config=limrun` build automatically
reinstalls and relaunches the app, no separate install step:

```bash
bazelisk --digest_function=sha256 build --config=limrun //App
```

Notes:
- **Attach upfront** if you already know you want a sim: `lim xcode rbe --ios`
  (attaches at startup, removed on `--stop`).
- Auto-install happens server-side from the build's events; there is no
  `lim xcode rbe install` subcommand or `--target` flag. To force a reinstall,
  rebuild (a cache-hit rebuild is seconds).
- It fires when the successful invocation produced a single app, so in a
  multi-app workspace build one app target per invocation (`//App`, not
  `//...`); a multi-app build succeeds but installs nothing.

To tap, type, read the element tree, screenshot, or record the running app,
switch to the **`limrun-ios-simulator`** skill.

## Upload builds as assets

To publish a build as a Limrun asset (preview links, installing on other
simulators, CI artifacts), arm uploads at tunnel start or upload one build
after the fact:

```bash
lim xcode rbe --auto-upload preview/my-app --upload-ttl 24h  # every successful build refreshes the asset
lim xcode rbe upload preview/my-app --ttl 24h                # one-shot: the newest successful build
```

- `--auto-upload` holds for the tunnel's lifetime: each successful
  `--config=limrun` build re-uploads the app under that asset name, no
  post-build step. Upload results land in `.limrun/rbe.log`.
- `rbe upload` runs from the workspace root and needs a background tunnel
  plus at least one successful build; it errors otherwise.
- TTLs are Go durations (`24h`, `30m`; `1d` is invalid) and optional; each
  upload without one pushes the asset's expiry to 14 days from that upload.
- To change the `--auto-upload` config of a running tunnel, `--stop` and
  re-run; the CLI refuses a mismatched re-arm instead of silently ignoring it.
- Preview an uploaded app in a browser at
  `https://console.limrun.com/preview?asset=<name>&platform=ios`.

## Teardown

Stop with **`lim xcode rbe --stop`** (~20s to tear the remote stack down) and delete the instance with **`lim xcode delete <id>`**

## Gotchas

- **Always pass `--digest_function=sha256` before `build`** (use the command the
  CLI prints verbatim). The Limrun cache is SHA256-only; Bazel 9 defaults to
  BLAKE3. It's a startup flag, so it can't live in `--config=limrun`. Symptoms:
  build → `Cannot use hash function BLAKE3 with remote cache`; install →
  `non-SHA256 digest … rebuild with --digest_function=sha256`.
- **Run `lim xcode rbe` from the workspace root**, not a subdirectory — it writes
  `.limrun/` there and fails fast otherwise.
- **A green build doesn't prove remote execution** — cache hits (`action cache
  hit` / `remote cache hit`) make builds pass even with the tunnel gone. To force
  and verify real remote execution, see `references/verify-remote.md`.
- **The printed `.ipa` path won't exist on your machine** — the build command
  carries `--remote_download_outputs=minimal`, which keeps the artifact in the
  instance's cache and downloads nothing. Bazel still prints its usual
  `Target //App:App up-to-date: …/App.ipa` line, but that file is **not** on
  disk. This is expected, not a failed build. With a simulator attached, the
  build auto-installs from the artifact's cache digest (read from the build event
  log, not the local file). Only if you genuinely need the `.ipa` locally, drop
  `--remote_download_outputs=minimal` from the build command and Bazel downloads
  the top-level output; auto-install keeps working either way.
- **A fresh instance can fail the first build with `Lost inputs no longer
  available remotely`** (e.g. `… Assets.car`). It's a transient cache eviction
  between instances, not a code error; Bazel prints `Found transient remote cache
  error, retrying the build...` and the retry succeeds. To avoid hitting it
  mid-demo, pre-warm with a full build right after `lim xcode rbe`.
- **`You don't have permission to save … in "CoreSimulator"`** (actool/ibtool) is
  a fleet-side device gap, not your config. Retry; if it persists, report it to
  Limrun.
- **The project's own Bazel settings can fight RBE** (Xcode pinned via a Starlark
  transition, custom `remote_default_exec_properties`, sandbox-hostile genrules).
  These are per-project, not Limrun bugs — walk
  `references/project-compatibility.md` before concluding RBE is broken.