Continuous Integration for Android using Jenkins + Gradle

I want to share my best practices on the automatic assembly of Android applications. In this article I will give an example of assembly for two types of applications, the first is a simple application containing unit tests in a separate folder, the second is an application using the android library project.

In the end, we will receive a report on completed tests as well as a signed apk file, available for download from Jenkins build artifacts.

Requirements for setting up automatic assemblies


  1. Jenkins Essential Plugins
    • Android Emulator Plugin
    • Git Plugin (if you use git)
    • Multiple SCMs Plugin (for the ability to work with multiple repositories)
    • Xvnc Plugin (for the ability to run the emulator if the X server is not installed on the server)
  2. Android SDK
    Download from here . Section DOWNLOAD FOR OTHER PLATFORMS -> SDK Tools only
  3. Gradle
    It is better to download the archive here (gradle - ** - bin.zip) and unzip it to / usr / local / lib
  4. Environment variables
    ANDROID_HOME = / usr / local / lib / android / sdk
    GRADLE_HOME = / usr / local / lib / gradle-1.8
    JAVA_HOME = / usr / lib / jvm / jdk1.7.0_03
    PATH = $ PATH: $ ANDROID_HOME / tools: $ ANDROID_HOME / platform-tools: $ JAVA_HOME / bin: $ GRADLE_HOME / bin
  5. Library for starting the emulator
    In case Jenkins runs on a 64-bit OS, you need to add the ia32-libs library, otherwise the emulator will not start
    sudo apt-get install ia32-libs

When all the requirements are met, let's get started.

In my work I use Eclipse, so the projects have, so to speak, an old structure that is not typical for gradle projects (such as creates androidStudio). Further examples will be given based on what the project looks like this:
Project
 | -res
 | -src
 | -assets
 | -libs
 | -tests (folder containing the project with unit tests)
     | -src
     | -res
     | -AndroidManifest.xml
 | -AndroidManifest.xml
 | -build.gradle
 | -gradle.properties

Customization


First, set up a regular project. The first step is to create the build.gradle file, in the root of the project
buile.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.6+'
    }
}
apply plugin: 'android'
android {
    compileSdkVersion 18
    buildToolsVersion "18.1.1"
    defaultConfig {
    	minSdkVersion 8
    	targetSdkVersion 18
    	testPackageName "com.project.tests"
        testInstrumentationRunner "android.test.InstrumentationTestRunner"
    }
    sourceSets {
    	main {
    	    manifest.srcFile file('AndroidManifest.xml')
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
    	}
    	instrumentTest {
            java.srcDirs = ['tests/src']
            manifest.srcFile file('tests/AndroidManifest.xml')
            java.srcDirs = ['tests/src']
            resources.srcDirs = ['tests/src']
            res.srcDirs = ['tests/res']
            assets.srcDirs = ['tests/assets']
        }
    }
    dependencies {
    	compile fileTree(dir: 'libs', include: '*.jar')
    }
    //Загружаем значения для подписи приложения
    if(project.hasProperty("debugSigningPropertiesPath") && project.hasProperty("releaseSigningPropertiesPath")) {
        //Файлы в которых хранятся значения для подписи
        File debugPropsFile = new File(System.getenv('HOME') +  "/" + project.property("debugSigningPropertiesPath"))
        File releasePropsFile = new File(System.getenv('HOME') +  "/" + project.property("releaseSigningPropertiesPath"))
        if(debugPropsFile.exists() && releasePropsFile.exists()) {
            Properties debugProps = new Properties()
            debugProps.load(new FileInputStream(debugPropsFile))
            Properties releaseProps = new Properties()
            releaseProps.load(new FileInputStream(releasePropsFile))
            //Дописываем в конфиг загруженные значения
            signingConfigs {
                debug {
                    storeFile file(debugPropsFile.getParent() + "/" + debugProps['keystore'])
                    storePassword debugProps['keystore.password']
                    keyAlias debugProps['keyAlias']
                    keyPassword debugProps['keyPassword']
                }
                release {
                    storeFile file(releasePropsFile.getParent() + "/" + releaseProps['keystore'])
                    storePassword releaseProps['keystore.password']
                    keyAlias releaseProps['keyAlias']
                    keyPassword releaseProps['keyPassword']
                }
            }
            buildTypes {
                debug {
                    signingConfig signingConfigs.debug
                }
                release {
                    signingConfig signingConfigs.release
                }
            }
        }
    }
}
To run the tests, it is enough to specify the instrumentTest section in android.sourceSets, in which the paths to the folders in the test project will be indicated. When I was sorting out with Gradle, I saw more than one article in which it was written that to run unit tests you need to create a separate task, create a separate entry for it in sourceSets and add junit depending on it. In general, I just wasted my time on this, everything is much simpler.
The part of the config that is responsible for signing the application will be described below. After creating the config, proceed to configure Jenkins.

Jenkins setup


The first step is to specify the repository with the project. Then you need to select Run an Android emulator during build in which you need to choose one of two options - either specify the name of an existing emulator, or specify the parameters for launching a new one. In this case, leave a checkmark on the Show emulator window item.
Next, you need to add the Invoke Gradle script assembly step and specify the necessary commands for Gradle. To build and run the tests, it is enough to specify build and connectedCheck.
image

The last thing you need to do is add the actions performed after the build.
1. Publish JUnit test result report(we publish reports on the execution of unit tests). In the line for the xml file, with the report on the completed tests, it is necessary to write the following path:
**/build/instrumentTest-results/*/*.xml
Gradle creates 2 types of reports - html and xml. HTML reports can be found in the build / reports / instrumentTests / connected / folder, but for Jenkins you must specify an xml report.
2. Archive artifacts (we give the opportunity to download a compiled signed application directly from the artifacts). File path for archiving:
**/build/apk/workspace-release.apk
Now back to signing the application

Application signature


To sign an application, you must have a key created using the keytool utility, which will store data about the application developer. The standard command for creating:
keytool -genkey -v -alias appAlias -keyalg RSA -keysize 2048 -keystore release.keystore -validity 10000
The alias parameter must be used for each application, it will then need to be specified when signing the application.

For security reasons, the key should not be kept under the version control system, as having this key, you can sign any other application, but the system will recognize it as one and the same. Therefore, the file must be stored directly on the CI server.
Based on these considerations, I added the gradle.properties file to the project root, which indicated just a couple of settings:
releaseSigningPropertiesPath=.androidSigning/releaseProperties
debugSigningPropertiesPath=.androidSigning/debugProperties
The value of releaseSigningPropertiesPath indicates the path (relative to the home directory, ~ / .androidSigning /) along which the file with the parameters for the keys (passwords and alias) is located. For the project, 4 files should be stored in this folder:
release.keystore - key for the release build of the application
releaseProperties - parameters for the release key
debug.keystore - key for the debug build of the application
debugProperties - parameters for the debug key

Each of the * Properties files should have the following structure:
keystore=файл-ключ (находящийся в этой директории)
keystore.password=пароль к хранилищу ключей
keyAlias=alias указанный при создании ключа
keyPassword=пароль к ключу

Example:
keystore=release.keystore
keystore.password=mypassword
keyAlias=appAlias
keyPassword=mypassword
All these parameters are specified when creating the key.

All other actions were specified in the build.gradle file. What makes Gradle convenient, because inside the config you can also execute regular java code, which allowed us to create a similar signature mechanism. That's it, now you can safely run the assembly and get a signed and tested application.
Now let's look at a second example of building an application that uses a library application.

Building an application using android-library


In my case, the project and the library project are in different repositories. To be able to work with multiple repositories for Jenkins, you must install the Multiple SCMs Plugin plugin. The first thing again is to create configs for gradle. The first file is in the library project
library build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.6.3'
    }
}
apply plugin: 'android-library'
android {
    compileSdkVersion 18
    buildToolsVersion "19.0.0"
    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 18
        testPackageName "com.project.tests"
        testInstrumentationRunner "android.test.InstrumentationTestRunner"
    }
    sourceSets {
        main {
            manifest.srcFile file('AndroidManifest.xml')
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }
        instrumentTest {
            manifest.srcFile file('tests/AndroidManifest.xml')
            java.srcDirs = ['tests/src']
            resources.srcDirs = ['tests/src']
            res.srcDirs = ['tests/res']
            assets.srcDirs = ['tests/assets']
        }
    }
    dependencies {
        compile fileTree(dir: 'libs', include: '*.jar')
    }
}
The main difference of the project library is the applied plugin: apply plugin: 'android-library' The

second config is already inside the project
project build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.6.3'
    }
}
apply plugin: 'android'
android {
    compileSdkVersion 18
    buildToolsVersion "19.0.0"
    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 18
    }
    sourceSets {
        main {
            manifest.srcFile file('AndroidManifest.xml')
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }
    }
    dependencies {
        compile fileTree(dir: 'libs', include: '*.jar')
        compile project(':MyLibrary')
    }
    //Загружаем значения для подписи приложения
    if(project.hasProperty("debugSigningPropertiesPath") && project.hasProperty("releaseSigningPropertiesPath")) {
        //Файлы в которых хранятся значения для подписи
        File debugPropsFile = new File(System.getenv('HOME') +  "/" + project.property("debugSigningPropertiesPath"))
        File releasePropsFile = new File(System.getenv('HOME') +  "/" + project.property("releaseSigningPropertiesPath"))
        if(debugPropsFile.exists() && releasePropsFile.exists()) {
            Properties debugProps = new Properties()
            debugProps.load(new FileInputStream(debugPropsFile))
            Properties releaseProps = new Properties()
            releaseProps.load(new FileInputStream(releasePropsFile))
            //Дописываем в конфиг загруженные значения
            signingConfigs {
                debug {
                    storeFile file(debugPropsFile.getParent() + "/" + debugProps['keystore'])
                    storePassword debugProps['keystore.password']
                    keyAlias debugProps['keyAlias']
                    keyPassword debugProps['keyPassword']
                }
                release {
                    storeFile file(releasePropsFile.getParent() + "/" + releaseProps['keystore'])
                    storePassword releaseProps['keystore.password']
                    keyAlias releaseProps['keyAlias']
                    keyPassword releaseProps['keyPassword']
                }
            }
            buildTypes {
                debug {
                    signingConfig signingConfigs.debug
                }
                release {
                    signingConfig signingConfigs.release
                }
            }
        }
    }
}

Now you need to correctly specify the repositories in Jenkins.
Unlike the previous example, in this case, in the "Source Code Management" section, it is necessary to select "Multiple SCMs" rather than a specific version control system. After which it will be possible to add your system and specify the path to the repository. The first is to specify the repository with the project, the second is with the library, while for the repository with the library you should specify the additional setting “Check out to a sub-directory”, containing the name of the folder in which the code will be located. The name of this folder should match the name of the project that we specified in the dependencies inside the build.gradle file (in the example, this is MyLibrary)
compile project(':MyLibrary')
Thus, the working directory will look like a normal project, with only one additional MyLibrary folder in which the library will be located.

All other settings are exactly the same as for a regular project.

As a bonus


In my project I have to work with various environments. For example, for the test version of the application, it is necessary to send requests, to receive any data, to the test server, during the development process, to the local server. Thus, 3 typical environments dev, stage and prod can be distinguished. I set up environment-dependent settings in the application’s resources, in the res / values ​​/ environment.xml file, which contains the URL at which you need to apply for data. I put the settings files for specific environments into a separate environment folder, which contains 3 settings files: dev.xml, stage.xml and prod.xml. In order for the application to work with the necessary environment, you just need to substitute one of these files instead of environment.xml.
To do this, in Jenkins, you need to add the shell command launch as the first step in the assembly and specify the following:
cp $WORKSPACE/environment/prod.xml $WORKSPACE/res/values/environment.xml

Useful links
www.gradle.org/docs/current/javadoc - Gradle
tools.android.com/tech-docs/new-build-system/user- guide technical documentation
tools.android.com/recent/updatingsdkfromcommand-line - updating the Android SDK via the console (in case there are no X)
vimeo.com/34436402 - video explaining the work of gradle wrapper

Also popular now: