server/priv/docs/en/guides/features/cache/xcode-cache.md
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.
[!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:
tuist init
Once you have a Tuist.swift file referencing your fullHandle, you can set up the caching for your project by running:
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>.
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: trueto yourTuist.swiftfile:swiftimport ProjectDescription let tuist = Tuist( fullHandle: "your-org/your-project", project: .tuist( generationOptions: .options( enableCaching: true ) ) )
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:
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:
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.
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:
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.
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:
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:
lsof ~/.local/state/tuist/<org>_<project>.sock
If the command prints nothing and exits with status 1, no process is listening on the socket.
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:
log stream --predicate 'subsystem == "dev.tuist.cache"' --debug
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:
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.
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.