What is Package.resolved, when does it change, and how do you make CI resolve exactly what developers tested?

16.7.2 · L3 · Interview

Predict first

What does each command build after upstream publishes 1.1.0?

// === run.sh ===
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
W=$(mktemp -d); cd $W
mkdir -p Lib/Sources/Lib && cd Lib && git init -q -b main
cat > Package.swift <<'E'
// swift-tools-version: 6.0
import PackageDescription
let package = Package(name: "Lib", products: [.library(name: "Lib", targets: ["Lib"])], targets: [.target(name: "Lib")])
E
echo 'public let v = "1.0.0"' > Sources/Lib/V.swift; git add -A; git commit -qm 1; git tag 1.0.0
echo 'public let v = "1.1.0"' > Sources/Lib/V.swift; git commit -qam 2
cd ..; mkdir -p App/Sources/App; cd App
cat > Package.swift <<E
// swift-tools-version: 6.0
import PackageDescription
let package = Package(name: "App", dependencies: [.package(url: "file://$W/Lib", from: "1.0.0")],
  targets: [.executableTarget(name: "App", dependencies: [.product(name: "Lib", package: "Lib")])])
E
printf 'import Lib\nprint("Lib", v)\n' > Sources/App/main.swift
echo "first build:            $(swift run 2>&1 | tail -1)"
(cd ../Lib; git tag 1.1.0)
echo "after 1.1.0 is tagged:  $(swift run 2>&1 | tail -1)"
swift package resolve --force-resolved-versions >/dev/null 2>&1
echo "force-resolved:         $(swift run 2>&1 | tail -1)"
swift package update >/dev/null 2>&1
echo "after package update:   $(swift run 2>&1 | tail -1)"
echo "pinned version now:     $(grep -o '"version" : "[^"]*"' Package.resolved | tail -1)"
  • 1.0.0 on a plain build and after force-resolved; 1.1.0 only after swift package update, and the pin changes
  • 1.1.0 on the very next build
  • force-resolved moves to 1.1.0

Mental model. Package.resolved is the lock on a shared-office door: everyone keeps using the same key until someone deliberately re-keys the lock and tells the rest.

Package.resolved records, for each dependency, the exact version and commit revision the resolver chose. Once it exists SwiftPM uses it instead of resolving again: in the check, a new 1.1.0 tag appears upstream and both the next swift run and swift package resolve --force-resolved-versions still build against 1.0.0. Only an explicit swift package update (Xcode: File ▸ Packages ▸ Update to Latest Package Versions) moves the pin, to 1.1.0, and rewrites the file. The file is also rewritten when you change the manifest so the old pin no longer fits (for example raising from: above it). Because it holds the revision, it also protects against a moved or re-pushed tag.

For an app, commit it (in an Xcode project it lives in the workspace’s xcshareddata/swiftpm folder); it is what makes a CI build equal the build on your machine. For a library package it is ignored by consumers (the root package’s file wins), so committing it only helps your own CI. To make CI strict, use swift package resolve --force-resolved-versions (or swift build --force-resolved-versions), which fails instead of re-resolving when the file and manifest disagree; the Xcode equivalents are xcodebuild -onlyUsePackageVersionsFromResolvedFile and -disableAutomaticPackageResolution (listed in xcodebuild -help, which is what was checked; they were not run against a project here). Update dependencies deliberately, on a branch, in their own commit, so a regression is attributable to a version bump.

The pin is honoured until an explicit update changes it.

Common traps

  • Adding Package.resolved to .gitignore in an app. Every developer and CI run can then build different dependency versions.
  • Expecting a new upstream tag to show up on the next build. It will not until you run update.

They might follow up with

  • Why does the file store a revision as well as a version? Tags can be moved; the commit hash cannot.
  • Why does a conflict appear after you edit the manifest? The old pin is outside the new range, so the resolver has to move it.

Related questions

  • 16.7.1 How do from:, upToNextMinor, exact, ranges and branch requirements differ, and which version does the…
  • 16.7.5 How do package mirrors work, and when do you use one for a company or offline CI?
  • 16.2.1 How do Swift Package Manager, CocoaPods, Carthage and XCFrameworks compare, and how do you choose?
  • 16.6.1 A CI pipeline takes hours on a large modular app. How do you analyse and cut it: affected builds, caching…

← How do from:, upToNextMinor, exact, ranges and branch requirements…   Two dependencies need incompatible versions of a third. What does the… →