# Continuous Integration for Android Applications

In our quest to make everything as automated as possible, the SeatGeek dev team has been using Travis-CI to build our Android application apks automatically. Since the process of doing Android-based CI is a bit difficult to get correct without trial and error, we’ll document it here for everyone else to use!

## A few caveats

Travis-CI does not support Java 6 - Oracle JDK 7 and 8 are available, as is the OpenJDK. We encountered an issue related to jar-signing, which was easily fixed by adding the following to our app’s pom.xml file:

<!-- Fix for jar signing with java 1.7
http://stackoverflow.com/questions/8738962/what-kind-of-pitfals-exist-for-the-android-apk-signing/9567153#9567153 -->
<configuration>
<arguments>
<argument>-sigalg</argument><argument>MD5withRSA</argument>
<argument>-digestalg</argument><argument>SHA1</argument>
</arguments>
</configuration>


Travis sometimes upgrades their infrastructure. You may need to install packages from source to get everything working. The only time this has bitten us is with the Maven version, which we’ll discuss below.

Travis currently does not have the android SDKs by default, so you need to manage this yourself. We ended up building a script to cache them, though there is an outstanding PR that may fix the issue, so be on the lookout.

## Initial Setup

Since we upload APKs to S3, we’ve included a few gems in our Gemfile:

source 'https://rubygems.org'

gem 'travis', '1.5.4'
gem 'travis-artifacts', '~>0.1'


In our .travis.yml, we’ll want to specify the java build-type and the jdk version:

language: java
jdk: oraclejdk7


Next, you’ll want to specify some environment variables for the ANDROID_TARGET and ANDROID_ABI. SeatGeek uses a single matrix entry, though you are welcome to build for multiple targets:

env:
matrix:
- ANDROID_TARGET=android-18  ANDROID_ABI=armeabi-v7a


We also upload the build apk to our S3 for exposure using our build-artifacts application, so we have some S3-related environment variables. Note that we encrypt our S3 access_key_id and secret_access_key, which is something I highly recommend doing:

env:
global:
- "ARTIFACTS_AWS_REGION=us-east-1"
- "ARTIFACTS_S3_BUCKET=shared_bucket"
# travis-artifacts ARTIFACTS_AWS_ACCESS_KEY_ID
- secure: "SOME_SECURE_ACCESS_KEY_ID"
# travis-artifacts ARTIFACTS_AWS_SECRET_ACCESS_KEY
- secure: "SOME_SECURE_SECRET_ACCESS_KEY"


I recommend enabling the apt cache. We need to install some system packages in order to run the android build tools, so this will cut down some time from your build. In our testing, it was ~4 minutes.

cache:
- apt


If you have setup the build-artifacts tool, then you’ll need a notification entry for the app:

notifications:
webhooks:
urls:
# build-artifacts app
- http://ancient-foot-stomps-alligator.herokuapp.com/travisci/


## before_install: The Meat and Potatoes

Next is the before_install step. This is the meat of our setup, and was the result of quite a bit of trial and effort. I’ll be candid and state it took ~100 builds to figure out everything. The android devs were confused about whether the app actually worked, to say the least.

before_install:
- sudo apt-get install -qq --force-yes expect libgd2-xpm ia32-libs ia32-libs-multiarch s3cmd > /dev/null

- export ANDROID_HOME=${HOME}/android-sdk-linux/ - export ANDROID_APP_DIR=${PWD}
- export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${HOME}/.bundle/ruby/1.9.1/bin
- export GEM_PATH=${HOME}/.bundle/ruby/1.9.1:${GEM_PATH}

# Install 3.0.5 maven
- bin/install_maven

# Install Ruby requirements
- bin/cache_deps -a $ARTIFACTS_AWS_ACCESS_KEY_ID -b$ARTIFACTS_S3_BUCKET -d 'Gemfile.lock' -e $HOME -f ".bundle" -i "bin/install_gems" -p "android/bundles" -s$ARTIFACTS_AWS_SECRET_ACCESS_KEY

# Install Android requirements
- bin/cache_deps -a $ARTIFACTS_AWS_ACCESS_KEY_ID -b$ARTIFACTS_S3_BUCKET -d 'deps.txt' -e $HOME -f "android-sdk-linux" -i "./bin/install_sdk" -p "android/deps" -s$ARTIFACTS_AWS_SECRET_ACCESS_KEY


I’ll explain this step-by-step.

1. We need to install some system packages for the android dev tools. They won’t run without these, unfortunately, and the packages aren’t included by default in TravisCI.
2. We set some android-related environment variables to ensure the app can be properly built. The PATH and GEM_PATH are overriden because we want to specify where the build tools live, as well as use a custom path for the bundle requirements.
3. If you use maven in any way, then your best bet is to force the installation of maven 3.0.x. This ticket explains it in more detail than I can in a few words.
4. At the time I originally implemented this, there was no bundler caching. There is bundler caching built into Travis-CI now, but it is untested in our setup. Probably works, so worth trying!
5. We cache all android sdk requirements using a custom dependency file which I’ll outline below. Trying to hit the Android repositories for the SDK is error-prone, and you’ll eventually be throttled, which balloons a 4-minute build to a 45-minute build.

## Helper scripts

You’ll notice we have two scripts in use, cache_deps, install_maven, install_gems, and install_sdk. They are outlined below:

### install_maven

The install_maven command was actually created today - we realized recent updates in TravisCI broke all of our builds, and the change that broke things was upgrading to Maven 3.1.1. Thankfully the awesome folks at Travis lent me a built-vm to futz with and I was able to fix the issue. This can’t be easily fixed on their end without requiring everyone use an older version of Maven, which is undesirable, so this script is available for all to use!

#!/usr/bin/env bash

VERSION=3.0.5

if [ -d /usr/local/maven-3.* ]; then
echo "- Removing existing maven 3.x installation"
sudo rm -fr /usr/local/maven-3.*
fi
if [ -L /usr/local/maven ]; then
echo "- Removing old maven symlink"
sudo rm /usr/local/maven
fi

echo "- Downloading maven ${VERSION}" curl -O http://apache.mirrors.tds.net/maven/maven-3/$VERSION/binaries/apache-maven-$VERSION-bin.tar.gz 2>/dev/null retval=$?
if [ $retval -ne 0 ]; then echo "- Failed to download maven" exit$retval
fi

echo "- Extracting maven ${VERSION}" tar -zxf apache-maven-$VERSION-bin.tar.gz > /dev/null
retval=$? if [$retval -ne 0 ]; then
echo "- Failed to extract maven"
exit $retval fi echo "- Moving maven${VERSION} to /usr/local/maven-${VERSION}" sudo mv apache-maven-$VERSION /usr/local/maven-$VERSION retval=$?
if [ $retval -ne 0 ]; then echo "- Failed to extract maven" exit$retval
fi

echo "- Symlinking /usr/local/maven-${VERSION} /usr/local/maven" sudo ln -s /usr/local/maven-$VERSION /usr/local/maven
retval=$? if [$retval -ne 0 ]; then
echo "- Failed to extract maven"
exit $retval fi echo "- Updating alternatives for maven" sudo update-alternatives --install /usr/bin/mvn mvn /usr/local/maven-$VERSION/bin/mvn 1
retval=$? if [$retval -ne 0 ]; then
echo "- Failed to update package alternatives"
exit $retval fi echo "- Maven${VERSION} successfully upgraded!"


The script is pretty self-explanatory, but feel free to ping us with questions on it.

### cache_deps

We wrote this script to cache a directory of dependencies to S3 and retrieve them for later use. It uses an md5 sha of the specified dependency file to figure out if it needs to regenerate the cache. It is loosely based on the wad.

This is untested outside of the usage in this blog post, though it should work for Python requirements as well, as the idea is the same.

This command requires s3cmd, which we installed in our before_install step.

#!/usr/bin/env bash

ARCHIVE_FOLDER_NAME=false
ARTIFACT_PREFIX=""
DEPENDENCY_FILE=false
EXTRACT_PATH=false
INSTALL_COMMAND="true"
ROOT_PATH=pwd
S3_ACCESS_ID=false
S3_BUCKET="shared_bucket"
S3_SECRET_KEY=false

LOG () { echo -e "[LOG] $1"; } RUNCOMMAND () { echo -e "[CMD]$1" && eval $1; } while getopts "a:b:d:e:f:h:i:p:r:s:" opt do case$opt
in
a)
S3_ACCESS_ID=$OPTARG ;; b) S3_BUCKET=$OPTARG
;;
d)
DEPENDENCY_FILE=$OPTARG ;; e) EXTRACT_PATH=$OPTARG
;;
f)
ARCHIVE_FOLDER_NAME=$OPTARG ;; h) echo "cache_deps" echo "" echo "usage:" echo " -a S3_ACCESS_ID - S3 Access ID" echo " -b S3_BUCKET - S3 Bucket name" echo " -B ARCHIVE_BASE - Base path to where the folder being archived" echo " -d DEPENDENCY_FILE - File that manages dependencies" echo " -e EXTRACT_PATH - Path to extract dependencies into" echo " -f ARCHIVE_FOLDER_NAME - Name of folder to archive" echo " -h - This help screen" echo " -i INSTALL_COMMAND - Command to run to install dependencies" echo " -p ARTIFACT_PREFIX - Prefix to use for artifact uploads" echo " -s S3_SECRET_KEY - S3 secret key" exit 0 ;; i) INSTALL_COMMAND="$OPTARG"
;;
p)
ARTIFACT_PREFIX=$OPTARG ;; r) ROOT_PATH=$OPTARG
;;
s)
S3_SECRET_KEY=$OPTARG ;; \?) echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done

ROOT_PATH=${ROOT_PATH%/} EXTRACT_PATH=${EXTRACT_PATH%/}

unamestr=uname
if [[ "$unamestr" == 'Linux' ]]; then ARCHIVE_NAME=$(md5sum $DEPENDENCY_FILE | awk '{ print$1 }')
else
ARCHIVE_NAME=$(md5$DEPENDENCY_FILE | awk '{ print $4 }') fi GZIP_FILENAME="${ARCHIVE_NAME}.tar.gz"
GZIP_FILEDIR="${ROOT_PATH}/tmp" GZIP_FILEPATH="${GZIP_FILEDIR}/${GZIP_FILENAME}" S3_PATH="${ARTIFACT_PREFIX}/${GZIP_FILENAME}" setup () { if which s3cmd >/dev/null; then echo ""; else LOG "Missing s3cmd in PATH" exit 1 fi RUNCOMMAND "ensure_config" RUNCOMMAND "get_archive" if [$? -eq 0 ]; then
LOG "Archive installed"
else
LOG "Archive not available on S3"
RUNCOMMAND "install_dependencies"
installed=$? if [$installed -eq 0 ]; then
RUNCOMMAND "put_archive"
else
echo "Failed properly fetch or install archive. Please review the logs."
exit 1
fi
fi

return $? } get_archive () { RUNCOMMAND "s3_read" if [$? -eq 0 ]; then
LOG "S3 Read succeeded, extracting archive to ${EXTRACT_PATH}" RUNCOMMAND "tar -xzf${GZIP_FILEPATH} -C ${EXTRACT_PATH}" return$?
fi

return 1
}

install_dependencies () {
LOG "Installing dependencies"
RUNCOMMAND $INSTALL_COMMAND return$?
}

put_archive () {
RUNCOMMAND "zip_archive"
if [ $? -eq 0 ]; then RUNCOMMAND "s3_write" fi return$?
}

if [ -f $GZIP_FILEPATH ]; then LOG "Removing archive from filesystem" rm -rf$GZIP_FILEPATH
fi

LOG "Trying to fetch Wad from S3"
RUNCOMMAND "mkdir -p $GZIP_FILEDIR" RUNCOMMAND "s3cmd get s3://$S3_BUCKET/$S3_PATH tmp/$GZIP_FILENAME >/dev/null"
return $? } zip_archive () { LOG "Creating Wad with tar ($GZIP_FILEPATH)"
RUNCOMMAND "tar -czvf $GZIP_FILEPATH -C$EXTRACT_PATH $ARCHIVE_FOLDER_NAME" return$?
}

s3_write () {
LOG "Trying to write Wad to S3"
RUNCOMMAND "s3cmd put --acl-public $GZIP_FILEPATH s3://$S3_BUCKET/$S3_PATH >/dev/null" if [$? -eq 0 ]; then
return 0
else
LOG "Failed to write to S3, debug with 'wad -h'"
return 1
fi
}

ensure_config () {
tee ~/.s3cfg > /dev/null <<EOF
[default]
access_key = $S3_ACCESS_ID secret_key =$S3_SECRET_KEY
EOF
}

setup
rm -rf ~/.s3cfg


### install_gems

Pretty simple script, it simple installs ruby gems:

#!/usr/bin/env bash

bundle install --path ~/.bundle --without='development production' --deployment


### install_sdk

This script is a bit odd. It deals with the absolute nonsense that is the download api for the Android SDKs. I cannot guarantee this will always work - there is a hack for a case where it stopped working because installing sysimg-18 required two agreements be accepted - but it does currently work.

We use a deps.txt file format that is pretty simple to grok:

platform-tools
tools
build-tools-18.0.1
android-18
android-17
extra-android-m2repository
extra-android-support


No tricks there, just a text file with the SDK elements we want installed. The following is the script itself:

#!/usr/bin/env bash

LOG () { echo -e "[LOG] $1"; } RUNCOMMAND () { echo -e "[CMD]$1" && eval $1; } DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

if [ -z "$ANDROID_HOME" ]; then ANDROID_HOME="${PWD}/android-sdk-linux/"; fi
echo $ANDROID_HOME exit # Install base Android SDK RUNCOMMAND "wget http://example.com/android-sdk_r22.0.5-linux.tgz" RUNCOMMAND "tar xzf android-sdk_r22.0.5-linux.tgz" # install android build tools # - sudo apt-get install --force-yes unzip RUNCOMMAND "wget http://EXAMPLE.COM/build-tools_r18.0.1-linux.tgz" RUNCOMMAND "tar -xf build-tools_r18.0.1-linux.tgz -C$ANDROID_HOME"
RUNCOMMAND "mkdir -p $ANDROID_HOME/build-tools/" RUNCOMMAND "mv$ANDROID_HOME/android-4.3 $ANDROID_HOME/build-tools/18.0.1" # Install required Android components. # For a full list, run android list sdk -a --extended # Note that sysimg-18 downloads the ARM, x86 and MIPS images (we should optimize this). # Other relevant API's for dep in cat$DIR/../deps.txt; do
echo "Installing $dep" expect <<DONE set timeout -1 # install dependencies spawn android update sdk --filter$dep --no-ui --force --all
match_max 1000000

# Look for prompt
expect "*?$y\/n$*"

# Accept the prompt

send -- "yes\r"

# send blank line (\r) to make sure we get back to gui
send -- "\r"

expect eof
DONE
done

echo "Installing sysimg-18"
expect <<DONE
set timeout -1
# install dependencies
spawn android update sdk --filter sysimg-18 --no-ui --force --all
match_max 1000000

# Look for prompt

# Accept the prompt
send -- "yes\r"

# Look for prompt

# Accept the prompt
send -- "yes\r"

# send blank line (\r) to make sure we get back to gui
send -- "\r"

expect eof
DONE


A few notes:

• You’ll need to specify a url to the base Android SDK. This isn’t provided by us. We upload ours to S3.
• You’ll also need to specify a url for the android build_tools. Again, we suggest using S3.
• If you use different version of the sdk than we do, then feel free to modify this script.

## Finishing up

We’ll want to skip the installation of requirements, since we more or less took care of it in the before_install. Feel free to move that to this section. We simply skipped it for simplicity:

install: true


Next, we’ll want to actually run tests. Since we use maven, performing a mvn clean package will not only create a package, but will also ensure that all tests pass before doing so:

script: mvn clean package


For those who wish to use our build-artifacts tool, you’ll want to use the travis-artifacts gem to upload your apk. Here is what we do:

after_success:
- "cd android/target"
- "bundle exec travis-artifacts upload --path android-1.0-SNAPSHOT-aligned.apk --target-path android-app/\$TRAVIS_BUILD_NUMBER"
- "bundle exec travis-artifacts upload --path android-1.0-SNAPSHOT-aligned.apk --target-path android-app/latest"


Note that these are production releases. Debug code is turned off, so if you want to enable that, you’ll need to adjust your maven settings.

## Some closing thoughts

One advantage to our setup is that it allows developers to write code without necessarily having an Android dev installation setup. I use it for this at my home computer when quickly testing bug fixes. While it isn’t a complete solution, it does bring us to that last mile.

A few possible improvements:

• Use bundler caching instead of a custom caching script
• Switching to native Android SDK support should simplify the setup of an Android application, hopefully removing much trial and error.
• Another possible success would be to use HTTP caching. Travis does offer this, but you’ll need to ask directly, and it’s likely not going to be approved for large files which you should place in your S3 ;)
• Fixing Maven 3.1.x support will remove a bit of the complexity, though that seems unlikely to occur in the near future.
• Build times aren’t extremely quick. On a recent Macbook Pro, we see build times of a minute or less. Travis averages ~5 minutes for us. This is therefore better used for branch integration, and potentially for creating releases for the Play Store.

We’re pretty happy with the final outcome, and while we have had to work on it every so often, the tweaks are pretty easy and usually apply to our Vagrant development environment.

Shout out to the Travis-CI folks for being so supportive as we’ve abused their systems :)