Pinning Swift Package Versions
Predictable SPM Package Versions Across All Machines
Author: Lucas van DongenIntroduction
Today I'm going discuss a small but important aspect of working on Swift projects. As soon as the project leaves your computer and will be built on other machines, you're going to have to deal with Swift Package versions in one form or another.
Non-deterministic behavior always costs a lot of time. For example tests that work on your machine start failing on the CI, or new team mates cannot build the project. Because of this, we need to make sure that whenever a new developer or the CI pulls the project's code, it works exactly as when you pushed it from your machine.
One of the issues that crops up often are discrepancies between package versions when using the Swift Package Manager for managing external dependencies.
When you are dealing with Swift applications at even modest scale, you always should strive to minimize the amount of non-deterministic build behavior to the minimum. Just dealing with the odd curveball Xcode throws by itself from time to time is already enough.
This might not be a new concept for all of you, according to this poll the majority of you already use version pinning. But you still might learn a new thing or two from this article even if you do.
Swift Package Manager Does Not Really Lock Its Files
What surprises most people coming from other platforms or CocoaPods is that Package.resolved
is not really comparable to a .lock
file. Whenever a teammate checks out the project and Xcode starts to look for the Swift Packages, it simply takes the most recent package version that fits the Dependency Rule in Xcode.
This can lead to unexpected issues where your CI (Continuous Integration Server) or your new team mate gets a newer version of the package than you have cached on your local machine, and they might start getting issues you cannot reproduce on your local machine.
Not all developers pin all of their package versions
How To Pin a Swift Package Version
Pinning a Swift Package version is very easy. From the same screen you add your dependencies, you'll find a Dependency Rule column. The same drop down menu exists when you add a new dependency:
- "Up to Next Major Version" is the default setting and will update until the first digit increases: 1.x.x.
- "Up to Next Minor Version" does the same, but stops whenever the middle number increases: 1.3.x.
- "Exact Version" is the option that pins the version.
- "Range of Versions" gives you the version an upper and lower bound.
- "Branch" takes whatever is the latest commit on a given branch.
- "Commit" pins it to a specific commit.
The last two options can be really handy whenever you have an issue that is fixed in a given branch or commit, but haven't been properly released yet. Picking a specific commit hash can also prevent against bad actors trying to inject code into compromised packages, because the version can be spoofed, but the commit hash can not.
Advantages of Pinning a Swift Package Version
1. Projects Work Reliably Across Machines
Once you have your iOS, Xcode, Swift and dependency versions locked, you can have reliable builds across all machines. The only thing that can still wreak havoc are your project files. This issue can be solved using a project generation tool like Tuist.
2. Less People Deal With Fixing Version Issues
Instead of version issues popping up at random when somebody or some machine tries to build the project after a problematic new version of a package was released, issues now happening at predictable moments:
- When somebody tries a new version of Xcode that is incompatible with the currently pinned version
- When a developer wants a feature that only exists in a newer version of the package
- After a package is deprecated because of a security issue or incompatibility with it's back-end
Because these issues happen at predictable moments, developers realize much quicker the stems from a package version issue, instead of dismissing the issue as just an Xcode fluke at first.
3. More Predictable Caching
Since you won't be dealing with more than two versions between active branches, any machine trying to build the project typically already has exactly the dependency needed. This can speed up build times, especially when the CI verifies the package checksums and skips downloading when they already match what is cached.
Issues When Pinning Versions
New Xcode Versions
Every time a new Xcode version reaches its beta stage, developers that publish popular packages will download them and fix any issues that might arise in the new version. This means that any project that updates its packages to accommodate the next major version typically includes those fixes by the time the final version of Xcode is released.
This is not the case when you pin your dependencies. Every time a new Xcode version gets released any pinned version might break. However, in larger projects there is always a conservative approach towards new Xcode versions, and there will always be a specific green lighted version of Xcode to use. New Xcode versions have a green lighting procedure even without considering package versions breaking, so it's usually just a small extra burden for larger organizations.
Missing Out on New Features
Packages evolve, and some developers might find that one needed piece of functionality exists only in a newer version of the package. Instead of simply updating the package, this developer now needs to go through the procedure of getting approval to pin a higher version of the package before being able to use it.
Security Issues
This is a particular nasty one, because most other issues just announce themselves by builds failing or developers complaining. But a security issue can sit there for years. Exposing your user's secrets, compromising bank details or even downloading malware.
There is no way around this besides manually checking all of your dependencies from time to time. One GitHub feature that can really help is Dependabot, that quite recently got Swift support. It will check all of your dependencies automatically, and send an alert automatically when there's a security issue.
Increased Manual Maintenance
Since your packages don't get updates by accident anymore, they will be stuck perennially on the same version. Given the risk of security issues, but also the chance that it's functionality slowly starts deteriorating against newer version of iOS and Xcode it's required to check every dependency at least every three months for updates, and evaluate if any new version is worth upgrading to.
Conclusion
Pinning the versions of your packages to an exact Version or Commit is a great practice, especially for larger teams. Dealing with the extra overhead to manually monitor and update the package is a fair exchange for taking this headache out of your development chain.
Thank You for Contributing
I want to thank everybody that voted and commented on my poll about pinning package version on LinkedIn, especially:
- Rool Paap for the Dependabot tip
- Logan Blevins for the CI checksum check idea
- Dave Poirier for pinning to a specific commit hash
See You After WWDC 2024
I'm going to take a slight break from writing until WWDC 2024, to study other topics that do not result into blog posts and take a slight break, so I'm fresh again for the onslaught of new information. I sure hope we can find lots of new interesting stuff around Swift and developing for Apple platforms in general.