Android CI on DEV@cloud
Today's blog is a guest blog by Ashley Willis, Android developer extraordinare. Read all about the nifty project she did for K-9 Mail, using BuildHive and DEV@cloud!
In my spare time (which I've had a lot of recently), I've been developing for K-9 Mail. We recently had a security/privacy bug fix go out that broke K-9 on older Android versions (I won't mention any names), and then the quick fix for that didn't work for some users with a certain setup. Yes, we've had a complete lack of QA, but I can count on one hand the number of active developers we have—and, no, I'm not counting in binary!
Shortly after that someone mentions BuildHive. I decide to check it out, thinking it might help us avoid such situations in the future (assuming we actually write some tests to cover more than 5% of our code). BuildHive is quick and easy to get started with if you're using GitHub (which K-9 is), but it doesn't quite meet my needs. DEV@cloud, also from CloudBees, gives the full Jenkins product, though, and you don't have to be using GitHub, or even Git. Also, CloudBees is nice enough to have special accounts for FOSS projects, which is great as firing up an Android emulator and running tests on it takes some time, and then you might want to test on every target you support for your stable branch.
Building an Android project on CloudBees shouldn't be any different than building any other Java project—it's getting the emulator going that's the challenge. There's an Android Emulator plugin for Jenkins, but it, among some other Jenkins plugins, are not available on free accounts. And even if you do have access to it, it starts up before anything else is done, wasting valuable EC2 time if your build horribly fails and there's nothing to install on it (it seems like a great plugin if you're running your own Jenkins multi-core server, though). So, I wrote a Bash script (available here ) which does much of what the plugin does, but when I want it done—after a successful compile (which could be slower with an emulator running in the background).
For our Jenkins setup, I created a job named controller. Miscellaneous options I filled in include our Google code website and GitHub project (which I think is not needed as CloudBees has no special permissions on our public repo), as well as a description.
I also have a Job Notification Endpoint pointing to a port on my computer that lets me know when jobs are started and finished—this was useful before we had access to the IRC plugin which is not standard with free accounts. Below that is the option to Restrict where this project can be run where you can enter m1.large if your account has access to that. I do this when I'm creating a new AVD snapshot as they take quite some time, otherwise I don't restrict and it defaults (usually?) to m1.small.
Since we are using GitHub, I selected Git under Source Code Management . I have the Repository URL as git://github.com/k9mail/k-9.git, then click on Advanced and set the Name as origi n and the Refspec as +refs/heads/controller:refs/remotes/origin/controller. Then for Branches to build I have origin/controller, then click on Advanced and select Fast remote polling . All this gets this job to pay attention to only the controller branch, and I have another job for our stable branch.
Under Build Triggers I selected Build when a change is pushed to GitHub so that whenever anyone pushes to our repo, DEV@cloud will automatically build and test. Under Build Environment I selected Add timestamps to the Console Output so I can see how long various steps take.
Now onto the main Build section. You can add various steps here, from invoking Ant or Maven, installing Android packages if using the Android Emulator plugin, and so on, but I only have one—Execute shell , which contains a list of commands to run:
# THIS SETS UP THE ANT ANDROID BUILD PROPERTIES rm -f local.properties; $ANDROID_HOME/tools/android update project --path ./; cp -f local.properties tests/;
# path to cloudbees private storage: export PRIVATE=${JENKINS_HOME/home/private} PRIVATE=${PRIVATE%/hudson_home}
# always sign with the same debug key: mkdir -p ~/.android cp -f $PRIVATE/debug.keystore ~/.android/
# build the project: cd tests/ ant all clean bash $PRIVATE/build-cb.sh emma debug artifacts
# start/create the emulator: export AVD_NAME=android-7 export AVD_TARGET=android-7 bash $PRIVATE/auto-avd.sh -n $AVD_NAME -t $AVD_TARGET -c 10M source $WORKSPACE/.adbports
# do tests and such: ANDROID_ADB_SERVER_PORT=$ANDROID_ADB_SERVER_PORT bash $PRIVATE/build-cb.sh -Dadb.device.arg=-e emma installd test cd .. bash $PRIVATE/build-cb.sh javadoc > javadoc.log # the log is ignored, but building javadoc spews tons of junk in general ANDROID_ADB_SERVER_PORT=$ANDROID_ADB_SERVER_PORT bash $PRIVATE/build-cb.sh lint-xml #monkey
# fix output from running via build-cb.sh: eval `grep -P '^(DIST|BETA)_' $PRIVATE/build-cb.sh` find javadoc/ lint-results.xml monkey.txt tests/coverage.xml tests/junit-report.xml -type f -print0 | \ xargs -0 perl -pi -e"s|$BETA_TLD/$BETA_DOMAIN/$BETA_PROJECT|$DIST_TLD/$DIST_DOMAIN/$DIST_PROJECT|g" find javadoc/ lint-results.xml monkey.txt tests/coverage.xml tests/junit-report.xml -type f -print0 | \ xargs -0 perl -pi -e"s|$BETA_TLD\.$BETA_DOMAIN\.$BETA_PROJECT|$DIST_TLD.$DIST_DOMAIN.$DIST_PROJECT|g" find javadoc/ -type f -print0 | xargs -0 perl -pi -e"s|$BETA_LOGTAG|$DIST_LOGTAG|g" if [[ "${DIST_TLD}" != "${BETA_TLD}" ]]; then mv javadoc/${BETA_TLD} javadoc/${DIST_TLD} fi if [[ "${DIST_DOMAIN}" != "${BETA_DOMAIN}" ]]; then mv javadoc/${DIST_TLD}/${BETA_DOMAIN} javadoc/${DIST_TLD}/${DIST_DOMAIN} fi if [[ "${DIST_PROJECT}" != "${BETA_PROJECT}" ]]; then mv javadoc/${DIST_TLD}/${DIST_DOMAIN}/${BETA_PROJECT} javadoc/${DIST_TLD}/${DIST_DOMAIN}/${DIST_PROJECT} fi
# kill the emulator: kill `cat /tmp/$USER-$AVD_NAME.pid` ANDROID_ADB_SERVER_PORT=$ANDROID_ADB_SERVER_PORT $ANDROID_HOME/platform-tools/adb kill-server
I've added some extra build targets to our Ant project for lint-xml, javadoc, and monkey (commented out above); as well as artifacts, which copies both the project and the test project to unique names based on Jenkins build properties. I also overrode Android's test target in tests/build.xml in order to get code coverage and the JUnit report in XML format. For the JUnit support, I replaced the default test runner with android-junit-report by dropping the source in tests/src/.
Also notice the environment variables set above: AVD_NAME=android-7 and AVD_TARGET=android-7. AVD_TARGET must be set to a valid target installed on DEV@cloud (they have the standard ones and then some), where as AVD_NAME you can set to whatever you want. I'm thinking about adding a feature to the script to randomly or sequentially go through the different AVDs, one per build, unless the testing failed on the previous build. On our stable branch I plan on going through all the generic targets we support. The script that creates and starts AVDs is auto-avd.sh , which must be called by bash since CloudBees private storage is mounted without execute permissions.
Finally concerning the building, some Ant targets are called with build-cb.sh (available in the auto-avd repo) instead of ant. This Bash script with Perl one-liners temporarily renames the project (it can also be used to completely fork a project with an entirely new name), so that the created apk files have a different project name. This allows the build and tests to be installed and ran on any device which already has our project installed without interfering with the active installation, and so we can get a better idea of what might be going wrong on a particular user's device. You'll need to edit its variables to suit your project before using it. The above code also fixes the reports and Javadoc so they don't have the temporary name in them.
Last but not least, after the build is complete, I have the following Post-build Actions selected. These publish the various reports and do notifications. You might not have all of these with a free account, and some of these require installing the Jenkins plugins at
https://<account-id>.ci.cloudbees.com/pluginManager/ :
Publish Android Lint results with the file lint-results.xml
Scan for compiler warnings with Parser Java Compiler (javac)
Archive the artifacts with files bin/K9Cloud-controller-*.apk,tests/bin/K9CloudTests-controller-*.apk
Publish Android monkey tester result with filename monkey.txt
Publish JUnit test result report with file tests/junit-report.xml
Publish Javadoc with directory javadoc
Record Emma coverage report with file tests/coverage.xml
E-mail Notification with Send separate e-mails to individuals who broke the build checked
IRC Notification with Notification Strategy set to all and Channel Notification Message set to Summary, SCM changes and failed tests .
When it's all finished, our IRC channel is notified with a summary, including which tests failed if any. Also, Javadoc, Lint, EMMA coverage, and JUnit reports are published on the job page, and the artifacts are available for download and can be tested by anybody. And that's all pretty nifty. :)
Hopefully I've made these scripts general-purpose enough for any project, but they might require some tweaks depending on how your project is laid out. If you have suggestions for improvement, please let me know—preferably via a pull request or issue report at https://github.com/ashleywillis/auto-avd/ .
Ashley Willis
To read Ashley's original blog: http://blog.androgynoid.com/2012/06/android-ci-on-devcloud.html
Stay up to date
We'll never share your email address and you can opt out at any time, we promise.