Speeding Up Custom Script Phases

Some Xcode projects use a handy feature for extending the build process called a Run Script build phase. These build phases can be added to the linear list of steps that Xcode steps through when building a target, such that custom tasks such as preparing dynamically generated source code, verifying build results, etc., can call be done as part of Xcode’s standard build routine.

A problem you might run into is that a long-running script phase can be a real drag to wait through every single time you build. You can work around this problem in a few ways, depending on what makes sense for your project. Two of the simplest options are:

  1. Check the box to run the script only “when installing.”
  2. This is particularly handy if the stuff that’s happening in the build phase really doesn’t need to be there until the final release build, or if you are willing to manually perform the steps during development.

  3. Add logic to your build phase script to detect the build configuration, and either perform a more expedient version of the task, or skip it altogether. For example, a build phase that does laborious profiling of your finished product may not need to be done on “Debug” builds, so you could add a clause to the script:
    if [ ${CONFIGURATION} == Profiling ] ; then
    	echo "Running a very long profiling task!"
    	...
    	echo "Finished running a very long profiling task!"
    else
    	echo "Skipping long profiling task! Whoo!"
    fi
    

    (Note: you don’t have to use bash for your shell script phases. In fact, you can run whatever interpreter you like. I typically use Python, but used bash here as a canonical example).

These tricks work out fine when a script phase is clearly only needed under special circumstances, but what about script phases that have to be done every time you build, rely upon files that may change over time, and take a considerable amount of time to finish? For this we can take advantage of another nuanced feature of Xcode script phases, which is the ability to specify arbitrary input and output files for the script. I think it’s time for an illustrative screenshot:

Screenshot of a configured Xcode script phase

Here’s the low down on the impact of specifying input and output files with script phases in Xcode:

  1. Lack of modification to any of the listed input files encourages Xcode not to run the script phase. Hooray! Fastness!
  2. Non-existence of any listed output file compels Xcode to run your script phase.
  3. The number of input and output file paths is passed to the script as ${SCRIPT_INPUT_FILE_COUNT} and ${SCRIPT_OUTPUT_FILE_COUNT} environment variables.
  4. Each input and output path is passed as e.g. ${SCRIPT_INPUT_FILE_0}, ${SCRIPT_OUTPUT_FILE_1}, etc.

In practice, what does this mean when you go looking to speed up your script phase? It means you should:

  1. List as an input every file or folder that may affect the results of your script phase.
  2. List at least one output, even if your script doesn’t leave any reliable artifacts.

One frustrating bug I should point out here is that Xcode (as of at least Xcode 6.0 and including 6.2 betas) is not as attentive as it should be about changes to the the input and output file lists. If you make changes to these lists, rebuild several times, and don’t see the efficiencies you expect being applied, you should close and reopen your Xcode project document. It seems to cache its notion about script phase dependencies, and will stubbornly stick to those rules until forced to re-evaluate them from scratch. I reported this to Apple as Radar #19233769.

Referring back to the screenshot above, you can that in this contrived example I have listed two input “files,” but in this case they are actually directories. This is to illustrate that if you list a directory, Xcode will trigger a re-running of your script phase whenever any shallow change occurs in that directory. For example, if I add, delete, or modify any files in ImportantStuff”, the script will run on the next build. However, if I modify a file within “ImportantStuff/Goodies”, it will not trigger the script to run because Xcode does not monitor for changes recursively.

You will also see that I’ve gone to the trouble of artificially adding a bogus file to the derived files folder, indicating the script has run. That has the effect of causing a “clean” of the project to wipe out the artificial file and thus cause the script phase to run again even though none of the input files may have changed. If I wanted to ensure that the script phase is skipped as aggressively as possible, I could provide e.g. a path to an output file that is guaranteed to exist, such as a stable file in my project like “$(SRCROOT)main.m”. Because the designation “Output File” in this case is only a hint to the Xcode build system, you don’t have to worry (and can’t expect) that the file will be created or altered unless you do so yourself in the script. This feels a little dodgy to me though, because Apple could choose to change the behavior in the future to e.g. imply that “clean” should delete all listed output files for a script phase.

Script build phases are a woefully under-documented, incredibly useful component of the Xcode build system. They are especially useful when they run blazingly fast or are skipped completely if not needed. I hope this information gives you the resources you need to take full advantage of them.