Continuous Integration для Android с использованием Jenkins + Gradle

в 7:50, , рубрики: android, gradle, Разработка под android, метки: ,

Хочу поделится своими наработками по автоматической сборке Android приложения. В этой статье я приведу пример сборки для двух типов приложений, первый — простое приложение, содержащее в отдельной папке unit-тесты, второй — приложение использующее проект-библиотеку (android library project).

В конечном итоге мы получим отчет о выполненных тестах а так же подписанный apk файл, доступный для скачивания из артефактов сборки Jenkins.

Требования для настройки автоматических сборок

  1. Необходимые плагины Jenkins
    • Android Emulator Plugin
    • Git Plugin (если вы используете git)
    • Multiple SCMs Plugin (для возможности работать с несколькими репозиториями)
    • Xvnc Plugin (для возможности запуска эмулятора, если на сервере не установлен X server)
  2. Android SDK
    Загружаем отсюда. Раздел DOWNLOAD FOR OTHER PLATFORMS -> SDK Tools only
  3. Gradle
    Лучше скачать архив тут (gradle-**-bin.zip) и распаковать в /usr/local/lib
  4. Переменные окружения
    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. Библиотека для запуска эмулятора
    На случай если Jenkins работает на 64-битной ОС, необходимо добавить библиотеку ia32-libs, иначе эмулятор не запустится
    sudo apt-get install ia32-libs

Когда все требования выполненны, приступаем к настройке

В своей работе я использую Eclipse, поэтому проекты имеют, так сказать, старую структуру, не типичную для gradle проектов (такую как создает androidStudio). Дальнейшие примеры будут приведены исходя из того что проект выглядит таким образом:

Project
 |-res
 |-src
 |-assets
 |-libs
 |-tests (папка содержащая проект с unit-тестами)
     |-src
     |-res
     |-AndroidManifest.xml
 |-AndroidManifest.xml
 |-build.gradle
 |-gradle.properties

Настройка

Для начала настроим обычный проект. Первым делом необходимо создать файл build.gradle, в корне проекта

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

Для запуска тестов, достаточно указать секцию instrumentTest в android.sourceSets, в которой будут указаны пути к папкам в тестовом проекте. Когда я разбирался с Gradle видел не одну статью, в которой было написано что для запуска unit-тестов необходимо создать отдельную задачу, создать для нее отдельную запись в sourceSets и добавить в зависимости junit. В общем только зря потратил на это время, все гораздо проще.
Часть конфига, отвечающую за подписывание приложения, опишу немного ниже. После создания конфига приступим к настройке Jenkins.

Настройка Jenkins

Первым делом необходимо указать репозиторий с проектом. Затем необходимо выбрать пункт Run an Android emulator during build в котором необходимо выбрать один из двух вариантов — либо указать имя существующего эмулятора, либо указать параметры для запуска нового. При этом оставить галочку на пункте Show emulator window
Следующим делом необходимо добавить шаг сборки Invoke Gradle script и указать необходимые команды для Gradle. Для сборки и прогона тестов достаточно указать build и connectedCheck
image

Последним делом необходимо добавить действия выполняемые после сборки.
1. Publish JUnit test result report (публикуем отчеты о выполнении unit тестов). В строке для xml файла, с отчетом о выполненных тестах, необходимо прописать следующий путь:
**/build/instrumentTest-results/*/*.xml
Gradle создает 2 типа отчета — html и xml. HTML отчеты можно найти в папке build/reports/instrumentTests/connected/, но для Jenkins необходимо указать xml отчет.
2. Заархивировать артефакты (даем возможность скачать скомпилированное подписанное прилоежение прямо из артефактов). Путь к файлу для архивации:
**/build/apk/workspace-release.apk
Теперь вернемся к подписыванию приложения

Подпись приложения

Для подписи приложения необходимо иметь ключ созданный с помощью утилиты keytool, в котором будут хранится данные о разработчике приложения. Стандартная команда для создания:
keytool -genkey -v -alias appAlias -keyalg RSA -keysize 2048 -keystore release.keystore -validity 10000
Параметр alias необходимо исползьовать для каждого приложения свой, его потом необходимо будет указывать при подписывании приложения.

В целях безопасности ключ не стоит держать под системой контроля версий, т.к. имея этот ключ можно подписать любое другое приложение, но системой оно будет распознаваться как одно и то же. Поэтому файл необходимо хранить непосредственно на сервере CI.
Исходя из этих соображений, я добавил в корень проекта файл gradle.properties, в котором указал всего пару настроек:

releaseSigningPropertiesPath=.androidSigning/releaseProperties
debugSigningPropertiesPath=.androidSigning/debugProperties

Значение releaseSigningPropertiesPath указывает на путь (относительно домашней директории, ~/.androidSigning/) по которому расположен файл с параметрами для ключей (пароли и alias). Для проекта в этой папке должно хранится 4 файла:
release.keystore — ключ для релизной сборки приложения
releaseProperties — параметры для релизного ключа
debug.keystore — ключ для дебаг сборки приложения
debugProperties — параметры для дебаг ключа

Каждый из файлов *Properties должен иметь следующую структуру:

keystore=файл-ключ (находящийся в этой директории)
keystore.password=пароль к хранилищу ключей
keyAlias=alias указанный при создании ключа
keyPassword=пароль к ключу

Пример:

keystore=release.keystore
keystore.password=mypassword
keyAlias=appAlias
keyPassword=mypassword

Все эти параметры указываются при создании ключа.

Все остальные действия были указаны в файле build.gradle. Чем удобен Gradle, тем что внутри конфига можно так же выполнять обычный java код, что и позволило сделать подобный механизм подписи. Все, теперь смело можно запускать сборку и получать подписанное и протестированное приложение.
Теперь давайте рассмотрим второй пример сборки приложения, которое использует приложение-библиотеку.

Сборка приложения использующего android-library

В моем случае проект и проект-библиотека находятся в различных репозиториях. Для возможности работать с несколькими репозиториями для Jenkins необходимо установить плагин Multiple SCMs Plugin. Первым делом опять необходимо создать конфиги для gradle. Первый файл находится в проекте-библиотеке

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

Основное отличие проекта-библиотеки это применяемый плагин: apply plugin: 'android-library'

Второй конфиг уже внутри проекта

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

Теперь необходимо правильно указать репозитории в Jenkins.
В отличии от предыдущего примера, в этом случае в разделе «Управление исходным кодом» необходимо выбрать не какую то конкретную систему контроля версий а «Multiple SCMs». После чего будет возможность добавить свою систему и указать путь к репозиторию. Первым необходимо указать репозиторий с проектом, вторым — с библиотекой, при этом для репозитория с библиотекой следует указать дополнительную настройку «Check out to a sub-directory», содержащую имя папки в которой будет находится код. Имя этой папки должно соответствовать имени проекта, которое мы указали в зависимостях внутри файла build.gradle (в примере это MyLibrary)
compile project(':MyLibrary')
Таким образом рабочая директория будет выглядеть как обычный проект, только с одной дополнительной папкой MyLibrary, в которой будет находится библиотека.

Все остальные настройки точно такие же как и для обычного проекта. Теперь можно указать параметры для gradle — clean build и получить готовое подписанное приложение. В случае если проект-библиотека содержит unit тесты, тут есть проблема, о ней расскажу в конце.

В качестве бонуса

В своем проекте мне приходится работать с различными окружениями. Например для тестовой версии приложения необходимо отправлять запросы, для получения каких либо данных, на тестовый сервер, в процессе разработки на локальный сервер. Таким образом можно выделить 3 типичных окружения dev, stage и prod. Настройки зависящие от окружения я разместил в ресурсах приложения, в файле res/values/environment.xml, в нем указан URL по которому необходимо обращаться за данными. Файлы с настройками для конкретных окружений я вынес в отдельную папку environment, в которой находятся 3 файла с настройками: dev.xml, stage.xml и prod.xml. Для того чтобы приложение работало с необходимым окружением нужно просто подставить один из этих файлов вместо environment.xml.
Для этого в Jenkins необходимо добавить первым шагом сборки запуск команды shell и указать следующую:
cp $WORKSPACE/environment/prod.xml $WORKSPACE/res/values/environment.xml

В завершении

Я не рассматривал в статье использование lint или proGuard. Ниже я добавил ссылку на техническую документацию, по запуску proGuard там все просто, можно посмотреть и настроить, а вот что касается поддержки lint, ее пока нет, обещают в скором времени добавить.

Я так и не смог разобратся с запуском unit тестов в библиотечном проекте. При запуске тестов возникает ошибка, которую так и не удалось решить. Я до сих пор не получил никакого ответа ни на stackoverflow ни в группе google. Надеюсь в ближайшее время этот вопрос решится.

Полезные ссыли
www.gradle.org/docs/current/javadoc/ — документация Gradle
tools.android.com/tech-docs/new-build-system/user-guide техническая документация
tools.android.com/recent/updatingsdkfromcommand-line — обновление Android SDK через консоль (на случай если нет иксов)

Автор: cooper0k

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js