In the previous article, we talked about how we can publish a swift package, and how we version it using semantic git tags. But what does semantic mean? In this article, we’ll explore this subject in more detail.
A library’s public API is all the symbols (e.g: types, variables, and functions) used by its clients via import. When we depend on a library, we also depend on its public API to accomplish a task. This dependency makes part of our code coupled to it.
Imagine we’re using a library from a popular Swift package. What would happen if the maintainers released a new version with huge changes to its public API? Our code would break. To avoid this issue, we could rely on a single version. But this means we would be locked in it, incapable of updating the package without updating our code. To make things worse, imagine each package owner has a different way of handling versions. These scenarios are what we call dependency hell.
Semantic versioning is a specification aimed at putting an end to dependency hell. It divides versions into three numbers: major, minor, and patch (e.g. 1.5.8). Here’s what each number mean:
Majormeans the public API has changed, requiring our code to be updated
Minormeans new code was added to the public API, the additions don’t break existing integrations (backward compatible)
Patchmeans bugs were fixed without changing the public API
Selecting package versions
When adding a Swift package, the recommended approach is to select a version using the up to next major dependency rule:
It means our project we’ll receive updates that don’t break our code. Select the Up to next minor rule if you only wish to receive bug fixes.
When declaring dependencies on the package, we can also specify those same version rules:
// ... dependencies: [ .package( url: "https://github.com/apple/swift-algorithms.git", .upToNextMajor(from: "1.0.0") ) ], // ...
Besides defining how to organize our versions, the specification also comes with some important guidelines:
Development versions (below 1.0.0)
Every version below 1.0.0 is considered unstable and under development. The public API isn’t defined yet and can change drastically.
The initial version (1.0.0)
Once the public API is defined, it’s time to release the initial version of the package. The public API is considered stable and set in stone.
Never modify a released version
If you wish to correct something, release a new version on top of the one with errors.
Use a development branch
You don’t need to release every single change you make. We can release multiple changes using a single version. Having a development branch allows developers to decide when to release a batch of changes as a single major, minor, or patch.
We can also pre-release versions. This is useful for testing features as they get added. They follow the same versioning rules but have a specific format in the end:
1.2.5-beta1 1.2.5-1 1.2.5-alpha-633
A pre-release doesn’t need to be stable. Use it only to test a set of features while they are being developed. Pre-releases have less precedence over official versions:
1.2.5 > 1.2.5-beta1
Usage with SPM
On the package side, to specify a pre-release, we use the
// ... dependencies: [ .package( url: "https://github.com/apple/swift-algorithms.git", from: Version(1, 2, 5, prereleaseIdentifiers: ["alpha", "1"])) ], // ...
The above code specifies a range of versions:
1.2.5-alpha-1 ..< 2.0.0.
On the project side (using Xcode 13.2.1), we can include the pre-release identifiers directly:
If we include the pre-release identifiers, SPM will fetch the versions automatically when we update our packages.
Keep a changelog
It’s important to document every version being added. The documentation is added to a file called
CHANGELOG.md. It contains the history of your project, with relevant information on what was added, removed, or changed. Here’s an example from the Swift-Algorithms package.
For more information, visit this link.
If you wish to correctly use dependency managers, make sure to understand what semantic versioning is. We’ve explored what problems it solves, and also some of its main guidelines. We also understood what a
CHANGELOG file is, and how we can use it.
In the next article, we’ll talk about building executables with SPM. We’ll also take a look at its command-line interface.