Engineering guide

Tune Xcode Build Caches on a Remote Cloud Mac

Tune Xcode Build Caches on a Remote Cloud Mac

Two days before a release, you add another remote Mac. The repository clones in minutes, but the first Xcode build seems to take forever. The second build is faster, only for stale resources, indexing problems, or dependency resolution to reappear after switching branches. The issue is usually not the Mac’s performance. It is that DerivedData, Swift package downloads, and build results all share default locations, making it difficult to tell whether a cache was hit or selectively export useful data before the rental period ends.

Establish a Comparable Build Baseline

Before tuning caches, control the variables. Record xcodebuild -version, the macOS version, commit hash, Scheme, build configuration, and destination platform. During testing, do not upgrade dependencies, switch Xcode versions, or change compiler flags at the same time. Otherwise, the build times will not be comparable.

Run at least three tests:

  1. Remove the project-specific DerivedData, then perform a cold build.
  2. Make no code changes and perform a warm build with the same command.
  3. Modify a regular Swift file, then perform an incremental build.

For each run, save the start time, end time, exit code, and result bundle. The progress indicator in the Xcode UI is useful for observation, but not for precise comparisons. Use xcodebuild consistently for automated measurements.

The goal of caching is not to make one particular build look as fast as possible. It is to produce predictable build times and results from identical inputs.

Separate Caches by Responsibility

Create four types of directories under the workspace: source code, DerivedData, Swift package downloads, and build results and logs. Do not share one DerivedData directory across multiple projects, and do not allow Debug and Release builds to overwrite each other. For larger project collections, include the Xcode major version and a commit or branch identifier as well.

Directory Contents Recommended strategy
Source Git working tree Keep separate for each project
DerivedData Indexes and intermediate artifacts Isolate by version, Scheme, and configuration
SourcePackages Swift package downloads and checkouts Reuse while the lock file remains unchanged
Results Result bundles and logs Archive by build time

A Swift package directory can be reused as long as the dependency lock file is identical. If the lock file changes, allow dependency resolution to complete normally rather than copying an old checkout directory directly into the new project. DerivedData is more sensitive to the toolchain. After switching Xcode versions, create a new directory instead of overwriting and continuing to use the old one.

Run Builds with a Consistent Command

The script below makes the important paths explicit. Replace the workspace, Scheme, and destination platform with the values used by your project. The result bundle path must not already exist when the command starts, so the script removes any directory with the same name first.

#!/bin/bash
set -euo pipefail

ROOT="${HOME}/BuildWorkspace"
SCHEME="Application"
CONFIGURATION="Release"
DERIVED="${ROOT}/DerivedData/${SCHEME}-${CONFIGURATION}"
PACKAGES="${ROOT}/SourcePackages"
RESULT="${ROOT}/Results/${SCHEME}.xcresult"
LOG="${ROOT}/Results/${SCHEME}.log"

mkdir -p "${DERIVED}" "${PACKAGES}" "${ROOT}/Results"
rm -rf "${RESULT}"

xcodebuild \
  -workspace "${ROOT}/Source/Application.xcworkspace" \
  -scheme "${SCHEME}" \
  -configuration "${CONFIGURATION}" \
  -destination "generic/platform=iOS" \
  -derivedDataPath "${DERIVED}" \
  -clonedSourcePackagesDirPath "${PACKAGES}" \
  -resultBundlePath "${RESULT}" \
  build | tee "${LOG}"

First run xcodebuild -list in Terminal to confirm that the workspace actually exposes the intended Scheme. If the project uses an Xcode project file instead of a workspace, replace -workspace with -project. Do not pass both options.

Generate a Stable Identifier for Each Cache Directory

At a minimum, a cache key should include the Xcode version, the dependency lock-file digest, the Scheme, and the configuration. You may also include the branch name, but do not use it as the only identifier because dependencies can change within the same branch.

XCODE_KEY="$(xcodebuild -version | shasum -a 256 | cut -c1-12)"
PACKAGE_KEY="$(shasum -a 256 Package.resolved | cut -c1-12)"
printf '%s-%s-%s-%s
' "${XCODE_KEY}" "${PACKAGE_KEY}" "${SCHEME}" "${CONFIGURATION}"

If the workspace contains multiple Package.resolved files, explicitly select the one that actually participates in the build. When no lock file is available, do not generate a fixed empty key. Doing so would place different dependency states in the same directory.

Diagnose Unnecessary Rebuilds by Build Phase

If warm builds are still slow, identify which phase is consuming the time. If dependency resolution runs repeatedly, check whether a script is rewriting the lock file and whether the package directory has the correct permissions. If compilation keeps repeating, inspect compilation conditions, generated files, and build phase scripts. If the linking phase behaves unexpectedly, check whether version metadata or embedded content changes on every build.

The most common issue is a script phase with no declared inputs or outputs. If a script rewrites the timestamp of a generated file on every run, downstream targets will be treated as out of date and rebuilt. Fix this by replacing the file only when its contents change, and by declaring accurate input and output paths in the Xcode build phase.

Do not make “delete every cache” a routine step. It can tell you whether a cache is contaminated, but not where the contamination originated. Move the project’s DerivedData aside first. If the issue remains, then address the Swift package directory. This preserves diagnostic evidence and avoids downloading every dependency again for each test.

Retain Only What Will Be Useful Before the Rental Expires

Before exporting data, stop any running builds and verify that all logs have finished writing. Then package the dependency lock files, any necessary Swift package downloads, result bundles, and project scripts. Treat committed repository content as the authoritative source code. Review any uncommitted changes in the working tree separately.

Avoid retaining the entire DerivedData directory long term. It is usually large and contains indexes, object files, and intermediate artifacts tied to a specific toolchain. If the next Mac uses a different Xcode version, regenerating it is generally more reliable. Do not include sensitive certificates, private keys, tokens, or temporary environment files in cache archives. Clear shell history, temporary keychains, and project-generated credential files first, then review the export manifest.

Finally, keep a short record of the toolchain version, cache key, last successful commit, build command, exported paths, and paths intentionally excluded from the export. Rebuilding the environment on the next cloud Mac from this record is more reproducible than copying the entire home directory.

Frequently asked questions

Should DerivedData be shared across different Xcode versions?

No. Separate it by Xcode version, project, scheme, and configuration. Rebuild DerivedData after changing toolchains so stale indexes and intermediate files cannot affect the next build.

Which cache files are worth exporting before a rental ends?

Export dependency lockfiles, reusable Swift package downloads, result bundles, and diagnostic logs first. Object files and indexes are large and toolchain-specific, so regenerating them is usually safer.

Will deleting every cache fix slow builds?

It can confirm cache corruption, but it is not a routine fix. Measure dependency resolution, compilation, linking, and testing separately, then remove only the directory tied to the slow or failing stage.

ArmMacs Cloud Mac

Use dedicated physical nodes for your project timeline

Choose the chip, memory, storage, rental term, and available node. When inventory is available, proceed to delivery.

Choose a configuration and order