I’ve previously discussed Continuous Integration for iPhone Projects in TeamCity using Xcode 3 and Building Xcode 4 Projects from the Command Line. Now I’ll tie those together and use TeamCity to automatically create ad hoc builds I can install over the air (directly onto a device without using iTunes) every time I check in code.
I created a basic project configuration in TeamCity 6 to checkout my iOS project from git.
I set the artifact paths for this configuration to
project_name.acceptance.app*
ad_hoc/*.png
This will eventually allow TeamCity to collect the icons, project_name.acceptance.app.ipa, and project_name.acceptance.app.plist files needed to perform an over the air install of the ad hoc build.
Finally I created the following custom script build step (available as https://gist.github.com/949831).
#!/bin/bash
# https://gist.github.com/949831
# http://blog.carbonfive.com/2011/05/04/automated-ad-hoc-builds-using-xcode-4/
# command line OTA distribution references and examples
# http://nachbaur.com/blog/how-to-automate-your-iphone-app-builds-with-hudson
# http://nachbaur.com/blog/building-ios-apps-for-over-the-air-adhoc-distribution
# http://blog.octo.com/en/automating-over-the-air-deployment-for-iphone/
# http://www.neat.io/posts/2010/10/27/automated-ota-ios-app-distribution.html
project_dir=`pwd`
# Configuration
environment_name="staging"
keychain="ci_keys"
keychain_password="super secret"
workspace="MyApp.xcworkspace"
scheme="Ad Hoc"
info_plist="$project_dir/MyApp-Info.plist"
environment_plist="$environment_name.plist"
environment_info_plist="$environment_name-Info.plist"
product_name="My App $environment_name"
mobileprovision="$project_dir/ad_hoc/MyAppStaging.mobileprovision"
provisioning_profile="iPhone Distribution: My Company, LLC"
build_number="%env.BUILD_NUMBER%"
artifacts_url="http://my_ci_server.example/artifacts/$build_number"
display_image_name="Icon-57.png"
full_size_image_name="Icon-512.png"
function failed()
{
local error=${1:-Undefined error}
echo "Failed: $error" >&2
exit 1
}
function validate_keychain()
{
# unlock the keychain containing the provisioning profile's private key and set it as the default keychain
security unlock-keychain -p "$keychain_password" "$HOME/Library/Keychains/$keychain.keychain"
security default-keychain -s "$HOME/Library/Keychains/$keychain.keychain"
#describe the available provisioning profiles
echo "Available provisioning profiles"
security find-identity -p codesigning -v
#verify that the requested provisioning profile can be found
(security find-certificate -a -c "$provisioning_profile" -Z | grep ^SHA-1) || failed provisioning_profile
}
function describe_sdks()
{
#list the installed sdks
echo "Available SDKs"
xcodebuild -showsdks
}
function describe_workspace()
{
#describe the project workspace
echo "Available schemes"
xcodebuild -list -workspace $workspace
}
function increment_version()
{
cd "MyApp"
agvtool -noscm new-version -all $build_number
cd ..
}
function set_environment()
{
#copy the info plist for the selected environment into place
cp -v "MyApp/$environment_info_plist" $info_plist || failed environment_plist
#copy the environment settings plist into place
cp -v "MyApp/$environment_plist" "MyApp/environment.plist" || failed environment
#extract settings from the Info.plist file
info_plist_domain=$(ls $info_plist | sed -e 's/\.plist//')
short_version_string=$(defaults read "$info_plist_domain" CFBundleShortVersionString)
bundle_identifier=$(defaults read "$info_plist_domain" CFBundleIdentifier)
echo "Environment set to $bundle_identifier at version $short_version_string"
}
function build_app()
{
local devired_data_path="$HOME/Library/Developer/Xcode/DerivedData"
#get the name of the workspace to be build, used as the prefix of the DerivedData directory for this build
local workspace_name=$(echo "$workspace" | sed -n 's/\([^\.]\{1,\}\)\.xcworkspace/\1/p')
#build the app
echo "Running xcodebuild > xcodebuild_output ..."
# disabled overriding PRODUCT_NAME, setting applies to all built targets in Xcode 4 which renames static library target dependencies and breaks linking
# xcodebuild -verbose -workspace "$workspace" -scheme "$scheme" -sdk iphoneos -configuration Release clean build PRODUCT_NAME="$product_name" >| xcodebuild_output
xcodebuild -verbose -workspace "$workspace" -scheme "$scheme" -sdk iphoneos -configuration Release clean build >| xcodebuild_output
if [ $? -ne 0 ]
then
tail -n20 xcodebuild_output
failed xcodebuild
fi
#locate this project's DerivedData directory
local project_derived_data_directory=$(grep -oE "$workspace_name-([a-zA-Z0-9]+)[/]" xcodebuild_output | sed -n "s/\($workspace_name-[a-z]\{1,\}\)\//\1/p" | head -n1)
local project_derived_data_path="$devired_data_path/$project_derived_data_directory"
#locate the .app file
# infer app name since it cannot currently be set using the product name, see comment above
# project_app="$product_name.app"
project_app=$(ls -1 "$project_derived_data_path/Build/Products/Release-iphoneos/" | grep ".*\.app$" | head -n1)
# if [ $(ls -1 "$project_derived_data_path/Build/Products/Release-iphoneos/$project_app" | wc -l) -ne 1 ]
if [ $(ls -1 "$project_derived_data_path/Build/Products/Release-iphoneos/" | grep ".*\.app$" | wc -l) -ne 1 ]
then
echo "Failed to find a single .app build product."
# echo "Failed to locate $project_derived_data_path/Build/Products/Release-iphoneos/$project_app"
failed locate_built_product
fi
echo "Built $project_app in $project_derived_data_path"
#copy app and dSYM files to the working directory
cp -Rf "$project_derived_data_path/Build/Products/Release-iphoneos/$project_app" $project_dir
cp -Rf "$project_derived_data_path/Build/Products/Release-iphoneos/$project_app.dSYM" $project_dir
#rename app and dSYM so that multiple environments with the same product name are identifiable
echo "Retrieving build products..."
rm -rf $project_dir/$bundle_identifier.app
rm -rf $project_dir/$bundle_identifier.app.dSYM
mv -f "$project_dir/$project_app" "$project_dir/$bundle_identifier.app"
echo "$project_dir/$bundle_identifier.app"
mv -f "$project_dir/$project_app.dSYM" "$project_dir/$bundle_identifier.app.dSYM"
echo "$project_dir/$bundle_identifier.app.dSYM"
project_app=$bundle_identifier.app
#relink CodeResources, xcodebuild does not reliably construct the appropriate symlink
rm "$project_app/CodeResources"
ln -s "$project_app/_CodeSignature/CodeResources" "$project_app/CodeResources"
}
function sign_app()
{
echo "Codesign as \"$provisioning_profile\", embedding provisioning profile $mobileprovision"
#sign build for distribution and package as a .ipa
xcrun -sdk iphoneos PackageApplication "$project_dir/$project_app" -o "$project_dir/$project_app.ipa" --sign "$provisioning_profile" --embed "$mobileprovision" || failed codesign
}
function verify_app()
{
#verify the resulting app
codesign -d -vvv --file-list - "$project_dir/$project_app" || failed verification
}
function build_ota_plist()
{
echo "Generating $project_app.plist"
cat << EOF > $project_app.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>$artifacts_url/$project_app.ipa</string>
</dict>
<dict>
<key>kind</key>
<string>full-size-image</string>
<key>needs-shine</key>
<true/>
<key>url</key>
<string>$artifacts_url/$full_size_image_name</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<true/>
<key>url</key>
<string>$artifacts_url/$display_image_name</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>$bundle_identifier</string>
<key>bundle-version</key>
<string>$short_version_string $build_number</string>
<key>kind</key>
<string>software</string>
<key>subtitle</key>
<string>$environment_name</string>
<key>title</key>
<string>$project_app</string>
</dict>
</dict>
</array>
</dict>
</plist>
EOF
}
echo "**** Validate Keychain"
validate_keychain
echo
echo "**** Describe SDKs"
describe_sdks
echo
echo "**** Describe Workspace"
describe_workspace
echo
echo "**** Set Environment"
set_environment
echo
echo "**** Increment Bundle Version"
increment_version
echo
echo "**** Build"
build_app
echo
echo "**** Package Application"
sign_app
echo
echo "**** Verify"
verify_app
echo
echo "**** Prepare OTA Distribution"
build_ota_plist
echo
echo "**** Complete!"
That is quite a few functions but the bottom of the script steps through them in what is hopefully an understandable sequence.
- Unlock the keychain containing the private key and provisioning profile used to sign the ad hoc build. Necessary since the TeamCity user’s login keychain may be locked when this build runs.
- List the available sdks on the build machine (unnecessary but I found it helpful when debugging build settings).
- List the schemes found in the workspace to be built (again purely for debugging).
- Copy a plist containing application settings to “environment.plist” which will be copied into the app bundle when built and used to define application behavior, for example it contains the url of the server this build should communicate with.
- Build the app using the specified workspace and scheme. Copy the resulting app and dSYM to the project directory so TeamCity can easily find them as build artifacts.
- Sign the app using the specified mobile provisioning profile and create a “.ipa” package.
- Verify that the app was successfully signed.
- Create a plist to allow over the air installation of the app.
Once run any device which has been added to the mobile provisioning profile used to sign this build can install the app just by visiting (using an appropriate %system.teamcity.buildType.id% for the TeamCity build configuration).
itms-services://?action=download-manifest&url=http://teamcity.example.com:8111/guestAuth/repository/download/%system.teamcity.buildType.id%/.lastSuccessful/project_name.acceptance.app.plist
Possible issues:
- Access to the keychain will present a security dialog the first time this build runs so it was necessary for me to sign into the TeamCity user’s account using VNC and allow access to that keychain.
- I found that builds occasionally failed to correctly create the CodeResources symlink so I recreate it manually. When this link was broken the ipa would fail verification.
- An iOS device won’t be able to install the app if the artifacts require authenticating to the TeamCity server so I enabled guest access. Alternately I could have exposed those artifacts through some other service and created a nice installation guide page which links to them if I didn’t want to allow guest access to my TeamCity server.
For additional resources for building over the air distribution builds take a look at:
Mike Nachbaur’s posts on “How to Automate your iPhone App Builds with Hudson” and “Building iOS Apps for Over the Air Ad Hoc Distribution“.
Vincent Daubry’s “Automating Over The Air Deployment for iPhone“.
Basil Shkara’s “Automated OTA iOS App Distribution“.
Pingback: Continuous Integration “ad hoc” builds for iOS apps with TeamCity and Xcode 4 | Gary Pallett
Pingback: Getting “Test”-y in iOS Apps: Test-Driven Development and Automated Deployment | The Carbon Emitter
Pingback: Xcode | Pearltrees
Pingback: Building iOS Apps in TFS - Visual Studio ALM + Team Foundation Server Blog - Site Home - MSDN Blogs