id: 161 View:article
Tutorials  Build  Gradle
The Signing Process

Release early, release often

This is the first in a short series collecting together a few techniques for enhancing the build process for Android. It's aimed any anyone starting a project who wants a little more productivity than the default startup wizards in Android Studio (3.1) offers.

Sometimes it can be useful to see how release builds measure up early on in a project's development.

This might be to keep an eye on the final APK size, to see what performance is like without the debug code or to make pre-releases available to testers, etc. Android Studio (3.1) does a great job of setting up the builds for new projects in debug, but the variant for release isn't really there from the get-go. This is most likely because everyone's signing process is different.

Sure, when you try to run in release mode you're prompted for all the signing credentials, which is OK for occasional signed builds. However, a solid, repeatable signing system, which works unaltered with automated building tools such as Jenkins, is useful to slot into the dev process early on.

The full project presented here is available on GitHub.

Signing credentials

The concern with signing is that the credentials must never be shared. This is a problem for a number of reasons - not least of which is each team member with access to the version control system could well end up checking them out, if not handled correctly.

What's needed is a kind of abstracted credential supplier, which is aware of where it needs to go to get the signing details, be it locally for a developer build or somewhere on a server for CM builds. Fortunately, Gradle supports this as does the CM used to illustrate the technique, Jenkins.

When you use the default project wizard in Android Studio (3.1) to create a project, a bunch of boilerplate code and support files are created for you. To control the builds, both app-level and module level build.gradle files are generated. If you try this, you'll also find two variants are created - debug and release. You see these in the "Build Variants" tab. The debug variant will run right away, but trying to run the release one pops up this dialog:

release run dialog 481x758

Notice the request to provide signing credentials at the bottom, along with the handy "Fix" tool which just prompts for them. The app-level build.gradle looks something like this:

app-level build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.otamate.android.myapplication"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation 'com.android.support:design:27.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

Notice an entry for "release" has been made under buildTypes. Under the hood, a release build invokes gradles assembleRelease task, which indeed requires the signing credentials. However, there's a cool feature which can automate it. Add something like the following so it looks like this:

app-level build.gradle

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        if (project.hasProperty("THE_SIGNING_PROCESS_SIGNINGPROPS")) {
            signingConfig signingConfigs.release
        }
    }
}

Properties to the rescue

Here we've added a condition to test for a project property, and declared a named signingConfig if it's found. Project properties are nothing more complicated than the key=value pairs found in the various config files which gradle reads. So if you had foo=bar in your local gradle.properties, that would return positive when tested with project.hasProperty("foo"). Crucially, your CM build system also reads these. This means we're on track for our remote builder using exactly the same config files as your local development system. We now need to declare the new signingConfig entry, but before we do let's consider gradle's support for automatically supplying the signing credentials. One way is to hardcode them directly:

app-level build.gradle

signingConfigs {
    release {
        storeFile file("release.keystore")
        storePassword "******"
        keyAlias "******"
        keyPassword "******"
    }
}

This solves the automation problem, but we shouldn't do it exactly like that because it forms part of build.gradle, which is checked in and therefore means the signing credentials will get shared. Fortunately, with a little help from our friends the properties, we can solve that:

app-level build.gradle

if (project.hasProperty("THE_SIGNING_PROCESS_SIGNINGPROPS")) {
    signingConfigs {
        release {
            Properties properties = new Properties()
            properties.load(file(THE_SIGNING_PROCESS_SIGNINGPROPS).newDataInputStream())
            project.ext.set("RELEASE_STORE_FILE", properties.getProperty('RELEASE_STORE_FILE'))
            project.ext.set("RELEASE_STORE_PASSWORD", properties.getProperty('RELEASE_STORE_PASSWORD'))
            project.ext.set("RELEASE_KEY_ALIAS", properties.getProperty('RELEASE_KEY_ALIAS'))
            project.ext.set("RELEASE_KEY_PASSWORD", properties.getProperty('RELEASE_KEY_PASSWORD'))

            storeFile file(project.RELEASE_STORE_FILE)
            storePassword project.RELEASE_STORE_PASSWORD
            keyAlias project.RELEASE_KEY_ALIAS
            keyPassword project.RELEASE_KEY_PASSWORD
        }
    }
}

This gives us the level of abstraction we require, and our CM system, Jenkins, can also support it. We are declaring a properties file who's name itself comes from a project property, in this case THE_SIGNING_PROCESS_SIGNINGPROPS. So we can have a local property file pointing to it in one build environment, and a remote one for CM pointing to it elsewhere, all without referring to it's contents and using exactly the same build files. This file can be called anything, all that matters is that it is referred to correctly later. Here's an example of one on a Windows system. It's location is C:\Users\Example\.gradle\signing.properties file:

signing.properties

RELEASE_STORE_FILE=/Signing/private.keystore
RELEASE_STORE_PASSWORD=secret
RELEASE_KEY_ALIAS=alias
RELEASE_KEY_PASSWORD=bar

A good place for a local developer to specify the filename/property is their local, private, system wide gradle.properties file, since this isn't checked in. Gradle searches for this file in the local path as part of it's file name resolution process. Notice this isn't the project's gradle.properties, since again that's usually checked in.

gradle.properties

THE_SIGNING_PROCESS_SIGNINGPROPS=C\:\\Users\\Example\\.gradle\\signing.properties

Note there can be as many entries in there as for projects which need signing - just add more key=value pairs.

Since the CM build system supports properties the same way, all that's needed is to configure the job which builds this project to set them up in the same manner. In Jenkins, this is on the Build section using a "Switches" entry. In our example, on a Jenkins  system running on Linux, it might be:

-PTHE_SIGNING_PROCESS_SIGNINGPROPS=/jenkins/signing/signing.properties

Only the Jenkins admin has access to that file, so security is preserved.

After using this, or something like it for a while, it soon becomes second nature for all new projects. Keep on releasing early, releasing often!