Continuous Integration: Scripting Xcode Builds
In this tutorial series we will explore a rarely discussed (but highly valuable) process of developing software that is disappointingly absent in the iOS and mobile world: Continuous Integration.
Also available in this series:
- Continuous Integration: Series Introduction
- Continuous Integration: Tomcat Setup
- Continuous Integration: Hudson Setup
- Continuous Integration: Scripting Xcode Builds
- Continuous Integration: Script Enhancements
Where We Left Off
In part 1, we discussed the concept of Continuous Integration and how it can assist us in developing software faster. Part 2 went through installing "Apache Tomcat", the web server that runs our CI server software. In Part 3, we installed and configured Hudson to monitor our project and begin the build process whenever we update our project repository.
The main subject matter of this tutorial will be Bash scripting. Bash scripts allow us to automate a great many things such as adding users to a system, iterating and verifying all files in a group of folders, performing automatic archiving of old files, and a lot more. In this tutorial, we will discuss writing a Bash script that automatically builds and signs a ".ipa" file from our Xcode project.
Bash scripts work just like any other script or code you might have written before. We specify terminal commands to be executed in a specific order and the operating system is responsible for running them.
Step 1: Write A Basic Bash Script
As always the best way to learn about something is to jump into it, so lets practice writing a very basic script that will print your name onto into the terminal.
Open a text editor and create a new file on your desktop called 'BashScript.sh' end enter the following text:
#!/bin/sh echo "Hello world from Bash!"
You should be able to figure out what happens in this script. Firstly (and the less obviously) we specify the shell we will be using for the script, in this instance 'sh' (if you don't know what a 'shell' is don't worry too much, its not required to write basic Bash scripts. You just need to know that the line is there for now) The second line simply prints 'Hello there!' onto your terminal window.
Let's try running it. Open a new terminal window and type the following:
If you see "Hello world from Bash!" it all worked!
Step 2: Using Variables In Bash Scripts
Bash scripts can make use of variables but they can be a bit tricky to use if you haven't done them before, so we'll cover this very briefly before going any further.
Edit your code to say the following:
#!/bin/sh name="Frank" echo "Hello world from $name"
If you execute the script again you should see "Hello world from Frank" (or whatever name you decided to put there). We can also pass variables into the script from the command line. Change your 'name="frank"' line to:
and then execute the script from the terminal window but add a name on the end:
The script will use the variable that was passed in as variable '1' (in this case 'Frank').
These are the basics of using variables in Bash. Some common slip ups to avoid are:
- You must use double quotations (") when printing out a string containing a variables, or it will not work correctly.
- Variable declarations cannot have a space between the name and value. Eg
name = "Frank"will not parse correctly.
Now you've got the basics of Bash down! Now to put it to some use.
Step 3: Compile & Generate Your ".app"
There are two main steps to automating your build from a Bash script:
- Build your .app and dysm file
- Package and sign your .app into an .ipa
We use the "xcodebuild" command to compile our projects into a ".app" file, and we use "xcrun" to sign the app using your developer certificate and bunlde it into an IPA file.
Before we build it through our script, let's run the xcodebuild from the command line. Open a new terminal window on your Mac workstation and change directories to one of your projects. Once there, enter in the following command:
xcodebuild -target <Target Name>
If you provide an appropriate target name, you will see the app compile in the terminal window:
This builds the the .app and puts it into Xcode's Derived Data Directory located at:
This isn't a great place to store the build files though. If you look in the derived data location, you'll probably see a large collection of folders, sometimes with multiple versions of the same
app! Not practical at all. Instead, we'll decide where to store ours using optional parameters:
xcodebuild -target <target name> OBJROOT=/Users/<username>/Desktop/Obj.root SYMROOT=/Users/<username>/Desktop/sym.root
Run the build again and you'll notice two new folders on your desktop, obj.root and sym.root. The obj.root isnt too important for what we want to do but if you look in the sym.root you'll see our .app file along with a .dysm file.
For those of you who don't know, a .dysm file allows symbolication of crash logs. When you upload your binary to apple (or a service like TestFlight) it's usually recommended you upload the apps accompanying .dysm file, so its important to keep tabs on this.
Step 4: Sign And Bundle The App
Now we have the .app file we need to sign and bundle it. In your terminal window navigate to your sym.root folder:
And execute the following command:
xcrun -sdk iphoneos PackageApplication -v "AwesomeApp.app" -o "/Users/<username>/Desktop/AwesomeApp.ipa" --sign "<certificate name here>"
If all goes well you will see your .ipa on your desktop! If you are not sure what your certificate name is you can open your Xcode Project and see what your available certificate names are. In the below example my certificate name is "iPhone Developer: Aron Bury" (Although technically you should be using a Distribution Certificate for ad-hoc builds, unfortunately I didn't have one on this system).
It's important to note that we haven't specified a provisioning profile to be used. Xcodebuild and xcrun uses the profile that was last selected in the Xcode project settings.
Step 5: Write The Build Script
Now that we know how to do it from the command line, let's update our build script. Open your "build_script.sh" file (It should be located in the 'Scripts' folder in the top level of your projects working directory).
Delete the echo line so we have a blank script. First off we're going to add some variables that will make reusing the script easier:
#!/bin/sh appname="AwesomeApp" target_name="$appname" sdk="iphoneos" certificate="iPhone Distribution: Aron Bury" project_dir="$HOME/Documents/Apps/iOS/awesomeapp/$appname" build_location="$Home/Builds/$appname
An explanation of the variables are:
appname: The app name is the name of the app which we mostly use for our own references and directory structures.
target: The target name is the target that we will be building for (in this example it is the same as the app name so I have just passed it that variable).
sdk: This is the SDK that we will be building against. Generally speaking it is either the iPhone OS or for the simulator. We obviously want to build for the iPhone OS.
certificate: This is the name of the certificate in the keychain that we will be signing the app with.
project_dir: This is the directory of the project on your computer. The '$HOME' variable is an inbuilt system variable that relates to the home directory of the current logged in user.
build_location: This is where we will be storing the build files on the system. the .app and .ipa files will both be located here after the build.
Obviously you will need to modify the variable values to suit your own project values. Once you have done this (and double checked they are correct) add the following to your script, below the variable declarations:
if [ ! -d "$build_location" ]; then mkdir -p "$build_location" fi cd "$project_dir"
In the above code we check to see if the build location exists. If it doesn't we create it. We then tell the script to change directory into our project directory.
Finally we add the two commands to compile, bundle and sign our app. Add the following below the 'cd' command:
xcodebuild -target "$appname" OBJROOT="$build_location/obj.root" SYMROOT="$build_location/sym.root" xcrun -sdk iphoneos PackageApplication -v "$build_location/sym.root/Release-iphoneos/$appname.app" -o "$build_location/$appname.ipa" --sign "$certificate"
And that's a build script! Let's make sure it works. Save the script, open up a terminal window and execute the script. If all goes well, you will have the build files in the location we specified. If you get any errors, don't worry. Make sure you've specified your values correctly and make sure the app actually compiles (open the project in Xcode and make sure a build succeeds).
Step 6: Using The Script With Hudson
While the above script works great, the directory path wont be correct on our CI server. We need to make one small adjustment before we commit it to the repository. Do you remember last time when we used the '$WORKSPACE' variable in Hudson? We are going to pass that value into the script through Hudson. First change your project_dir line to:
And in Hudson, open your jobs system configuration and change the command to the following:
Save the changes to your script and Hudson job configuration and commit your changes to the repository. When Hudson detects a change it will update the script and execute it. If all goes well you should see the build succeed and all your desired files waiting for you in the 'Build' Directory of the servers logged on user!
Step 7: Organize A Method For Managing Multiple Developer Certificates
If there is one thing that will consistently break an iOS CI server, its not having the right certificates and/or provisioning profiles installed on the system. If a team member commits in an Xcode workspace with a certificate selected that the CI server does not have on the system, it will fail to compile and the build will break. This is because Apple requires any code compiled for a device to be signed by a certificate (separately from the signing of the app into an .ipa). For this reason it is wise for developers to have a system of managing this.
If your entire team is developing using a single certificate and private key then this isn't too much of an issue. If everyone uses a different certificate then everyone should export their developer and distribution certificate with their accompanying private keys and import them into the servers login keychain. This means no matter whose Xcode workspace Hudson works with, it will have the certificate to sign it.
There are more elegant ways to deal with this problem, which we will discuss in detail in the next tutorial.
We've got a working build server, and thats fantastic, but we are just scratching the surface of what we can accomplish! In the next tutorial we will cover how to implement automatic deployment, reading and writing to the info.plist, working with different configurations and working with the keychain. It's gonna be awesome. Catch you next time!
P.S: You can view and download the completed script here: