How do from:, upToNextMinor, exact, ranges and branch requirements differ, and which version does the resolver pick?

16.7.1 · L3 · Interview

Predict first

Which version does each requirement resolve to against tags 1.0.0, 1.1.0, 1.2.0, 2.0.0 and an untagged main?

// === 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
for v in 1.0.0 1.1.0 1.2.0 2.0.0; do
  echo "public let version = \"$v\"" > Sources/Lib/V.swift; git add -A; git commit -qm $v; git tag $v
done
echo 'public let version = "main-dev"' > Sources/Lib/V.swift; git commit -qam dev
cd ..
i=0
for req in 'from: "1.0.0"' '.upToNextMinor(from: "1.0.0")' 'exact: "1.1.0"' '"1.0.0"..<"2.0.0"' 'branch: "main"'; do
  i=$((i+1)); mkdir -p App$i/Sources/App
  cat > App$i/Package.swift <<E
// swift-tools-version: 6.0
import PackageDescription
let package = Package(name: "App", dependencies: [.package(url: "file://$W/Lib", $req)],
  targets: [.executableTarget(name: "App", dependencies: [.product(name: "Lib", package: "Lib")])])
E
  printf 'import Lib\nprint(version)\n' > App$i/Sources/App/main.swift
  echo "$req -> $(cd App$i; swift run 2>&1 | tail -1)"
done
  • from → 1.2.0, upToNextMinor → 1.0.0, exact → 1.1.0, range → 1.2.0, branch → main-dev
  • from → 2.0.0, because it means "or newer"
  • every rule resolves to 1.0.0 until you run update

Mental model. A requirement is a shopping list that says “any cereal from this aisle”; the resolver picks the newest box that fits, and Package.resolved is the receipt that makes next week’s shop buy the same box.

A requirement is a range over semantic versions, and the resolver picks the highest version that satisfies every requirement in the graph. In the check, a library tagged 1.0.0, 1.1.0, 1.2.0, 2.0.0 plus an untagged commit on main resolves to 1.2.0 for from: "1.0.0" (that means 1.0.0 ..< 2.0.0, never 2.0.0) and for the explicit range "1.0.0"..<"2.0.0"; to 1.0.0 for .upToNextMinor(from: "1.0.0") (1.0.0 ..< 1.1.0); to exactly 1.1.0 for exact:; and to the tip of main for branch: "main". revision: pins a commit hash. The default for a library that follows semver is from:: you get fixes and additions but are protected from the major version that is allowed to break source compatibility.

Two caveats. A branch or revision requirement is not a version: a package that depends on you cannot use you through it (SwiftPM rejects it, a rule I did not run here), so branches are for a root app while you wait for a release. And semver is a promise, not a guarantee: a minor release of a dependency can still break your build or behaviour, which is why the resolved file, not the range, is what you commit. Pre-1.0 packages are the weak spot: from: "0.4.0" allows 0.5.0, and pre-1.0 authors often break things in minors, so prefer .upToNextMinor for 0.x dependencies. The lab applies each rule to a tag list.

from: and a half-open range stop before the next major; upToNextMinor stops before the next minor; exact is one version; branch tracks a moving commit.

Common traps

  • Reading from: "1.0.0" as “1.0.0 or newer, including 2.x”. It stops before the next major.
  • Shipping a library that depends on a branch. Consumers cannot resolve it by version; use a tag.

They might follow up with

  • How do you try an unreleased fix? Point the root app at a branch or revision (or a local path: checkout) temporarily and switch back once it is tagged.
  • What does the resolver do with several requirements on one package? It intersects the ranges and picks the highest version inside the intersection, or reports a conflict (next question).

Related questions

  • 16.7.2 What is Package.resolved, when does it change, and how do you make CI resolve exactly what developers tested?
  • 16.7.3 Two dependencies need incompatible versions of a third. What does the resolver say, and how do you get out?
  • 16.1.1 What are targets, products and modules in a Swift package, and what does a first Package.swift look like?
  • 16.2.1 How do Swift Package Manager, CocoaPods, Carthage and XCFrameworks compare, and how do you choose?

What is Package.resolved, when does it change, and how do you make CI… →