Automating Xcode version standardization across your team
At some point during your research on software development methodologies, you might stumble upon the The Twelve-Factor App methodology. For those who don't know, this is a methodology for building high-quality software-as-a-service. It's intended to target any programming language and any application developer, but since it can be somewhat abstract, when we bring its concepts to the iOS context, sometimes we lack real world examples.
That's what motivated the creation of iOS factor, written by @KrauseFx. You can read more about it on his blog post iOS-factor: A methodology for building high-quality iOS apps.
Explicitly declare and isolate dependencies
One of the concepts of the twelve-factor methodology (or, one of the 'factors') is on how to manage dependencies of your project.
The benefit of having dependency versions explicitly declared is to have 100% reproducible environments, even after months or years. You're able to checkout on a git tag 4 years back, knowing that the build will succeed just like it used to do back then.
You already declare your Swift version in your Xcode project settings, or in a
.swift-version file. If you use
bundler to manage your gems (and you should), your gems such as fastlane are explicitly declared in your
Gemfile, and your
Gemfile.lock declares the exact versions installed. Your Ruby version can be declared by a
.ruby-version file and Ruby version managers such as
rvm will automatically pick it up. But when it comes to Xcode, there's no industry standard, really.
This necessity inspired the creation of
.xcode-version, a file analogous to
.ruby-version, but that defines the Xcode version that should be used in a given environment.
This is not an 'official thing', so there's a more properly documented proposal you can find here.
So if this file is not an industry standard yet, can we actually use this anywhere?
Yes! The popular xcode-install gem that installs and updates Xcode versions via CLI actually reads this file already, if defined. It's not officially documented yet, but xcpretty/xcode-install#424 should take care of that.
To adopt the usage of a dependency version lookup in your development environment, you will need a hook in your workflow to check whether the right version of the dependency is running, and, in case it's not running the right version, install the right one.
With Xcode it's no different. But we can't run this within Xcode (e.g. in a Run Script) not only because this check doesn't need to happen on every build, but also because it makes no sense for Xcode to potentially trigger an Xcode installation. 😅
What projects usually have is an environment setup script that every developer has to run before getting started with development. This usually comes in the form of a Makefile where developers simply call
make to install the project dependencies, or sometimes a custom bash script. Regardless of how your team does it, I think this is a suitable hook for your Xcode version validation step.
Go ahead and create your
.xcode-version file and place it at the same level as your
.xcworkspace file (usually at the root of your project). This file should contain just a string declaring the Xcode version you'd like to use. Assuming your team should be running Xcode 12.4, ou can create this file by running:
$ echo -n "12.4" > .xcode-version
Next, you need to make sure your team has
xcode-install gem installed. Again, I recommend declaring this in your
Gemfile, in a
bundler-powered environment. Then, in your hook, add this line:
bundle exec xcversion install
With this command, if the currently installed Xcode version doesn't match the one declared in the version file, it will proceed to the installation. Otherwise, it will print a nice and brief:
Version 12.4 already installed.
Where to go from here
I couldn't recommend more revisiting all your project dependencies, and making sure all of them are being declared explicitly.
Other than declaring the Xcode version, some common mistakes include not using
bundler to manage Ruby gems, and using the native macOS Ruby version instead of one managed by a Ruby version manager. In case you missed it, Ruby (and other scripting languages) actually won't be included in macOS by default in future versions of macOS according to Apple.
If you liked this article, if you spotted mistakes in it, or if you just want to say hi, ping me @rogerluan_ 🤗