How do package mirrors work, and when do you use one for a company or offline CI?
Predict first
Which copy of Lib does the app build, and what does Package.resolved record?
// === 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 = "origin"' > Sources/Lib/V.swift; git add -A; git commit -qm 1; git tag 1.0.0
cd ..; mkdir mirror; cp -R Lib mirror/Lib
(cd mirror/Lib; echo 'public let v = "mirror"' > Sources/Lib/V.swift; git commit -qam m; git tag 1.0.1)
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: "$W/Lib", from: "1.0.0")],
targets: [.executableTarget(name: "App", dependencies: [.product(name: "Lib", package: "Lib")])])
E
printf 'import Lib\nprint("built against:", v)\n' > Sources/App/main.swift
swift package config set-mirror --original "$W/Lib" --mirror "$W/mirror/Lib" >/dev/null 2>&1
echo "mirror config written: $(ls .swiftpm/configuration)"
swift run 2>&1 | tail -1
echo "pin keeps original location: $(grep -c "\"location\" : \"$W/Lib\"" Package.resolved)"
- The mirror copy (v = "mirror") while Package.resolved keeps the original location
- The original copy, since mirrors only apply to binaries
- The mirror copy, and Package.resolved records the mirror URL
Mental model. A mirror is a local branch of the library: the catalogue still lists the original address, but your copy of the book comes from the shelf next door.
A mirror tells SwiftPM “whenever the graph asks for repository X, fetch it from Y instead”. It is configured outside the manifests (swift package config set-mirror --original X --mirror Y, stored in .swiftpm/configuration/mirrors.json for the project or in the user’s configuration directory), so a package does not change at all. In the check the root asks for Lib by one location and gets a build that prints mirror: the resolver fetched the mirror’s copy, which has a newer tag 1.0.1, and Package.resolved still records the original location (the count of matching location lines is 1), so the pin file stays portable between people who do and do not use the mirror. Two details worth knowing: the mirror must have the same package identity (the last path component), otherwise the product lookup fails with an “unknown package” error, and in my run the plain-path style of URL was mirrored while a file:// URL was not, so test the exact URL form you use.
Uses: a company that wants builds to survive GitHub outages or to vet code (a read-only internal copy of each third-party repo, reviewed before it is updated), air-gapped or rate-limited CI, and speed (a nearby cache). Combine with the resolved file (Q618) so the mirror cannot silently serve different content, and with the shared dependency cache (~/Library/Caches/org.swift.swiftpm, cleared with swift package purge-cache) to avoid re-cloning on every run. For Xcode projects, xcodebuild -clonedSourcePackagesDirPath lets CI reuse a checkout directory between jobs, and -disableAutomaticPackageResolution stops it from reaching the network at all once the checkouts are in place. Package registries (swift package-registry, SE-0292) are the other way to serve versions from a company server; only the mirror flow was run here.
The mirror changes the fetch location; the pin file keeps the original identity.
Common traps
- Mirroring under a different repository name. The identity changes and the product lookup breaks.
- Assuming mirror settings travel with the package. They are local configuration; commit the file or script it for CI.
They might follow up with
- Does a mirror replace
Package.resolved? No; the pin still records revisions, the mirror only changes where bytes come from. - Mirror or registry? A mirror re-routes repository URLs; a registry serves versioned source archives under package identifiers.
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.4 Why does SwiftPM refuse a dependency that uses unsafeFlags, and what should you do instead?
- 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…
← Why does SwiftPM refuse a dependency that uses unsafeFlags, and what… How do you inspect a package and its dependency graph from the… →