A Bazel 9 iOS build is failing because the Linux worker can run Bazel but cannot provide the Apple SDK, Xcode toolchain, Simulator, or signing environment.

The fastest solution is to keep portable validation on Linux and route Apple compilation, Simulator tests, signing, archiving, and packaging to a real macOS node. For most medium and large teams, use a Linux-plus-remote-Mac design; for a small team, first prove the complete pipeline on one isolated remote Mac.

Who should read this: Build engineers integrating Bazel 9 into an iOS repository, DevOps engineers maintaining Linux CI capacity, and technical leads deciding whether to add one Mac worker or a mixed execution pool.

Last updated August 29, 2026. Compatibility was checked against the Bazel release documentation, Bazel 9 announcement, rules_apple and rules_swift documentation, and Apple’s Xcode 26 release information.

01

The decision timeline

A useful decision should happen in stages rather than after a failed migration:

  • Today: list every Bazel action that consumes an Apple SDK, Xcode tool, Simulator service, signing identity, provisioning profile, or archive exporter.
  • This week: run a clean checkout on Linux and macOS, inspect action execution data, and record which actions actually execute on each platform.
  • Before expanding the CI pool: verify the supported version combination for Bazel 9, rules_apple, rules_swift, Xcode 26, the selected SDK, and macOS.
  • Before production release: complete signed archive, export, restart recovery, cache reuse, and worker replacement tests.

This week’s recommended action: do not move the entire pipeline based on whether bazel build starts. First prove which actions run where and whether the resulting archive can be signed and exported without a graphical login.

02

Toolchain boundaries

Bazel is an orchestration and build-system layer. It analyzes targets, resolves toolchains, creates actions, and can participate in remote execution. It does not become an Apple SDK, an Xcode installation, or a replacement for Apple’s signing infrastructure.

The main components have different responsibilities:

  • Bazel 9 evaluates the build graph and schedules actions according to declared platforms, toolchains, constraints, and execution properties.
  • rules_apple provides Apple-platform build rules and associated toolchain integration. Its supported Bazel versions must be checked against the specific release used by the repository.
  • rules_swift supplies Swift-oriented build integration. Its documentation and releases must be checked separately rather than assuming that rules_apple and rules_swift share an identical compatibility range.
  • Xcode 26 supplies Apple compilers, SDKs, platform utilities, Simulator components, archive tooling, and signing-related command-line tools.
  • Apple SDKs and provisioning assets define whether a build can target the intended platform and whether the resulting application can be installed, archived, or distributed.

The Bazel release model and the Bazel 9 announcement should be treated as the starting point for lifecycle and migration checks. The exact rules_apple and rules_swift release combination still requires a repository-specific review. A command that exits successfully only proves that one action completed. It does not prove that the full iOS delivery chain is compatible.

Xcode 26 also introduces toolchain and platform requirements that must be checked against the selected macOS worker. Apple’s Xcode 26 release notes should be reviewed whenever the Xcode minor version, SDK, or host operating system changes.

Compatibility gate

A repository should continue migration only when all of these statements can be demonstrated:

  1. The selected Bazel 9 release is supported by the chosen rules_apple and rules_swift versions.
  2. The Xcode 26 version and SDK are supported by the macOS image on the target worker.
  3. The repository’s module dependencies resolve from declared inputs.
  4. The same toolchain selection is visible in CI logs rather than inherited from an engineer’s workstation.
  5. A clean build reaches the intended artifact stage.

If one of these checks is unknown, the correct decision is to hold the migration, upgrade the rules deliberately, or keep the existing build path until a compatibility test is complete. Community issues can identify investigation targets, but they should not be treated as official compatibility confirmation.

03

Platform allocation

The right question is not whether Bazel can run on Linux. It is whether each action’s inputs and tools exist on Linux.

Linux is often suitable for repository checks, formatting, dependency graph validation, static analysis, generic code generation, and tests that do not load Apple frameworks or require Apple platform services. Some Swift or business-logic tests may also remain portable, but that must be proven by their declared dependencies and execution logs.

The following actions normally belong on a compatible macOS worker:

  • Compilation against iOS SDK headers, frameworks, or Swift modules.
  • Final linking of an iOS application or framework.
  • Simulator boot, installation, and UI or integration testing.
  • Code signing and provisioning-profile evaluation.
  • Archive creation and export.
  • Packaging steps that invoke Apple platform tools.
  • Any custom action that reads Xcode or SDK paths from the host.

The Bazel platform and toolchain reference explains how constraints and toolchain selection should describe this boundary. A shell script called build.sh does not reveal its execution platform. An action that invokes xcrun, reads SDKROOT, or resolves an Apple framework does.

A mixed pipeline can therefore look like this:

Linux:
  checkout validation
  formatting and linting
  dependency and module checks
  portable unit tests
  generic code generation

macOS:
  Apple SDK compilation
  final link
  Simulator tests
  signing
  archive and export

Bazel action data should be inspected instead of relying on job labels. A team can begin with verbose execution diagnostics and preserve the relevant action metadata in CI artifacts:

bazel build //App:Release \
  --execution_log_json_file="$RUNNER_TEMP/bazel-execution.json" \
  --profile="$RUNNER_TEMP/bazel.profile"

The repository should replace //App:Release and the temporary path with its own target and artifact directory. The useful evidence is the selected platform, toolchain, command inputs, execution location, cache result, and failure reason.

A simplified result should look like this:

action: AppleCompile
platform: macos_arm64_xcode26
execution: remote-mac-worker-01
cache: miss

action: Lint
platform: linux_x86_64
execution: linux-worker-03
cache: hit

action: SimulatorTest
platform: macos_arm64_xcode26
execution: remote-mac-worker-01
cache: miss

This output is an example of the evidence format, not a claim about a particular repository. The important point is that the team should capture the actual action record and compare it with the intended routing policy.

04

Architecture comparison

Three deployment patterns cover most Bazel 9 iOS teams. The choice depends on queue pressure, signing needs, reproducibility, and the number of Apple-specific actions.

Architecture Linux role Mac role Best fit Main risk Acceptance signal
Single remote Mac Optional validation Entire iOS build and delivery path Small teams proving one complete workflow One node becomes a queue and failure domain Clean signed archive, export, test, and recovery succeed
Linux plus remote Mac Portable checks and tests Apple SDK actions, Simulator, signing, packaging Medium and large teams with mixed workloads Incorrect routing or mismatched toolchains Action logs show deterministic platform placement
Delayed migration Existing CI path No new Mac capacity yet Teams without a verified rules and Xcode combination Continued maintenance cost and no new capacity Migration resumes only after compatibility evidence exists

A single remote Mac is a sensible first stage when the team has one main iOS target, limited parallelism, and no need to isolate many Xcode versions. It also makes troubleshooting easier because the full chain is visible on one worker.

The hybrid model becomes stronger when Linux already handles repository-wide checks and the Apple workload is intermittent. It prevents Apple workers from spending capacity on formatting or generic validation. It also creates a clean place to measure Mac queue time separately from build time.

A delayed migration is not a failure. It is the correct outcome when the repository depends on undeclared host tools, an unsupported rules combination, manual keychain access, or a build script that cannot survive a worker replacement.

05

Reproducibility metrics

A reproducible Bazel 9 iOS build needs more than a pinned Bazel binary. The repository should make the complete toolchain combination visible and reviewable.

The minimum inventory should include:

  • Bazel 9 release and patch level.
  • MODULE.bazel contents and module extensions.
  • rules_apple and rules_swift versions.
  • Xcode 26 version.
  • macOS version and CPU architecture.
  • Selected Apple SDK and deployment target.
  • Compiler and linker flags.
  • Signing identities, provisioning-profile references, and keychain policy.
  • Remote cache and execution endpoint configuration.

Bazel 9’s module-based dependency flow also deserves a separate migration check. The Bazel 9 release information should be reviewed for the repository’s use of legacy dependency entry points, especially where an older project still assumes WORKSPACE behavior. A build that passes only because a developer has preloaded external repositories is not sufficiently sealed for CI.

Common hidden inputs include PATH, shell startup files, host-installed generators, local scripts outside the repository, untracked files, environment variables, and credentials mounted by the CI runner. These dependencies often explain why a build works on a long-lived Mac but fails on a replacement worker.

The strongest evidence is a repeated clean experiment:

  1. Create a clean checkout at a fixed commit.
  2. Use a fresh or explicitly controlled toolchain environment.
  3. Clear the local Bazel output and action cache.
  4. Run the same target on the intended platform.
  5. Compare the action graph, generated files, test results, archive metadata, and failure logs.
  6. Repeat on a replacement worker or after a restart.

A cache hit is not automatically proof of correctness. It can hide an incorrectly declared input if the cache key does not represent the real environment. The Bazel remote execution rules and remote build execution guidance should be used to separate declared action inputs from worker-local assumptions.

06

Cache and execution efficiency

Remote cache, remote execution, and remote Mac access solve different problems:

  • Remote cache reuses completed action outputs.
  • Remote execution sends eligible actions to workers for execution.
  • Remote Mac access provides an interactive or operational connection to a hosted macOS machine, commonly through SSH, VNC, or a web console.

A remote Mac login alone does not make a Bazel action remotely executable. Conversely, a remote cache can improve reuse without moving any action to another worker.

The team should record these metrics for each CI run:

  • Cache hit and miss status by important action group.
  • Queue wait time for Mac workers.
  • Execution time for compilation, linking, Simulator tests, signing, and export.
  • Artifact upload and download time.
  • Failure and retry count.
  • Time required to recover after a worker restart.
  • Number of jobs blocked by signing or Simulator capacity.

No universal speedup percentage should be assumed. A pipeline may be compute-bound, I/O-bound, cache-bound, queue-bound, or limited by the number of available Mac workers. A larger node does not fix a queue caused by one serialized signing stage, and a remote cache does not remove the need for a Mac when an action consumes an Apple SDK.

Expansion should follow observed data. Add Mac capacity when Mac queue time repeatedly delays the critical path, when retries are caused by worker saturation, or when parallel Simulator jobs cannot obtain isolated resources. Keep the pool unchanged when the dominant delay is dependency download, artifact transfer, cache misses caused by unstable inputs, or a routing error.

07

Signing and test acceptance

An unsigned compile is only one checkpoint. It cannot establish that an application can be archived, signed, exported, installed, or delivered.

The acceptance run should contain separate stages:

  1. Build a non-signing target to verify source compilation and linkage.
  2. Run Simulator tests on the selected macOS worker.
  3. Import signing assets into an isolated keychain or equivalent controlled environment.
  4. Build the signed application or archive.
  5. Export the expected distribution artifact.
  6. Inspect signing information and provisioning-profile selection.
  7. Upload logs and artifacts without exposing private keys.
  8. Repeat after cancellation, restart, and worker replacement.

Apple’s code signing and provisioning profile technical note should guide the separation of certificates, profiles, keychains, and application artifacts. The signing environment should not be mixed with the general-purpose build cache.

A non-interactive test is essential. If a worker needs a human to unlock a graphical session, approve a prompt, start the Simulator manually, or repair a keychain after reboot, the pipeline is not production-ready. The recovery test should begin with the worker in a clean state and verify that the CI job can restore the required services using documented automation.

A remote Mac can be connected through SSH for provisioning and diagnostics, while the CI system handles job dispatch and artifact transfer. These are separate control paths. The team should document which system owns credentials, cancellation, retries, and worker health.

08

Node acceptance and routing policy

Before onboarding a remote Mac, the team should publish a small acceptance record:

Bazel: <bazel-9-version>
rules_apple: <rules-apple-version>
rules_swift: <rules-swift-version>
Xcode: <xcode-26-version>
macOS: <macos-version>
Architecture: <arm64-or-other-supported-architecture>
SDK: <sdk-version>
Worker label: <macos-toolchain-label>
Signing mode: <isolated-keychain-policy>

The values are placeholders. They must be filled from the actual worker and repository rather than copied from a sample configuration.

The routing policy should be equally explicit:

  • Route actions with Apple SDK or Xcode inputs to a label matching the installed toolchain.
  • Route portable checks to Linux only when their inputs are declared and their tests do not load Apple platform services.
  • Prevent a generic Mac label from accepting jobs that require a different Xcode or SDK.
  • Keep signing actions isolated from unrelated tenants and general cache data.
  • Reject a worker when its toolchain fingerprint differs from the declared CI image.
  • Retry on a replacement worker only after preserving the original failure logs.

Teams preparing a first remote node can use a remote Mac build-node deployment and acceptance guide as the operational starting point. The guide should be adapted to the organization’s own CI controller, credentials, network policy, and Bazel execution layer.

For larger workloads, a remote Mac CI capacity planning approach is more useful than choosing capacity from a nominal specification. The relevant input is the measured Apple-action queue, concurrency, cache behavior, and recovery record.

If multiple Xcode 26 toolchains must coexist, the team should also define an isolation plan before adding workers. A machine that silently changes DEVELOPER_DIR or SDK selection between jobs can produce artifacts that are difficult to reproduce.

09

Current setup versus a remote Mac

A Linux-only CI design is inexpensive for portable validation, but it leaves the final iOS path unproven. A locally shared Mac can provide the missing toolchain, yet it introduces contention, manual state, workstation sleep, credential coupling, and recovery problems. A virtualized or improvised macOS environment can add licensing, hardware compatibility, device access, and toolchain maintenance uncertainty.

For a team that needs a temporary Apple build target, a dedicated remote Mac from NodeMini is often easier to isolate than changing the Linux fleet or purchasing another physical Mac immediately. The useful test is not a marketing claim: deploy the actual Bazel project, measure action placement, run the signed archive path, test cache behavior, and restart the node. Those records can then determine whether one rented Mac is sufficient or whether the hybrid pool needs additional workers.

Teams that already know the required region can review the available remote Mac options, but the architecture decision should remain tied to the acceptance evidence rather than a product label.

10

Frequently asked questions

Xcode and Bazel 9

An iOS target that consumes Apple SDKs, runs Simulator tests, signs an archive, or exports a distributable package still needs a compatible Xcode installation on macOS. Bazel coordinates the actions; it does not replace Apple’s SDKs or signing tools. Linux can validate portable portions, but it cannot certify the complete delivery path.

Linux CI boundaries

Bazel 9 can run in a Linux-based CI system, but that does not make every iOS action Linux-compatible. Linting, dependency checks, generic generation, and selected portable tests may remain there. Apple SDK compilation, linking, Simulator execution, signing, archiving, and export should be routed to macOS when the action requires Apple tooling.

Mac-only action routing

The safest routing method uses action metadata, platform constraints, toolchain resolution, and execution logs. Typical Mac-only work includes SDK-backed compilation, final linking, Simulator tests, signing, archive generation, and IPA export. A script name is not enough evidence because a seemingly generic script may call xcrun, read SDK paths, or invoke Xcode tools.

Remote Mac integration

A remote Mac should join the mixed cluster as a labeled worker or execution target. Its labels must identify architecture, macOS, Xcode, SDK, signing capability, and isolation policy. The Linux controller can retain portable actions while the Mac handles Apple-specific work. Before production use, test checkout, cache access, cancellation, artifact transfer, reboot recovery, and replacement.

Production acceptance

A node is not accepted after an unsigned build alone. The acceptance record should include the supported Bazel and rules versions, Xcode and SDK fingerprint, clean-checkout result, Simulator test result, signed archive, exported artifact, signing inspection, cache behavior, restart recovery, and retry behavior. Any manual graphical login or hidden host dependency should block production rollout.

The final architecture should be selected from evidence: one remote Mac for a small team proving the full loop, Linux plus remote Mac for teams separating portable and Apple-specific workloads, or a delayed migration when compatibility and recovery remain unverified. If a temporary but isolated Mac is needed to run the real project, NodeMini can provide the test environment; the measured queue, cache, signing, and recovery results should decide the longer-term rental size and worker count.