Пользователь
0,0
рейтинг
9 декабря 2013 в 11:50

Разработка → 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, в которой будет находится библиотека.

Все остальные настройки точно такие же как и для обычного проекта.

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


В своем проекте мне приходится работать с различными окружениями. Например для тестовой версии приложения необходимо отправлять запросы, для получения каких либо данных, на тестовый сервер, в процессе разработки на локальный сервер. Таким образом можно выделить 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

Полезные ссыли
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 через консоль (на случай если нет иксов)
vimeo.com/34436402 — видео поясняющее работу gradle wrapper
Bondar Sergey @cooper0k
карма
10,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (4)

  • 0
    1)
    Когда я разбирался с Gradle видел не одну статью, в которой было написано что для запуска unit-тестов необходимо создать отдельную задачу, создать для нее отдельную запись в sourceSets и добавить в зависимости junit. В общем только зря потратил на это время, все гораздо проще.

    Вы путаете юнит тесты с интеграционными в Android.
    Юнит тесты принято класть в src/test/java, запускать отдельным такском и исполнять их на jvm, а не на эмуляторе, используя либо обычный jUnit, либо Robolectric

    2) зачем устанавливать и настраивать на CI сервере gradle если можно использовать gradle wrapper и не париться с установкой и синхронизацией версий

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

    для этого в gradle android плагине есть build flavors.
    создаете несколько типов вашего приложения, подставляете им разные ресурсы и градл сам соберет несколько сборок, смотрящих на разные сервера. почитайте документацию по вашей ссылке

    • 0
      1. Спасибо, не знал.
      2. Да, действительно использовать wrapper гораздо проще и удобней, спасибо за подсказку. Не стал описывать как настроить, просто добавил в статью ссылку на видео, поясняющее его настройку, там все просто.
      3. На сколько я понял flavors позволяет указать другой пакет приложения, т.е. это отлично подходит для различных версий приложений, например, платная и бесплатная версии. Либо, я так понимаю, необходимо использовать gradle структуру проекта, где у каждого варианта будет своя папка res, я пока не готов перейти к этому, ждем stable версию AndroidStudio.
  • 0
    не, flavors позволяет указать не только пакет, там достаточно много конфигураций.
    gradle структура проекта вам не навязывается, вы можете создать папки с ресурсами под каждый build flavor в любом удобном для вас месте, а затем указать на них в sourceSets для каждого flavor
  • 0
    А Gradle точно необходим для решения такой задачи? Со сборкой android вполне справляется и ant

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.