Changes to Xcode last year led to our then-current build scripts becoming dysfunctional. Since I had to make changes to the build scripts anyway, I started looking at how they could be improved. And the process of researching build script improvements led me to wonder if Xcode couldn't be persuaded to do more of the build process heavy lifting. The answer was, yes, it could. And the tweaks I made along the way simplified our build process and increased confidence that our dev builds would more accurately mirror our production builds.
The existing build script did a fair amount of work to tweak the project as necessary for the given build targets. We have separate builds for QA and Release, which have separate bundle IDs and display names. We do this so that QA engineers can have both QA and Release versions of our app on their devices. Each build also needs its own Provisioning Profile (Development, Store, and an Ad Hoc version for the QA build).
Did I mention that the app in question also has a watch component? And, as you may know, WatchOS apps and extensions each have their own bundle ID – which have to match up with each other and the iPhone app. Apple also requires that build and version numbers have to match up for all three. Finally, our app has a separate settings bundle for Development/QA vs. Release.
So, we had a build script that was non-trivial and our development builds done under Xcode had little relation to the QA and Release builds done from the terminal. It was natural, then, that I should do some poking around with Xcode to see if I could simplify the build script by making Xcode settings do more of the heavy lifting – which should also result in builds done from the IDE matching better to the terminal builds for QA and Release.
Working through all that left me with a wish list like this:
- Have one place to enter the version and build numbers that get used for all targets (app, watch app, watch extension).
- Separate bundle IDs and display names that don’t need to be changed.
- Have specific profiles that automatically get used for each of the builds.
- Have two settings bundles, one for Development/QA and one for Release. And each should get used at the appropriate time.
Is it possible, then, to handle all of this in Xcode settings? Yeah, pretty much. Here’s how I worked it out. There are six things you need to know about how Xcode settings work:
- When you create a project in Xcode, it automatically generates two build configurations for you: Debug and Release. But you can add more.
- Xcode build settings are hierarchical. There are a number of places where you can set build settings. Any good Xcode book will give you the complete list. But the part that’s interesting here is that settings made at the project level are available for every target.
- Settings set at the both the project and target levels can be different for each build configuration.
- It is possible to define and set user-defined settings.
- There is a setting that is honored by Xcode but is not set or shown by default that allows you to exclude specific files for particular configurations.
- Xcodebuild can set both the scheme and build configuration to use.
Those hints may be enough to set you on your way, but just in case you need a bit more, I’ll explain how I used those bits of knowledge to solve my problems.
The first thing I did was to create a third build configuration, QA, so that we’d have a separate build configuration for each build type. You do this by going to the project settings for your project. Then click on your project to show your project settings. Then choose the Info tab.
If you’re in the right place, you’ll see your current Configuration list. When you click on the plus button, you’ll have the opportunity to clone one of your existing configurations. Since our QA builds are similar to our Debug builds, I chose to base our QA configuration on our existing Debug configuration.
Version, Build Numbers, and Bundle Ids
Next, I created user-defined settings at the project level so they’ll trickle down. Click on your project, then go to the Build Settings tab. Click on the plus sign and choose to create a user-defined setting.
You can see that I set variables for version and build numbers as well as bundle IDs and display names. Notice that the bundle IDs and display names are different for each build configuration.
Then in the appropriate target plist, I referred to the settings created at the project level - $(PROJECT_VERSION) for example. For the WatchKit App, I was able to use $(BASE_BUNDLE).watchkitapp.
Setting up the provisioning profiles was easy once I had build configurations for each build type. You can set your provision files in either the general or Build Settings tabs for your target, but it’s easier to see what you’ve done in the Build Settings tab.
As I mentioned above, we have two different settings bundles for the development builds and the release build. The settings bundles allow an end user to provide settings for your iOS application in the iPhone or iPad Settings app. In our case, the app has additional settings for dev and QA users that allow us to turn on various bits of test code.
I finally figured out how to fulfill this requirement when I discovered the setting EXCLUDED_SOURCE_FILE_NAMES. In the data for this setting, you can list files that are part of the project, but should not be included for this build configuration.
I was then able to create two nearly identical settings bundles – one in a Release Directory and the other in a Debug Directory. Then in the Settings for the iPhone target, I created an EXCLUDED_SOURCE_FILE_NAMES setting. In the dev and QA build configurations, I excluded the release bundle, and in the release configuration, I excluded the debug bundle. It seems backwards, I know, but it does the trick.
Setting up the Scheme for the App
While not strictly required, I also modified the scheme for the iPhone app target. I modified the build configuration for the archive task to be QA instead of Release. That way, I could do a QA build from my work machine if necessary. This doesn’t interfere with the release builds because you can specify the build configuration to use in the Xcodebuild command line, which will override the setting in the scheme.
Once I had this all set up in the project, I could validate builds from the Xcode IDE and have a lot more confidence that the build would be correct. It also made creating a build script easier by a wide margin.