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. In part 4, we wrote a basic build script that compiled our project and generated an IPA file.
Where To Go From Here?
Right now we've got a functioning build script, but there is so much more we can do! This tutorial is a little different from the others. Instead of taking you through a series of steps, there will be a collection of helpful additions that you can add to your script and, depending on your circumstances, you can choose to add them or not.
Addition 1: Modify Your Script To Use Functions
Bash scripts can use functions just like other languages. Large build scripts can get pretty long, so it's important to organize it as much as possible. Functions are a great way of doing this.
Let's put all of our existing code into a function called "buildApp". Open your script and enter the following code:
function buildApp { #existing code goes here }
To call our "buildApp" function, simply enter the following below the function declaration:
buildApp
As you add more functionality to your script you can place them in differently named functions such as "distributeApp" or "signApp".
Addition 2: Uploading To TestFlight
One of the most popular services that developers use to help test their apps is TestFlight. TestFlight is a great (free!) online service that allows developers to easily upload their ad-hoc IPA and TestFlight takes care of the distribution.
TestFlight provides an upload API that we can call from our bash script. That means that whenever we complete a new build we can immediately upload it to test flight and inform our testers that a new build is available.
First, make sure you have an account and obtain your API token and team token. Once this has been done, add the API and team key to the top of your script as variables.
Secondly, we need to prepare the *.dysm file for upload. If you don't remember what the *.dysm file is for, refer back to article 4 for a refresher. The *.dysm file needs to be zipped before it can be uploaded to TestFlight or Apple, so it's a good idea to do this as part of the build process.
Add the following code after your "xcodebuild" command:
#zip dYSM file for distribution cd "$build_location/sym.root/$configuration-$sdk/" || die "no such directory" rm -f "$appname.app.dSYM.zip" zip -r "$appname.app.dSYM.zip" "$appname.app.dSYM"
The above code simply changes directory to the location of the *.dysm file and removes any zip files that may have existed before. Next, it creates a ZIP of the *.dysm file.
After this has been added, add the following function to your build script:
function deployToTestFlight { /usr/bin/curl "http://testflightapp.com/api/builds.json" \ -F file=@"$build_location/$appname.ipa" \ -F dsym=@"$build_location/sym.root/$configuration-$sdk/$appname.app.dSYM.zip" \ -F api_token="$TESTFLIGHT_APIKEY" \ -F team_token="$TESTFLIGHT_TEAM" \ -F notes="$appname uploaded via the testflight upload API"\ -F notify="False" }
Now, if you need to deploy to TestFlight, all you need to do is call your "deployToTestFlight'" function after a build has been generated.
For full information about TestFlight's upload API, check out https://testflightapp.com/api/doc/.
Addition 3: Working With The Info.plist
Sometimes, we might need to modify or read values from the *.plist file. For example, we might want to store builds for each version of the app. We can read the app version from the PLIST file and store it in an appropriate folder. We also might want to edit the icon used for a specific build, or sometimes even the bundle identifier.
To set the bundle ID (i.e. overwrite the original value), the command is:
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $bundle_id" Info.plist
As you can see from the above image, "CFBundleIdentifier" is the key for the bundle ID value, so we simply "Set" it as whatever the $bundle_id
value is.
If we want to read a value out of the PLIST and set it as a variable in our script, it is a bit trickier. We need to do a bit of bash trickery:
app_version_number=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" Info.plist)
The above code in the brackets simply prints out the value for 'CFBundleVersion' and the bash script captures that value as a variable and assigns it to the 'app_version_number' variable.
Addition 4: Building A Specific Project Configuration
In Xcode you can use different configurations for your project. Each configuration can use different build settings, code signing options, even compile differently based on pre-processor macros. Although creating new configurations, adjusting build settings, and adding pre-processor macros is beyond the scope of this tutorial, I can show you how to build for them.
The default configuration when you build is "Release", but this can be set with a special flag when calling the "xcodebuild" command.
In this example, we have a configuration called "Testing". To build for this configuration, we simply adjust our build script based on the following:
configuration="Testing" xcodebuild -target "$appname" -configuration "$configuration" OBJROOT="$build_location/$app_version/$configuration/obj.root" SYMROOT="$build_location/$app_version/$configuration/ sym.root"
Here we are building our Xcode project based on a specific configuration and putting it in its own folder along with its own version number (which we can obtain by looking at the info.plist, see addition 3).
Addition 5: Working With The Keychain
If you are working for a medium to large organisation the chances are you will be using more than one development/distribution certificate to sign your apps. If you aren't in control of creating these certificates there is also a chance that all the certificates will have your companies name somewhere in it.
This is a problem because the keychain doesn't allow signing of a certificate when it is "ambiguous". Luckily, we can use the "security" command to add and delete certificates during our build process.
First, you will need to export the relevant certificates and their private keys out of the keychain and add them to the script directory. When you export your certificate and key, the keychain will prompt you to enter a passphrase. Enter a passphrase and then save the *.p12 file into the scripts directory of your repository. Set the *.p12 file as a variable name in your script.
Second, delete any and all certificates on the build server that might conflict with the certificate to be used.
Finally, add the following line to import the certificate:
security import "$WORKSPACE/Scripts/$CERTIFICATE_FILE.p12" -P "$password" -A -k ~/Library/Keychains/login.keychain
This line imports the designated *.p12 into the login keychain using the password "$password".
When you have finished the build, remove it from the keychain like so:
security delete-certificate -c "$certificate"
Using the above method you can build your app multiple times using multiple certificates that otherwise would have been ambiguous with each other.
Conclusion
While these are useful additions to any build script, there will always be specific ways that you can make it work better for you. I've done my best to teach you the basics and provide enough of a foundation for you to have a solid base on which to experiment and develop further.
A script containing all the described steps above can be found at https://gist.github.com/1404526
I hope you enjoyed this tutorial series, and that you are able to implement CI and automated processes into your development workflow. May you have many blue build lights! :)
Comments