Speeding up your custom Xcode build scripts

When a project reaches a certain size you quickly start getting frustrated with slow compilation times. This can be a particular problem if you’ve added scripts to auto-generate code, such as SwiftGen that work best when run as a build phase to remain in sync with your storyboards and strings.

In a recent project this reached the point where developers were waiting sixty to ninety seconds to launch the app even when they hadn’t made any changes to the code 😰.

A quick overview

Every target will have some build phases that are generated by Xcode, and these are usually necessary for xcode to be able to build the target. Every one of these phases will be executed each time you Howebuild the app, but Xcode does its best to optimise this. For example, the “Compile sources” phase knows that it doesn’t need to compile classes that you haven’t modified since the last time they were compiled.

If you’re adding your own build phases Xcode doesn’t have the same context to determine if it should be executing the build phase again - so it sensibly defaults to running the complete script completely each time the target is built.

More efficient build phases

In the past I’ve always glossed over the “Input files” and “Output files” section of the run scripts. It seemed it was just there so that I could specify some files that the script would be executed on - but I never really found a use for it. Easy enough to ignore.

Add a custom run script

But they have an extra feature! They’re used by Xcode as context to determine whether or not the build phase needs to be executed for this build. When you specify both input and output files, the script:

  • Will be executed if any of the output files don’t exist
  • Probably won’t be executed if the modification time on any of the input files is more older than on any of the output files.

Most build scripts that you’re interested in running will probably be run against some source files, and you should add all of these to your list of Input Files. We were using Swiftgen to build objects for our storyboards and localized strings - so we enumerated all of our storyboards and the localized string files.

However, there were some difficulties adding Swiftgen’s generated files to the list of Output Files. It seems like there’s a limitation in the logic to detect changes if the output file is contained in $(SRCROOT). It’s easy enough to get around this by touching a file in the $(DERIVED_FILE_DIR) and using this as the output file used by Xcode.

All of the files that you list are passed into your script using the variables SCRIPT_INPUT_FILE_COUNT, SCRIPT_OUTPUT_FILE_COUNT and matching counted files SCRIPT_INPUT_FILE_0, SCRIPT_INPUT_FILE_1 etc. This lets you easily use them within the script.

Adding some input and output files

If Xcode decides that it’s not necessary to run your custom script for a given build, it will still print a cached copy of the script output to the build log.

By setting this up for our builds we instantly saved about a minutes build time for the majority of builds. Across a large team, that’s a lot of wasted time!