Isolating Xcode Builds

I maintain an extensive array of automated builds with Jenkins. The automatic reporting of inadvertent build failures empowers me to stay much more on top of the quality of my own code, which is shared across many projects and targets many platforms and OS releases.

Unfortunately, I have frequently run into trouble with “false negative” builds. Builds which fail for perplexing reasons that I can usually only chalk up to “welp, something changed in Xcode.”

Sometimes the problem has seemed to be rooted in its finicky behavior with respect to paths that have spaces in them. Fine, I’ve removed all the spaces from my paths.

Other times, the problems have been much more nuanced, having to do with the problematic (I guess) sharing of cached data between builds. I don’t know how much of this should be chalked up to the fact that I’m building multiple projects at once, or that I’m building multiple problems of the same name, but with different dependencies and platform target. In any case, I have come to realize that the key to making sure my builds, you know, build when they should is to isolate each build as much as possible from the others.

To this end, over the years I have taken advantage of Xcode build settings such as SYMROOT, the setting that defines where your “Built Products” should be stored. But that only affects where the final products are saved. To make sure intermediate files that are used to build a product don’t get mixed and matched, I also override OBJROOT.

Overriding those two gets you a long way towards isolating a build from other builds that may have happened recently, or that may be happening right now. But, as it turns out, they don’t get you far enough.

Apple has increasingly moved towards some valuable changes in their approach to handling “cache” type data, including precompiled headers, header maps, module caches, etc. To the extent that Apple can use these caching strategies to enable faster builds, I’m all for it. But on my build server, it’s inevitably led to confusion and disarray.

Recently I’ve run up against confounding build errors that I can’t quite trace to specific problems. All I know is that “wiping out the Derived Data folder” solves it. Adding yet more build setting overrides to my build process also seems to solve some of the issues. For example, faced with a litany of precompiled header errors along the lines of:

fatal error: file 'BlahBlah.h' has been modified since the precompiled header 'WhooHah_Prefix-fmvtpvxgevrsamfytqmaabjnyjyx/WhooHah_Prefix.pch.pch' was built

I have found some relief by adding another override, this time for the SHARED_PRECOMPS_DIR. Recently, I ran up against a similar kind of failure, presumably related to the fact that I have started taking advantage of Apple’s support for “modules”:

fatal error: malformed or corrupted AST file: 'Unable to load module "[...]/DerivedData/ModuleCache/LQKN4K4Q4BR6/Foundation-1SPG61SLRK6SC.pcm": module file out of date'

That’s fine, but where is the “modules” equivalent of SHARED_PRECOMPS_DIR? It probably exists, though it’s not documented on the aforelinked build settings reference. Worse, since the advent of the DerivedData folder, it seems some of the claims in the build settings reference about default values are no longer true.

“Fine,” I thought, “how the heck do I override the Derived Data folder location altogether?” I don’t know why it took me so long to pursue this line of thinking. Perhaps my allegiance to the idea of saving effort through shared caching was too strong for my own good. When it comes down to it: I don’t mind that builds take a little longer or use more disk space, so long as they are an utmost reliable reflection of the current status of my source bases.

But how do you override the Derived Data folder location? If there is a build setting for it, then like the module caches folder, is evidently too new to be well-documented. Examining the build process in Xcode, it seems to be defined at a higher level than any of the other environment-variable based settings that can be observed in the build log e.g. for custom build phases.

I know you can set the Derived Data folder location in Xcode’s UI, but that, I assumed, only affects the builds that happen within the app. The brilliant idea finally dawned on me to check the xcodebuild man page, and what do you know?

-derivedDataPath path

Overrides the folder that should be used for derived data when performing a build action on a scheme in a workspace.

Since the vast majority, if not all, of the various cache and cache-like folders I’m trying to override are in fact located by default within the “Derived Data” folder, overriding just it could be the lynchpin that ensures my builds are isolated from one another.

I adjusted my build scripts to no longer override OBJROOT, CACHE_ROOT, or SHARE_PRECOMPS_DIR. Instead, they simply pass -derivedDataPath to xcodebuild with a suitably unique path:

xcodebuild -project MyProj.xcodeproj -scheme "MyScheme" -derivedDataPath "./MyCache"

You know what? Suddenly all my Jenkins builds are passing all their tests. Son of a gun.