The same commit may build successfully on a developer machine yet resolve to different dependencies on another cloud Mac. This is rarely a random compiler failure. More often, the resolution file, Xcode version, and cache boundaries were not locked together. Short-term nodes expose this problem especially often: the machine may arrive with a clean directory, while the pipeline still assumes that an old cache can be reused or silently rewrites dependency versions during the build.
Define What Counts as the Same Build
Reproducibility does not mean preserving the entire working directory between runs. It means that identical inputs must produce the same dependency set. Record at least four items: the source commit, the contents of Package.resolved, the Xcode build version, and the processor architecture. Recording only the major Xcode version is not enough, because toolchain patch versions can differ within the same major release.
Start by capturing an environment snapshot on the node:
set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD
Application projects should generally commit Package.resolved. For a workspace, the common path is App.xcworkspace/xcshareddata/swiftpm/Package.resolved. If the repository contains only a project file, it may instead be stored in the workspace directory inside the project. Do not maintain two resolution files at once. First determine which one the pipeline actually reads.
The test is straightforward: if
git status --porcelainreports a changed resolution file after the build script runs, that build cannot be treated as validation of the original commit.
Lock the Resolved Dependencies, Not Just Version Ranges
Version ranges in Package.swift define what SwiftPM is allowed to select. Package.resolved records what it actually selected for this build. Dependencies may be resolved explicitly when preparing a node for the first time, but production builds should accept only the versions already recorded.
set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$CACHE"
git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=macOS' \
-clonedSourcePackagesDirPath "$CACHE" \
-onlyUsePackageVersionsFromResolvedFile \
build
For an iOS target, replace destination with the generic device or simulator destination already defined by the pipeline. Do not let the script automatically select whichever device is currently booted. Pure Swift packages can use swift package resolve and swift build, but projects and workspaces should consistently use xcodebuild so that two resolution entry points do not produce different directory states.
Define Cache Boundaries with an Input Digest
A cache should only reduce download and checkout time; it must not become the source of dependency versions. A reliable cache key should include the architecture, Xcode build information, and a digest of the resolution file:
set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
| Input change | Reuse the working cache? | Reason |
|---|---|---|
| Application source only | Yes | The dependency set has not changed |
Package.resolved changes |
No | Checked-out versions or revisions have changed |
| Xcode build version changes | No | The toolchain and artifact formats may differ |
Switch between arm64 and another architecture |
No | Binary artifacts cannot be mixed |
When running across multiple nodes, do not allow several machines to write to the same directory concurrently. Store read-only cache archives by key, then have each physical node copy the appropriate archive into a local working directory. A failed retry should also begin by cleaning the current working copy rather than deleting every historical archive.
Fail the Job Immediately When Dependencies Drift
Checking repository state both before and after the build detects resolution-file rewrites as well as generated artifacts accidentally written into the source tree. Production jobs should start from a clean checkout:
set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
-clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
-onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"
Do not respond to a failure by immediately removing version constraints and resolving again. That turns a configuration error into an apparent success. Instead, preserve the resolution log, environment snapshot, and cache key. Then determine whether the repository is missing its locked result, a dependency revision is no longer valid, or the node selected the wrong Xcode installation.
Troubleshoot Common Failures in Order
Dependency Resolution Times Out
First verify that the node can reach the dependency sources, then check available disk space and permissions on the cache directory. Once connectivity is restored, retry with the same resolution file instead of upgrading dependencies first. If resolution repeatedly fails on the same package, validate that package’s declaration and confirm that the specified revision still matches it.
Resolution Succeeds but Compilation Fails
First compare xcode-select -p, xcodebuild -version, and uname -m between the two nodes. For binary dependencies, also confirm that the package contains a slice for the current architecture and supports the minimum operating system version configured by the project. Delete the local copy associated with that cache key and rebuild to distinguish cache corruption from source incompatibility.
Preflight Checklist
- The repository contains only one effective
Package.resolved. - The build command enables
-onlyUsePackageVersionsFromResolvedFile. - The cache key includes the architecture, Xcode build information, and a digest of the resolution file.
- Each node uses its own writable working directory.
- The resolution-file digest is identical before and after the build.
- Logs retain the commit ID, cache key, and toolchain version without recording credentials.
Frequently asked questions
Should Package.resolved be committed to the repository?
Usually yes for applications and deployable services, and builds should be restricted to the recorded versions. Libraries may choose a different policy, but release validation should still resolve and test the declared version range in a clean directory.
Can several cloud Macs write to one shared SwiftPM cache?
They should not write concurrently to the same directory. Build a cache key from the processor architecture, Xcode build version and Package.resolved digest, then give each node an independent working copy.
What should I check when resolution succeeds but compilation fails?
Verify the selected Xcode path and build version first, recreate the working cache for that key, and then inspect binary dependency architectures, minimum system versions and any build-time change to Package.resolved.
Keep a reproducible environment on the same cloud Mac
Choose a fixed model, region, and rental term, then use the full macOS graphical interface and command line for development or automation tasks.