Back to Tuist

Xcode cache {#xcode-cache}

server/priv/docs/en/guides/features/cache/xcode-cache.md

4.191.89.9 KB
Original Source

Xcode cache {#xcode-cache}

Tuist provides support for the Xcode compilation cache, which allows teams to share compilation artifacts by leveraging the build system's caching capabilities.

The Xcode cache was introduced in Xcode 26. You might also see it referred to as the Xcode build cache; it reuses compilation artifacts keyed by their inputs, and Tuist's remote cache makes those artifacts shareable across machines.

[!TIP] Combine with the module cache

The Xcode cache and the <.localized_link href="/guides/features/cache/module-cache">module cache</.localized_link> work at different granularity levels and complement each other. The module cache replaces whole modules with prebuilt .xcframeworks before the build runs, while the Xcode cache reuses compilation outputs during the build.

Setup {#setup}

[!WARNING] Requirements

  • A <.localized_link href="/guides/server/accounts-and-projects">Tuist account and project</.localized_link>
  • Xcode 26.0 or later

If you don't already have a Tuist account and project, you can create one by running:

bash
tuist init

Once you have a Tuist.swift file referencing your fullHandle, you can set up the caching for your project by running:

bash
tuist setup cache

This command creates a LaunchAgent to run a local cache service on startup that the Swift build system uses to share compilation artifacts. This command needs to be run once in both your local and CI environments.

To set up the cache on the CI, make sure you are <.localized_link href="/guides/integrations/continuous-integration#authentication">authenticated</.localized_link>.

Configure Xcode Build Settings {#configure-xcode-build-settings}

Add the following build settings to your Xcode project:

COMPILATION_CACHE_ENABLE_CACHING = YES
COMPILATION_CACHE_REMOTE_SERVICE_PATH = $HOME/.local/state/tuist/your_org_your_project.sock
COMPILATION_CACHE_ENABLE_PLUGIN = YES
COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS = YES

Note that COMPILATION_CACHE_REMOTE_SERVICE_PATH and COMPILATION_CACHE_ENABLE_PLUGIN need to be added as user-defined build settings since they're not directly exposed in Xcode's build settings UI:

[!NOTE] Socket Path

The socket path will be displayed when you run tuist setup cache. It's based on your project's full handle with slashes replaced by underscores.

You can also specify these settings when running xcodebuild by adding the following flags, such as:

xcodebuild build -project YourProject.xcodeproj -scheme YourScheme \
    COMPILATION_CACHE_ENABLE_CACHING=YES \
    COMPILATION_CACHE_REMOTE_SERVICE_PATH=$HOME/.local/state/tuist/your_org_your_project.sock \
    COMPILATION_CACHE_ENABLE_PLUGIN=YES \
    COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS=YES

[!NOTE] Generated Projects

Setting the settings manually is not needed if your project is generated by Tuist.

In that case, all you need is to add enableCaching: true to your Tuist.swift file:

swift
import ProjectDescription

let tuist = Tuist(
    fullHandle: "your-org/your-project",
    project: .tuist(
        generationOptions: .options(
            enableCaching: true
        )
    )
)

Cache upload policy {#cache-upload-policy}

By default, the cache service both downloads and uploads artifacts to the remote cache. You can control this with the cache option in your Tuist.swift file to enable read-only mode, where artifacts are downloaded but never uploaded:

swift
import ProjectDescription

let tuist = Tuist(
    fullHandle: "your-org/your-project",
    cache: .cache(
        upload: false
    ),
    project: .tuist(
        generationOptions: .options(
            enableCaching: true
        )
    )
)

A common pattern is to push artifacts only from CI, where builds are reproducible, while keeping local environments read-only. You can achieve this using Environment.isCI, which checks for the CI environment variable set by most CI providers:

swift
import ProjectDescription

let tuist = Tuist(
    fullHandle: "your-org/your-project",
    cache: .cache(
        upload: Environment.isCI
    ),
    project: .tuist(
        generationOptions: .options(
            enableCaching: true
        )
    )
)

With this setup, local builds benefit from cached artifacts without uploading, while CI builds populate the cache for the rest of the team.

Continuous integration {#continuous-integration}

To enable caching in your CI environment, you need to run the same command as in local environments: tuist setup cache.

For authentication, you can use either <.localized_link href="/guides/server/authentication#oidc-tokens">OIDC authentication</.localized_link> (recommended for supported CI providers) or an <.localized_link href="/guides/server/authentication#account-tokens">account token</.localized_link> via the TUIST_TOKEN environment variable.

An example workflow for GitHub Actions using OIDC authentication:

yaml
name: Build

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: jdx/mise-action@v2
      - run: tuist auth login
      - run: tuist setup cache
      - # Your build steps

See the <.localized_link href="/guides/integrations/continuous-integration">Continuous Integration guide</.localized_link> for more examples, including token-based authentication and other CI platforms like Xcode Cloud, CircleCI, Bitrise, and Codemagic.

Troubleshooting {#troubleshooting}

Builds are extremely slow and emit CAS error: deadlineExceeded warnings {#cas-deadline-exceeded}

If your builds take much longer than expected and the Xcode build log is full of warnings like:

Warning: CAS error: deadlineExceeded(connectionError: Optional(connect(descriptor:addr:size:): Connection refused (errno: 61)))
note: cache key query failed

or:

Warning: CAS error: deadlineExceeded(connectionError: Optional(connect(descriptor:addr:size:): No such file or directory (errno: 2)))

then the Xcode cache daemon (the local socket started by tuist setup cache) is not reachable. When the daemon is down, Xcode retries the connection on every compilation cache request rather than failing fast, which can make a build take an hour or more. This retry behavior is implemented inside Xcode and cannot be configured by Tuist, so the only fix is to make sure the daemon is running.

[!WARNING] Xcode cannot be told to fail fast

Tuist only provides the local socket Xcode communicates with. When the socket is unreachable, Xcode decides to keep retrying with a deadline per task, and that behavior is not something we can override. If you are not actively using the Xcode cache, disable it rather than leaving the build settings enabled without a running daemon.

If you are not using the Xcode cache, remove the COMPILATION_CACHE_* build settings and set enableCaching: false (or leave it unset) in your Tuist.swift so Xcode does not attempt CAS queries at all. You can also run tuist teardown cache to unload the LaunchAgent and remove the socket file so no daemon is kept alive in the background.

If you are using the Xcode cache, verify the daemon:

  1. Check that the socket file exists and has a listener. The socket path is printed by tuist setup cache on success and is usually ~/.local/state/tuist/<org>_<project>.sock:

    bash
    lsof ~/.local/state/tuist/<org>_<project>.sock
    

    If the command prints nothing and exits with status 1, no process is listening on the socket.

  2. Stream the daemon's logs and run your build in another terminal. If nothing is logged while xcodebuild runs, the requests are not reaching the daemon:

    bash
    log stream --predicate 'subsystem == "dev.tuist.cache"' --debug
    
  3. Tear down and re-run setup. The safest way to recover from a stale socket or a LaunchAgent that refuses to come back up is to run tuist teardown cache (which unloads the LaunchAgent, removes its plist, and deletes the socket file) followed by a fresh tuist setup cache:

    bash
    tuist teardown cache
    tuist setup cache
    

    If launchctl itself is failing, run tuist setup cache --verbose to see the bootstrap step and the path of the generated LaunchAgent plist (for example ~/Library/LaunchAgents/tuist.cache.<org>_<project>.plist).

On CI, run tuist setup cache on every job before any xcodebuild or tuist cache warm invocation. On developer machines, tuist setup cache only needs to run once per machine, but wiring it into a post-checkout Git hook (or an equivalent bootstrap script) is a reliable way to make sure the daemon is running after a reboot or a fresh clone.

Some artifacts upload successfully while others fail with deadlineExceeded in the same build {#intermittent-cas-errors}

A build log that mixes successful uploaded CAS output notes with deadlineExceeded warnings usually means the daemon was running when the build started but became unreachable partway through (for example it was killed, the socket file was removed, or a wrapper script restarted it). Follow the steps above to confirm the daemon is still running after the failing build, and make sure nothing in your CI or local tooling removes the socket file or kills the tuist cache-start process during the build.

uploaded CAS output appears locally even though uploads are disabled {#uploaded-cas-output-with-upload-disabled}

When cache: .cache(upload: false) (or upload: Environment.isCI on a non-CI machine) is set, you may still see note: uploaded CAS output ... in the build log. xcodebuild has no way to skip those calls, so the socket still receives them; the daemon short-circuits the request internally and does not send anything to the Tuist server. The dashboard metrics account for this, so no spurious upload traffic is reported.