My latest detour was to add an extra option to ReleaseTools, a command-line tool, written in Swift, that I made a few years ago.
Actually I’ve just checked, and I started it in 2019 🤯.
ReleaseTools (or rt
for short) basically exists to automate the process of uploading an app to the app store, or packaging it up for external distribution via Sparkle.
I guess you could think of it as a bit like Fastlane, but without any of the bells and whistles1.
It is yet another thing that I’ve never really got around to talking about! Largely because I made it for myself, and didn’t really want to end up supporting it for other people.
That said, it’s fairly mature now and I’ve used it in at least one client project as well as for a number of my own. Whilst I can’t guarantee that it will work for you, and I’m sure the documentation needs some improvement, I would certainly be happy to try to help someone else give it a go.
Scripting Like An Animal
Under the hood, rt
uses xcodebuild
, altool
and git
.
Prior to it existing, I was doing most of the same things that it does, using hand-rolled shell scripts for each project.
Or, in the case of my time working on Sketch, a big hairy load of Python I wrote2…
At some point I got annoyed with the fact that I was writing shell script, came to my senses, and decided that using Swift would be much more efficient fun.
Basic Design
If there was one thing I learned whilst hand-rolling all the previous bespoke solutions, it was that debugging build scripts is a massive pain in the arse.
Partly because debugging scripts is just generally painful, but also specifically because the build process tends to be slow, and it’s really annoying to have to keep waiting for a long task like archiving to run, only to have the subsequent exporting fail because you messed something up.
So when I made rt
, I decided to try to factor it out into a series of small commands that were intended to be run in sequence.
Thus the basic design is that you can individually run commands to do things like:
- archive
- export from the archive
- upload the export for distribution
- upload the export for notarisation instead
- wait for notarisation to finish
- compress the notarised app
- rebuild a sparkle feed to include the compressed app
- publish the sparkle feed to your website
This lets you write a simple shell script3 to run the commands in sequence during normal operation, but when something goes wrong you can just re-run that command, and/or the subsequent ones.
Anyway, I won’t go into a lot more detail about the specifics - if you want those you can check out the README.
Suffice to say that it basically works these days, and I use it daily.
In fact I’ve added a fast path where you just run rt submit
and it performs the archive
, export
and upload
commands in sequence.
Build Numbers
One of the nice things rt
does is calculate and inject a build number.
I prefer to do this, rather than relying on the app store portal to automatically assign one, as it lets me tag the exact commit that I built from in git. Once it knows that the upload has succeeded, rt
makes a tag in git including the exact build number, version, and platform.
Originally, I calculated the number just using the count-the-git-commits trick.
This works surprisingly well most of the time, but it is a little fragile. In particular, if you switch branches, the count can change, and consequently the build number can go down. This is Not A Good Thing™.
In practice, I rarely encountered this problem. I commit little-and-often, and for projects I’m working on myself, I would probably only submit from a single branch.
However, when I working on Club, we did have this problem. That project didn’t use rt
at all - it was cross-platform, and written in Dart/Flutter, with an entirely different toolchain. It started out doing the commit-counting, but we ended up switching to a solution that explicitly read the existing git tags and used them to work out the next build number.
When I subsequently came back to my own projects, I was inspired by the tag-based approach and decided that rt
should use it too.
Version Tags
So I added an option to rt
which calculated the new build number by:
- scanning for tags in the form
vX.Y.Z-platform-NNN
, egv1.0.2-iOS-234
. - treating the highest existing NNN for our platform as the current number
- adding 1 to it for the new number
That works really well, and because rt
also creates and pushes a tag in the same format, it’s pretty reliable.
The only wrinkle with it is that when you are supporting two or more platforms, you have to submit each one with rt
separately, e.g:
rt submit --platform=iOS
rt submit --platform=macOS
Sometimes a problem will occur, or something will get broken, such that one upload succeeds and the other fails.
This becomes a problem subsequently because you’ve now got one platform with a tag that worked, and which the app store knows to have that build number, but another platform that only has an older tag.
So when you subsequently upload the next build, you end up with different build numbers for each platform, even though they are built from the same commit. Which is confusing and a bit annoying.
Why Are You Telling Me All Of This Again?
Good question.
Come to think of it, the main point of this whole post - other than just answering, in a rambling way, the “how was your day dear?” that you didn’t ask - was to mention the existence of rt
.
Which I’ve done.
You can stop reading now!
What follows is more detail than you probably want to know about why I started actually working on rt
recently, what I did to try to fix the tagging problem, and why I’ve realised that I need to go back to my fix!
Inconsistent Tags
To fix the inconsistent tags manually, the easiest thing to do has been to use git on the command line to make a tag with the right build number, for the platform that had fallen behind, on the commit that it should have been uploaded from.
That way, rt
will find the tag next time, add 1 to it, and come up with the right value.
This is a bit clunky. Also, occasionally, I only want to release one platform, as I may have changed code that just affects it. Next time I submit though, I want to release both platforms using the highest build number for any platform.
So I decided to try to add some new capabilities to support this objective.
First, I’ve added an --explicit-build
option to allow you to specify the build number to use. That is a cleaner way to reset everything when doing a new upload, as you can just force the number for both platforms to get them back in sync.
I’ve also added an --existing-tag
option. This looks for the highest tag for any platform, as well as for the platform you’re uploading, and compares the two. If there’s a higher number for another platform, it will use that. If not, it will do what it used to do and increment the previous build number by one.
This works nicely for the situation where a build or upload failed for one platform in a transient way, you’ve fixed it with some minor commit, and you just want to upload something with the same number as the other platform(s).
I actually thought that I was done, but whilst writing this blog post, I’ve realised that I’m not.
If the commit has changed, you really should use a higher build number - but it needs to be higher than any platform has used, and not just higher than the platform you’re uploading.
I think posibly what rt
needs to do is to figure out whether the tag from the other platform is on the HEAD commit. If it is, then what you’re doing is “catching up” and it should just use the same build number - it’s the same commit, so using the same number makes sense.
If there’s no version tag on HEAD though, then we probably want to increment the highest number by one more, and use that. This will create a new baseline with the first platform that you upload, and subsequent platforms will then catch up to it.
So I guess that’s the next job then…
Or…
… although, maybe the problem here is that the design is wrong.
The basic problem is that when making the tag, rt
doesn’t know which platforms you’re uploading, as it does them one at a time.
I can see a couple of ways to fix this.
One would be a way to pass a list of platforms in, and have them all upload at once.
Another… which I am coming round to instead… is making the tag creation an explicit and separate step.
This feels more in keeping with the small-individual-commands design.
Maybe version tags should not be platform-specific, and just in the form vX.Y.Z-NNN
.
If you run rt submit
or rt archive
and there isn’t a version tag on HEAD, it should just refuse to do anything.
Then we can have a separate rt tag
command which makes a new version tag. So you run that first, then run the archive or submit commands for each platform in turn.
We can still have a small shell script that you can run for the normal situation where you want to upload everything at once:
rt tag
rt submit --platform=iOS
rt submit --platform=macOS
You can still run the individual bits though, and they will do the right thing. If you’re on a tagged commit they will work. If not, they won’t. If you run rt tag
on a commit that’s already tagged, it can spot that and complain.
This feels a lot cleaner actually.
I’m glad we had this talk.
Thanks for listening!