Saturday 4 January 2014

Xcode Build for iOS - conditional copy of resource files based on Build Configuration

A brief: Use Run Script build phase in Xcode to selectively copy resource files, such as Settings.bundle, security certificates, etc., depending on the build configuration, e.g. Debug or Release.
How-To: Project Navigator: select a project, select a target, select Build Phases tab; menu: Editor > Add Build Phase > Add Run Script Build Phase.

In detail:
Couple days ago I needed to make a minor change to how we compile/build our iOS app. The app has a few settings but all of them for development only. While preparing a submission to Apple, we wanted to remove the app from the Settings view on iPad completely. The challenge was to keep the settings for developer builds. This proved to be a bit more difficult than expected. Hence, a post to document how it can be done as well as a few things learned about Build Project settings, Targets, Build Configurations, Schemes, logs etc. in Xcode (v5).  

For an iOS app to have an entry to the standard Settings view, the app needs to include a Settings.bundle file (which is actually a directory on the file system, by the way). When the file is added to a project (any file for that matter actually), Xcode allows to selectively include it into the project's targets. Normally, an Xcode project would have the main target (named after the app) and a test target. So, one way to conditionally include a file, Settings.bundle in my case, into the build is to duplicate the main target and use that duplicated target for developer builds only. For example, let's say our app name is iStockFutures and by default the main target is iStockFutures. We could've duplicated that target into iStockFutures-Dev and kept the Settings.bundle file as a member of the iStockFutures-Dev target only. A sample screenshot is shown below.



That would accomplish the task. But there is drawback - having multiple targets means that developers have to be mindful when adding new files (any new file) and better not forget to include it into both targets. When running the app in Xcode, the dev target then should be used but when committing code to a build server, it had better be tested on both targets. Needless to say, my development team was not thrilled on that prospect. 

Luckily, there is a more transparent way:

The build process in Xcode includes multiple phases. One of them is Copy Bundle Resources. The phase has a list of resource files to copy. When the Settings.bundle was added to the project, Xcode automatically included it into that list. Unfortunately, Xcode 5 does not allow to have multiple versions of the list based on Configuration, e.g. Release or Debug. Not sure why Apple didn't do it, after all, such capability exists and is widely used in Build Settings. Anyway, this can be easily achieved by using a Run Script phase. Run Script is a feature in Xcode that allows to execute a custom script while building a Product (i.e. an App). Multiple script languages are supported (see the link above) but since all what we need to do is to copy a file, ah, sorry, I meant a directory, we just going to use the standard /bin/sh.
To create a Run Script phase, select the project in the Project Navigator, then make sure that a target is selected (otherwise the menu will be greyed out/disabled).
Then use the Editor menu to add a Run Script phase: Editor > Add Build Phase > Add Run Script Build Phase





When it's added, expand the Run Script phase and add this script (modify it as needed, of course) that copies Settings.bundle if build is run in Debug configuration:

echo "Checking configuration to determine whether to copy Settings.bundle: CONFIGURATION=$CONFIGURATION"
if [ "$CONFIGURATION" == "Debug" ]; then
echo "Copying ${SRCROOT}/${PRODUCT_NAME}/Settings.bundle directory to ${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app"
cp -R ${SRCROOT}/${PRODUCT_NAME}/Settings.bundle ${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Settings.bundle
echo "Settings.bundle directory has been copied."
else
echo "Skipped copying Settings.bundle - not required for $CONFIGURATION configuration."
fi

Now, remove the Settings.bundle from the target, iStockFutures in the sample app.
(There are at least 2 ways to do that - either edit the list under Copy Bundle Resources or select Settings.bundle in Project Navigator and uncheck all targets in the File Inspector's Target Membership [View > Utilities > Show File Inspector]).
This is important:

  • Run Product > Clean before running Product > Build, otherwise the Settings.bundle copied before will still be packaged into the app.
  • Uninstall the app from the device and/or simulator - that will remove the app entry from the Settings app.

Also, if you played with the solution presented first, i.e. a dev target, remember to remove that dev target.

Let's build the product in Debug configuration first and look into Xcode build log files to verify the script is being run.

Hint: Where to find Xcode build logs: from the menu: View > Navigators > Show Log Navigator


Select the default group and the All Messages option: 



This is it for building in Debug configuration.

To fully complete the work we need to verify it builds correctly in Release configuration as well. It can be done by running a command-line build configured to Release (a default configuration setting on the Project) on the team's CI (Continuous Integration) server. But of course a better approach is to test it on a developer's Mac beforehand.
An Xcode's feature called Scheme comes handy here (more info). It allows to maintain multiple sets of targets each configured to a specific Build Configuration, i.e. Debug or Release.

A new scheme can be created via the Product > Scheme menu.

Select Product > Scheme > Edit Scheme…, then Duplicate one of the existing schemes (1) and change the Build Configuration to Release (2):

 


After creating and configuring a new scheme, make it active by selecting it in Product > Scheme > iStockFutures-Release menu.
Build the app by running Product > Clean and Product > Build. When completed, check the build log file.



That should be all.

Ok, that's not been very complicated, why it took more time than expected? That's because a bulk went into attempting to figure out and tinkering with Xcode environment variables. 

Here is a couple things that helped.

How to print all Xcode environment variables:
Open a Terminal window, change the directory to the project directory, i.e. the one that contains <project_name>.xcodeproj file and run this command:

xcodebuild -project iStockFutures.xcodeproj -target "iStockFutures" -showBuildSettings > iStockFutures-build-settings.txt

All Xcode settings will be saved in the specified file. Replace iStockFutures with your project, of course.

Official Xcode Build Settings Reference doc from Apple:


And a last tip - for successful builds the log file does not show too many details. However, add a faulty Run Script build phase, something like this for example:

cp ${SRCROOT}/file-that-doesnot-exist.txt ${BUILT_PRODUCTS_DIR}/file-that-doesnot-exist.txt

Build fails and the log file can be expanded to see quite a bit of details; it might be handy in understanding how the build works:



Conclusion

By the way, the Run Script approach can be used to copy not only Settings.bundle but other environment-specific files. For example, we also used the script to copy Development and UAT/Production security certificates that we use for 2-way SSL/TLS authentication.