Хочу поделится своими наработками по автоматической сборке Android приложения. В этой статье я приведу пример сборки для двух типов приложений, первый — простое приложение, содержащее в отдельной папке unit-тесты, второй — приложение использующее проект-библиотеку (android library project).
В конечном итоге мы получим отчет о выполненных тестах а так же подписанный apk файл, доступный для скачивания из артефактов сборки Jenkins.
Требования для настройки автоматических сборок
- Необходимые плагины Jenkins
- Android Emulator Plugin
- Git Plugin (если вы используете git)
- Multiple SCMs Plugin (для возможности работать с несколькими репозиториями)
- Xvnc Plugin (для возможности запуска эмулятора, если на сервере не установлен X server)
- Android SDK
Загружаем отсюда. Раздел DOWNLOAD FOR OTHER PLATFORMS -> SDK Tools only - Gradle
Лучше скачать архив тут (gradle-**-bin.zip) и распаковать в /usr/local/lib - Переменные окружения
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 - Библиотека для запуска эмулятора
На случай если 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, в корне проекта
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
Последним делом необходимо добавить действия выполняемые после сборки.
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. Первый файл находится в проекте-библиотеке
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'
Второй конфиг уже внутри проекта
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