Installing and running Swift tools with enforced versions
Working on Swift projects often involves using tools to help with tasks such as linting using SwiftLint or generating code using Sourcery. When running tools we want to ensure all developers of the project are using the same version and that a specific version can be chosen to ensure compatibility. For most Swift tools there is the choice of many different installation methods, each with its own benefits and drawbacks, so for many people it might not be obvious what the best option is.
Before we look at the installation methods, let's consider what we would like to be able to do:
- Enforce versions between team members
- Use a specific version for each project
- Use the same version locally and on CI
- Install tools quickly
Installation options
Homebrew
Homebrew is a popular package manager for Mac and is useful for installing tools and apps, even allowing use of a Brewfile
. Unfortunately, unless a particular version is provided as a formula we cannot install a specific version, instead we would need to require team members to update to the latest version. On top of this some tools may not be published to Homebrew and releases can take longer to arrive.
Swift Package Manager Plugins
SPM has support for command and build-time plugins, linking to specific versions. Unfortunately, adding them to Xcode projects only allows running command plugins via the UI, so we would need an SPM package with a Package.swift
to run them from the command-line. Depending on the tool we are running we may need to build our own plugin.
Cocoapods
Many tools offer installation via Cocoapods and running them from the Pods
directory. Although we can pin to a specific version, not all tools will be available via Cocoapods. The main issue is that it requires the project to already be using Cocoapods or to add Cocoapods to it, which is Ruby-based.
Mint
Swift tools can be built and installed using Mint, where we can enforce versions using a Mintfile
. We will of course need another method for installing Mint at the right version itself. Many people use Mint happily, however it can be very slow building tools from sources when the version changes, particular on CI where we will need caching to avoid this happening for every build.
Binary or cloning
Instead of using a package manager, we could download the tool as a binary from the GitHub release or clone the repository and build it for ourselves. It will be difficult to ensure developers keep tools up-to-date and so the approach will likely involve maintaining some scripts.
An alternative approach
For many teams one of these installation methods may be the right approach, however they do come with some potential drawbacks:
- It may not be easy to use or error-prone
- Specifying and enforcing the version may not be possible
- Installation may involve building the tool from sources with its dependencies
- One of the tools may not support a particular installation method
- Our project may not be using Cocoapods or SPM
For a while I have been successfully using binaries wrapped with scripts to manage download, installation and execution. For developers it is much quicker than any method that builds from sources, it ensures consistent versions and it isn't dependent on use of any external package managers.
The scripts
First, we need to create a folder in the project to store the tools and then a script for the tool in question, such as swiftlint.sh
. We start the script by specifying the version at the top where it is easy to update.
VERSION="0.54.0"
We need to calculate the installation directory based on the script location. Make sure to add BuildTools/Tools
to the gitignore
to avoid binaries or sources being added to source control.
BASEDIR=$(dirname "$0")
INSTALL_DIR="${BASEDIR}/Tools/SwiftLint"
EXECUTABLE="${INSTALL_DIR}/swiftlint"
SwiftLint is provided as a binary on the GitHub release so we can download the version we need.
DOWNLOAD_URL="https://github.com/realm/SwiftLint/releases/download"
DOWNLOAD_URL+="/$VERSION/portable_swiftlint.zip"
We can now write our install function which will download and unzip the binary, before marking it as safe to execute on Mac.
function install_tool {
mkdir -p $INSTALL_DIR
curl -LO $DOWNLOAD_URL --output-dir $BASEDIR
unzip $BASEDIR/portable_swiftlint.zip -d $INSTALL_DIR
rm -rf $BASEDIR/portable_swiftlint.zip
xattr -dr com.apple.quarantine $EXECUTABLE
}
If the executable is not already installed, we use our function to install it.
if [[ ! -f $EXECUTABLE ]]
then
echo "$EXECUTABLE not found, installing…"
rm -rf $INSTALL_DIR
install_tool
fi
If the executable is already installed, we check if it is the right version and re-install it if not.
INSTALLED_VERSION=$(./$EXECUTABLE --version)
if [[ $VERSION != $INSTALLED_VERSION ]]
then
echo "Version out-of-date, re-installing…"
rm -rf $INSTALL_DIR
install_tool
fi
Finally, we run the executable, passing any arguments to it.
./$EXECUTABLE ${@:1}
An installable version of the full script is available on GitHub.
Supported tools
Whenever a binary can be linked to for a particular version by URL, such as GitHub releases, it will be compatible with this approach. If no binary is available the script could be adapted to obtain it via cloning and building from the Git tag as a backup.
I have been using this approach to run:
- SwiftLint, installable version on GitHub.
- SwiftFormat, installable version on GitHub
- Sourcery
The installable versions also include details of using them witin Fastlane, which is great for projects that are using Fastlane for automation.
Wrap up
Using scripts to install and run our tools combines the best of the options, whils keeping performance high:
- Easy to use
- Enforces versions of our tools across the team
- Independent versions for different projects
- Quicker than approaches that checkout and build from source
- Supports local and CI
It would be unfair if we didn't also consider the drawbacks, in that we have to maintain a script for each tool and the approach only works best when there is a binary available for download.
Hopefully eventually SPM command plugins will be executable from the command-line for Xcode projects in a performant way. This would make that the preferred approach due to being built-in and not involving maintaining custom scripts to run.
I hope the article was useful. If you have any feedback or questions please feel free to reach out.
Thanks for reading!
WRITTEN BY
Andrew Lord
A software developer and tech leader from the UK. Writing articles that focus on all aspects of Android and iOS development using Kotlin and Swift.
Want to read more?
Here are some other articles you may enjoy.