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

Where did that APK come from?

Welcome to the second in a short series covering how to give the Android build process a little boost from the get-go.

Once an APK is built and distributed, developers know they can kiss goodbye to controlling where it ends up. This can be a problem with testing, maintenance etc, and without further care such as clear versioning identification in the app itself, make tracing bugs harder. For example, if every released APK was just called MyApp.apk, how can you tell the one released this morning from the one released last month?

Versioning systems such as Subversion, Git etc support these id's, so it's possible to match up a particular check in with the files needed to create that APK. 

What's described here is a way to automatically add those version details to the produced APK file itself, so you'll end up with MyApp-debug-beta-1.1.0-368-dev.apk or whatever. The filename comes from a combination of the build variant name, the id in the version system and the source machine (eg. a CI build, or a local developer, "dev") which created it. The example uses subversion as the CM.

This project is available on GitHub.

It's all about tracing

The idea is to be able to see an APK in the wild and trace exactly how it was created. It's assumed a versioning system is being used, so the significant part of this is it's id. This will be available to local developers and the CI build system, since it's also assumed this release must have been checked out at some stage. We'll put aside issues of local modifications once it's been checked out, since this can't happen with CI and shouldn't happen when a developer is issuing local builds for others anyway.

We're running with Jenkins as the CI. The key to the magic of how the current version identifier is extracted, then made available to the build system, is the wonderful extensibility of gradle. The guys at TMate have made their svn -> java tool SVNKit available under an Open Source Licence, so with a little know-how it can be wired into gradle. That work begins with the project-level build.gradle, where we identify the repositories and dependencies:

project-level build.gradle

buildscript {
    
    repositories {
        google()
        jcenter()
        maven { url "https://maven.google.com" }
        maven { url "https://jitpack.io" }
    }
    
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.0'
        classpath group: 'org.tmatesoft.svnkit', name: 'svnkit', version: '1.8.10'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Now, when the various dependencies which must be satisifed to make theSVNKit API calls are resolved, the system knows which repository to look in. You can see how this works:

app-level build.gradle

apply plugin: 'com.android.application'

import org.tmatesoft.svn.core.wc.*

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.otamate.android.theversionprocess"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        ext.CMRevNumber = getSvnRevision()
        ext.CIBuildNumber = System.getenv("BUILD_NUMBER") ?: "dev"
        versionName "Beta-1.1.0-$CMRevNumber-$CIBuildNumber"
    }

    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            sourceSets.debug.resources.srcDirs = ['src/debug/res']
            buildConfigField 'Boolean', 'BUILD_EXPIRES', 'true'
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            debuggable false
            buildConfigField 'Boolean', 'BUILD_EXPIRES', 'false'
        }
    }

    applicationVariants.all { variant ->
        variant.outputs.all {
            outputFileName = getAppNameFromResource() + "-${variant.name}-${variant.versionName}.apk"
        }
    }
}

def getSvnRevision(){
    ISVNOptions options = SVNWCUtil.createDefaultOptions(true)
    SVNClientManager clientManager = SVNClientManager.newInstance(options)
    SVNStatusClient statusClient = clientManager.getStatusClient()
    SVNStatus status = statusClient.doStatus(projectDir, false)
    SVNRevision revision = status.getRevision()
    return revision.getNumber()
}

def getAppNameFromResource() {
    def stringResources = android.sourceSets.main.res.sourceFiles.find {
        it.name in ['strings.xml']
    }
    
    XmlParser parser = new XmlParser()
    String appName = parser.parse(stringResources).string.find {
        it.@name in ['app_name']
    }.text()
    
    return appName
}

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'
}

Most of the SVNKit calls are made in the getSvnRevision() method, which does the donkey work of asking the remote svn server for the current version and returning it as a suitably formatted string. This is set as the versionName entry for the build, which in turn is used to set the actual built filenames in the applicationVariants.all loop, which sets the filename for each variant in the build. Notice the hoops we have to jump through to get the app name in getAppName(). This is done so we don't have to repeat the app name defined in strings.xml, so extract it directly from inside that file.

If the system finds an environment variable named BUILD_NUMBER, it will append this to the filename, otherwise just use "dev". The reason for this is that Jenkins will populate this, whereas a local build machine will not. In short, if you see an APK ending in a build number, you know it's come from CM. If it end with "dev", its a developers local PC. Here's a few example of how the files look:

MyApp-release-Beta-1.1.0-490-11.apk	- release, Beta, svn rev 490, CI build 11
MyApp-debug-Beta-1.1.0-490-dev.apk	- debug, Beta, svn rev 490, a developers PC build

Hopefully it should be easy to see how further identification variables could be used, such as setting the actual developer's name, or the machine they used for the build.

In any case, tracing these builds should be a lot easier, and with it all being automated one less thing to worry about.