The same commit may pass locally yet intermittently fail on a continuously running cloud Mac with module file was created by a different version of the compiler. A rerun may then succeed. This usually does not mean the Swift source changed unpredictably. More often, different toolchains, SDKs, or parallel jobs are reading from the same precompiled module cache. Deleting all DerivedData may relieve the immediate symptom, but it also destroys the most valuable diagnostic evidence and allows the next cache contamination event to happen.
First Confirm That the Failure Is in the Module Cache
Clang compiles system headers and Objective-C modules into PCM files, while the Swift compiler maintains its own module cache. If the compiler, SDK, architecture, or build flags used to create a cached module differ from those used by the current job, module loading can fail.
Start by searching the complete log for these indicators:
| Log indicator | More likely cause | First thing to check |
|---|---|---|
different version of the compiler |
Xcode was switched | xcodebuild -version |
PCM file is out of date |
The SDK or header timestamps changed | Current SDK path |
could not build module |
An upstream module is corrupted | First module that failed |
| The same job passes after a rerun | Concurrent writes or a shared cache | Cache directory ownership |
Do not capture only the final error. A module-loading failure often triggers dozens of cascading errors. Search backward for the first occurrence of module, .pcm, or ModuleCache.
Before clearing any cache, preserve the failed build log, the Xcode build number, and the effective build settings. Without this information, even a successful recovery will not reveal whether the cause was toolchain drift, concurrent cache contamination, or corruption within the cache itself.
Preserve Comparable Evidence from the Failed Job
On a VMKeep cloud Mac, first record the developer directory that the commands actually resolve to rather than relying only on the version the script was expected to use.
set -o pipefail
mkdir -p artifacts/diagnostics
xcode-select -p | tee artifacts/diagnostics/developer-dir.txt
xcodebuild -version | tee artifacts/diagnostics/xcode-version.txt
xcrun --sdk iphoneos --show-sdk-path | tee artifacts/diagnostics/sdk-path.txt
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings \
> artifacts/diagnostics/build-settings.txt
Next, inspect SDKROOT, ARCHS, DEVELOPER_DIR, CLANG_MODULE_CACHE_PATH, and SWIFT_MODULE_CACHE_PATH in the failed command. If the job is launched through sudo, a remote session, or a scheduler process, also compare the effective user and HOME. Running the same script as a different user can change the default cache location.
Pin the Toolchain Entry Point
The build script should set DEVELOPER_DIR explicitly and run xcodebuild -version before compilation begins. If the version is not the one expected, exit immediately rather than allowing the job to write to a shared cache with the wrong toolchain. After upgrading Xcode, generate a new cache key instead of continuing to use the old directory.
Limit the Cache Boundary to a Single Job
A reliable directory key should include at least the Xcode build number, SDK, architecture, and job identifier. A branch name should not be used directly as a path; sanitize special characters first. The following approach gives each job its own DerivedData and module cache while keeping downloaded dependencies in a separate directory.
set -euo pipefail
JOB_KEY="$(printf '%s' "${CI_JOB_ID:-local}" | tr -cs 'A-Za-z0-9._-' '-')"
XCODE_BUILD="$(xcodebuild -version | awk '/Build version/{print $3}')"
SDK_BUILD="$(xcrun --sdk iphoneos --show-sdk-build-version)"
CACHE_KEY="${XCODE_BUILD}-${SDK_BUILD}-arm64-${JOB_KEY}"
ROOT="$HOME/BuildCaches/$CACHE_KEY"
DERIVED="$ROOT/DerivedData"
MODULES="$ROOT/Modules"
PACKAGES="$HOME/BuildCaches/SourcePackages"
mkdir -p "$DERIVED" "$MODULES/clang" "$MODULES/swift" "$PACKAGES"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath "$DERIVED" \
-clonedSourcePackagesDirPath "$PACKAGES" \
CLANG_MODULE_CACHE_PATH="$MODULES/clang" \
SWIFT_MODULE_CACHE_PATH="$MODULES/swift" \
build
Long-lived branches can reuse their own stable keys, while temporary merge requests should be isolated per job. Never allow two active xcodebuild processes to write to the same module directory. Likewise, do not package the module cache as a generic archive that can be restored across toolchains.
Clean Precisely Instead of Resetting Everything
After confirming a module-cache failure, stop any build processes using the affected directory, then increase the cleanup scope incrementally:
- Delete
Modules/clangandModules/swiftfor the current job. - Keep
SourcePackages, then resolve dependencies and rebuild. - If the index or intermediate artifacts still reference old modules, delete the current job's DerivedData.
- If only one third-party module fails, verify that its binary artifact supports the current architecture and SDK. Do not repeatedly clear caches to conceal a compatibility problem.
Cleanup must remain limited to the calculated job directory. Running rm -rf ~/Library/Developer/Xcode/DerivedData/* also affects other jobs and can turn a localized failure into a cold start for the entire machine.
Avoid Deleting Directories That Are Still in Use
Before reclaiming an old cache, use a job lock or execution records to verify that no active process is using the directory. Do not rely only on directory creation time, because a stable branch may continue reusing an older cache. The cleanup script should also verify that the target path is inside the expected root directory and exit immediately if a variable is empty or path calculation fails.
Make Cache Consistency a Build Gate
Ultimately, the pipeline should expose drift proactively. For every build, record the Xcode build number, SDK build number, architecture, cache key, and effective user. When the job ends, archive this information together with the failure log. When upgrading the toolchain, create a new namespace and reclaim the old cache only after the new path has proved stable.
You can also add a validation job that does not restore the module cache. If the regular job fails while the clean job passes, the problem is concentrated in the cache layer. If both fail, return to the source code, dependencies, or build settings and continue investigating. This avoids blaming every error on the cache without resorting to repeated runs and hoping for success.
The module cache is effectively part of the compiler's input. Once versions, directory ownership, and concurrency boundaries are encoded in the build scripts, PCM invalidation stops being a random failure and becomes an engineering incident that can be isolated and recovered.
Frequently asked questions
Should I delete all of DerivedData after a PCM failure?
Not initially. Preserve the log and build settings, then remove only the current job’s ModuleCache.noindex. Recreate its dedicated DerivedData only when indexes and build products are also inconsistent.
Can multiple Xcode versions share one module cache?
They should not. Include the Xcode build number, SDK, architecture, and job identity in the cache key so a toolchain change or concurrent build cannot reuse an incompatible PCM.
Continue validating this workflow on a cloud Mac
Choose the right memory, storage, node, and rental period from three Apple Silicon configurations.