This should be easy, right?
Android applications are built with Java. Gradle is (finally) a mature and well-known build framework. Jenkins is a Java-first Continuous Integration platform and already has fantastic support for Gradle (as well as Maven and Ant) out of the box. So what’s the problem?
Getting an Android build set up in Jenkins is easy. Especially on a development box. If you’re part of a small, flexible team, own your company’s CI servers, or run the IT department, then there probably isn’t a problem. If you’re like us, though, you don’t always have the freedom to update your CI platform at-will.
We don’t always have the rights to install Jenkins plugins or manage an Android SDK installation directly on corporate infrastructure. We don’t always own the keys to sign our applications or follow the same processes internally as we do with our big clients. That’s just the nature of big enterprise platform services—changes to infrastructure need a paper trail, and ownership of build processes, security and development are often divided amongst different verticals.
Disclaimer up front: this article is not another tutorial for on-boarding your Android application to Jenkins CI. There’s already a lot of good information out there on Android and Jenkins, with solid tutorials using Ant, Maven, and Gradle. There’s even more on Android and Travis CI, Hudson, CircleCI—even Bamboo—if Jenkins isn’t your CI platform of choice.
This article is also not another preachy persuasive essay trying to convince you to use Android Continuous Integration. As long as you weren’t tricked into clicking here, you’re probably already on-board with CI anyway. And if you’re not, lots of others have provided plenty of reasons you should be in a much more eloquent manner than I’d be able to.
As consultants and enterprise mobile developers, we need tools that allow us to integrate Android projects into any organization’s continuous delivery platform. What follows are some common concerns we’ve encountered time and time again on both internal projects and with our clients, along with recipes to address them.
My Jenkins don’t speak Internet
At DevNexus last week, I attended a Gradle continuous delivery presentation by a developer evangelist at Gradleware. The presentation was solid, but there was a slide in there that grated on my nerves a bit. The delivery went something like this (paraphrased for brevity and bad memory):
- Gradle Evangelist: Gradle is great!
- Matt: <Nods head>
- Gradle Evangelist: Continuous Integration is great!
- Matt: <Nods head>
- Gradle Evangelist: Don’t worry about the Gradle version. We’ve got this thing called the Gradle wrapper that automatically downloads the right Gradle version from the internet!
- Matt: <Roars, flips table, tackles presenter>’
So… that’s not exactly how it went, though I did skip the head nod at that point. Newsflash to our amazing tool developers: our internal infrastructure doesn’t always have access to the outside internet!
This is just as big of a problem with the Android SDK and SDK Manager as it is with Gradle. And in all fairness to the Gradleware rep, the Gradle Wrapper isn’t limited to pulling distributions from the internet, as we’ll see in one of the tips below.
Tip 1A: Packaging and Installing Gradle in Jenkins
Version A of this is relatively simple. Simply download the Gradle binary distribution(s) that you want to support from https://gradle.org/downloads/ and install them on your Jenkins server.
Easy, right? Alas, not really.
In our experience, this introduces yet another problem: our ‘server’ is actually lots of Jenkins slaves and are controlled like a production environment. We need a repeatable process that we can ‘deploy’ to each of the slaves. Probably a no-brainer to some of you, but we found it was easy to wrap this up into an RPM, DEB, CAB, or MSI file (depending on the platform of your Jenkins servers). This makes it much easier to manage each version of Gradle as a separate module and pilot or moonlight various versions as needed.
Tip 1B: Using the Gradle Wrapper in Jenkins
As an alternative to installing Gradle distributions directly to your Jenkins cluster, you actually can use the Gradle Wrapper. This requires that you make the Gradle distributions available somewhere on your enterprise network and you update the wrapper configuration in each of your projects.
- Get your Gradle distributions added to a local binary repo or server. Example – add them to your on-premises Nexus or Artifactory instance.
- Update your gradle-wrapper.properties with the following:
distributionUrl=https\://path/to/gradle/zip
Tip 2: Packaging and Installing the Android SDK
- Android SDK Tools – This is the toolset that includes things like the SDK Manager used, in an ideal world, to download all other components. This is versioned, but you really only have one version ever installed at a time unless you manage multiple ANDROID_HOME’s. This install also includes certain tools that are required to build and package your applications.
- Android SDK(s) – These are the actual SDK versions that map to Android versions. You have multiple versions of these installed on a given system at the same time to support targeting multiple Android versions. Absolutely necessary to compile your Android applications. The SDK version is specified as part of your application’s build scripts.
- Android Build Tools – Specific build binaries used to compile your source to dex, process your application resources, and package your apk. You can have multiple versions of the build tools installed on a given system at the same time. The build tools version is specified as part of your application’s build scripts.
- SDK Tools v24.0.2 – https://dl.google.com/android/android-sdk_r24.0.2-linux.tgz
- Build Tools v21.1.2 – https://dl-ssl.google.com/android/repository/build-tools_r21.1.2-linux.zip
- SDK Level 21 (Lollipop) – https://dl-ssl.google.com/android/repository/android-21_r02.zip
Tip 3: Backwards Compatibility and ZipAlign
- Update every single one of your existing apps to use the new build tools. Easy if you’re a flexible, small development team. No thanks if this is an enterprise platform supporting lots and lots of applications and lots and lots of different development teams.
- Get the zipalign binary added back into the main SDK tools folder.
cp -p ./build-tools-r21.1.2/android-5.0.1/zipalign %{RPM_BUILD_ROOT}/destination/on/server/android-sdk-linux/tools/
No, you can’t share the signing key with the rest of the team.
Your signing key can be used as a trust identifier in application code, particularly amongst separate integrated apps, so its security is a big deal. I’ve heard of lots of development shops where the team’s signing key is committed to version control with passwords embedded in build scripts or where the key lives unprotected on the local machine of the team’s development lead. We’ve found that securing the signing key and configuration on build servers provides a good balance between maintainability and security, with little impact to your development team.
Tip 4: Inject your Signing Configuration, and Make it Optional
- Keep our signing configurations secure
- Make sure the signing configuration is used for each release build
- Reduce any impact on the development team from a process standpoint
// apply the signing configuration, if provided if (project.hasProperty("build_signingConfig") && new File(project.build_signingConfig).exists()) { logger.debug("[SIGNING CONFIG] Applying signing configuration to release builds from: " + project.build_signingConfig); apply from: project.build_signingConfig; }
- Allows us to use a default, non-prod signing config (or none at all) in our development environment and on developer machines. This can be defined in gradle.properties for development purposes and overwritten with a production key in our Jenkins build configuration.
- Allows us to externalize our entire signing configuration to a local, secure file on our Jenkins servers, outside of version control.
android { signingConfigs { release { storeFile file(“/path/to/signing/configuration.jks") storePassword "${System.env.KEYSTORE_PW}" keyAlias “alias" keyPassword “${System.env.ALIAS_PW}" } } }
Where the <bleep> did this APK come from?
Most of our large clients maintain a sizable portfolio of different internal applications. Each of these applications is owned by a different team with a different set of developers. However, there are almost always central support, app distribution, and release management teams, and these teams need some way of tracking where a given production artifact may have come from.
Tip 5: Embed build info directly in your release APKs
//add CI build meta data to the manifest, if available from env if (System.getenv("BUILD_TAG") && System.getenv("SVN_REVISION") && System.getenv("SVN_URL")) { android.applicationVariants.all { variant -> variant.outputs.each { output -> output.processManifest.doLast { copy { from("${buildDir}/intermediates/manifests/full") { include "${variant.dirName}/AndroidManifest.xml" } into("${buildDir}/intermediates/filtered_manifests") } def manifestFile = new File("${buildDir}/intermediates/filtered_manifests/${variant.dirName}/AndroidManifest.xml") def content = manifestFile.getText() def buildTag = System.getenv("BUILD_TAG"); def svnRev = System.getenv("SVN_REVISION"); def svnUrl = System.getenv("SVN_URL"); def updatedContent = content.replaceAll("", ""); manifestFile.write(updatedContent) } output.processResources.manifestFile = new File("${buildDir}/intermediates/filtered_manifests/${variant.dirName}/AndroidManifest.xml") } } }
Who forgot to increment the version Code?
Tip 6: Externalize your Application Version and Auto-Increment on Release
## Version name will get injected into the AndroidManifest.xml file at build time. ## The version code will be derived. ## Must match MAJOR.MINOR.RELEASE format. artifact_version=1.0.0
android { ... defaultConfig { ... versionCode buildVersionCode(artifact_version) versionName artifact_version } ... }
gradle.allprojects { ext.buildVersionCode = { version -> def majorMinorBuild = version.tokenize(".") def vCode = 0; def powerOfTen = 1; majorMinorBuild.reverse().eachWithIndex() { obj, i -> (vCode += (obj.toInteger() * powerOfTen)); powerOfTen *= 1000; } logger.info("Build version code [" + vCode + "] from version name [" + version + "].") return vCode; } }
task increaseVersion << { logger.debug(":increaseVersion - Incrementing Package and Manifest Version...") def propsFile = file("../gradle.properties") def propsText = propsFile.getText() def patternVersionNumber = Pattern.compile("artifact_version=(\\d+)\\.(\\d+)\\.(\\d+)") def matcherVersionNumber = patternVersionNumber.matcher(propsText) matcherVersionNumber.find() def majorVersion = Integer.parseInt(matcherVersionNumber.group(1)) def minorVersion = Integer.parseInt(matcherVersionNumber.group(2)) def buildVersion = Integer.parseInt(matcherVersionNumber.group(3)) propsText = matcherVersionNumber.replaceAll("artifact_version=" + majorVersion + "." + minorVersion + "." + ++buildVersion) propsFile.write(propsText) }
Note that you’ll still need to invoke a shell script or use a VCS plugin afterward to commit the updated gradle.properties file back to version control.
Next Steps
- Using the Jenkins Android plugin with a headless emulator to execute instrument tests
- Connecting Jacoco test coverage in Android with Sonar
- Direct deployment from Jenkins through a distribution platform like Airwatch, Crashlytics Beta, etc.
- Bundling my common Gradle scripts into a plugin that can be referenced as a build script dependency