...

понедельник, 9 декабря 2013 г.

[Из песочницы] Continuous Integration для Android с использованием Jenkins + Gradle

Хочу поделится своими наработками по автоматической сборке 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 через консоль (на случай если нет иксов)


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий