Сборка Android-проекта в Docker-контейнере

  • Tutorial
Разрабатывая проект под платформу Android, даже самый небольшой, рано или поздно приходится сталкиваться с окружением для разработки. Кроме Android SDK, необходимо чтобы была последняя версия Kotlin, Gradle, platform-tools, build-tools. И если на машине разработчика все эти зависимости решаются в большей мере с помощью Android Studio IDE, то на сервере CI/CD каждое обновление может превратиться в головную боль. И если в web-разработке, решением проблемы окружения стандартом стал Docker, то почему-бы не попробовать решить с помощью него аналогичную проблему и в Android-разработке…

Для тех, кто не знает что такое Docker — если совсем просто, то это инструмент создания т.н. «контейнеров» где содержится минимальное ядро ОС и необходимый набор ПО, которые мы можем разворачивать где захотим, сохраняя при этом окружение. Что именно будет в нашем контейнере определяется в Dockerfile, который потом собирается в образ запускаемый где угодно и обладающий свойства идемпотентности.

Процесс установки и основы Docker прекрасно описаны на его официальном сайте. Поэтому, забегая немного вперед, вот такой Dockerfile у нас получился

# Т.к. основным инструментом для сборки Android-проектов является Gradle, 
# и по счастливому стечению обстоятельств есть официальный Docker-образ 
# мы решили за основу взять именно его с нужной нам версией Gradle
FROM gradle:5.4.1-jdk8

# Задаем переменные с локальной папкой для Android SDK и 
# версиями платформы и инструментария
ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip" \
    ANDROID_HOME="/usr/local/android-sdk" \
    ANDROID_VERSION=28 \
    ANDROID_BUILD_TOOLS_VERSION=28.0.3

# Создаем папку, скачиваем туда SDK и распаковываем архив,
# который после сборки удаляем
RUN mkdir "$ANDROID_HOME" .android \
    && cd "$ANDROID_HOME" \
    && curl -o sdk.zip $SDK_URL \
    && unzip sdk.zip \
    && rm sdk.zip \
# В следующих строчках мы создаем папку и текстовые файлы 
# с лицензиями. На оф. сайте Android написано что мы 
# можем копировать эти файлы с машин где вручную эти 
# лицензии подтвердили и что автоматически 
# их сгенерировать нельзя
    && mkdir "$ANDROID_HOME/licenses" || true \
    && echo "24333f8a63b6825ea9c5514f83c2829b004d1" > "$ANDROID_HOME/licenses/android-sdk-license" \
    && echo "84831b9409646a918e30573bab4c9c91346d8" > "$ANDROID_HOME/licenses/android-sdk-preview-license"    

# Запускаем обновление SDK и установку build-tools, platform-tools
RUN $ANDROID_HOME/tools/bin/sdkmanager --update
RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
    "platforms;android-${ANDROID_VERSION}" \
    "platform-tools"

Сохраняем его в папку с нашим Android-проектом и запускаем сборку контейнера командой

docker build -t android-build:5.4-28-27 .

Параметр -t задает tag или имя нашего контейнера, которое обычно состоит из его название и версии. В нашем случае мы назвали его android-build а в версии указали совокупность версий gradle, android-sdk и platform-tools. В дальнейшем нам проще будет искать нужный нам образ по имени используя такую «версию».

После того как сборка прошла мы можем использовать наш образ локально, можем загрузить его командой docker push в публичный или приватный репозиторий образов чтобы скачивать его на другие машины.

В качестве примера соберем локально проект. Для этого в папке с проектом выполним команду

docker run --rm -v "$PWD":/home/gradle/ -w /home/gradle android-build:5.4.1-28-27 gradle assembleDebug

Разберем что она означает:

docker run — сама команда запуска образа
-rm — означает что после остановки контейнера он удаляет за собой все что создавалось в процессе его жизни
-v "$PWD":/home/gradle/ — монтирует текущую папку с нашим Android-проектом во внутреннюю папку контейнера /home/gradle/
-w /home/gradle — задает рабочую директорию контейнера
android-build:5.4.1-28-27 — имя нашего контейнера, который мы собрали
gradle assembleDebug — собственно команда сборки, которая собирает наш проект

Если все сложиться удачно, то через пару секунд/минут вы увидите у себя на экране что-то вроде BUILD SUCCESSFUL in 8m 3s! А в папке app/build/output/apk будет лежать собранное приложение.

Аналогичным образом можно выполнять другие задачи gradle — проверять проект, запускать тесты и т.д. Основное преимущество — при необходимости сборки проекта на любой другой машине, нам не нужно беспокоиться об установке всего окружения и достаточно будет скачать необходимый образ и запустить в нем сборку.

Контейнер не хранит никаких изменений, и каждая сборка запускается с нуля, что с одной стороны гарантирует идентичность сборки независимо от места ее запуска, с другой стороны каждый раз приходиться скачивать все зависимости и компилировать весь код заново, а это иногда может занимать существенное время. Поэтому кроме обычного «холодного» запуска у нас есть вариант запуска сборки с сохранением т.н. «кэша», где мы сохраняем папку ~/.gradle просто копируя ее в рабочую папку проекта, а в начале следующей сборки возвращаем ее обратно. Все процедуры копирования мы вынесли в отдельные скрипты и сама команда запуска у нас стала выглядеть так

docker run --rm -v "$PWD":/home/gradle/ -w /home/gradle android-build:5.4.1-28-27 /bin/bash -c "./pre.sh; gradle assembleDebug; ./post.sh"

В итоге, среднее время сборки проекта у нас сократилось в несколько раз (в зависимости от числа зависимостей на проекте, но средний проект таким образом стал собираться за 1 минуту вместо 5 минут).

Все это само собой имеет смысл только если у вас есть собственный внутренний CI/CD сервер, поддержкой которого вы сами и занимаетесь. Но сейчас есть много облачных сервисов в которых все эти проблемы решены и вам не надо об этом переживать и нужные свойства сборки можно так же указать в настройках проекта.

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

Держите ли вы систему CI/CD внутри или пользуетесь сторонним сервисом

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 24

    0
    Такое бы, да для сборки яблочных приложений. Но ничего кроме специально подготовленных виртуалок ещё не видел. Может кто сталкивался?
      0
      `gradle` можно было поставить в `ENTRYPOINT`, например

      А самую мякотку-то?.. Ну, собрали, на выходе файл, владелец у него, в большинстве случаев, root, ну, или тот, кто в контейнере был, но уж точно не uid запустившего это дело на хосте. Не удобно.

      Скажете «это тривиально, посмотри в интернетах»? Так тут вся статья тривиальная, если уж на то пошло
        0

        Согласен — проблема с правами пользователя имеет место быть.
        Я знаю как минимум три или четыре решения, но нет ни одного прям красивого. Могу поделиться идеями, если интересно.
        С другой стороны — в ci/cd процессе, суть не на машине разработчика, проблемы с правами скорее всего нет, тем более, если использовать dind.

          0
          > суть не на машине разработчика

          Да, в сиае много вопросов отпадает, но как же «единство сборочного окружения»? :)

          А идеями поделиться никогда не лишне! Я, например, колдую небольшой скрипт, который на старте контейнера создает пользователя с нужным uid и только после этого выполняет команду. А что делают ваши три или четыре варианта? :)
            0

            Я идей накидаю, а Вы там соберите в кучу.


            Идея 1. Наивно можно запускать контейнер с ключом -u (--uid). Тогда исполняемый файл будет запускаться под юзером с указанным UID. Прокинуть айди текущего юзера можно через $(id -u). Проблема в том, что это работает, если все каталоги внутри образа с правами 777 и программа должна быть собрана и настроена портабельно. Это далеко не всегда так


            Идея 2. Раз предыдущее не работает, то давайте мы сделаем docker-entrypoint.sh скрипт, будем в нем свитчится в нужного юзера и там же заchown'им и chmod'им все необходимые для работы каталоги. В принципе это работает, но выглядит не красиво: приходится передавать айди текущего юзера через переменную окружения (docker run ... -e UID=$(id -u)) внутрь контейнера. А потом через тот же gosu свитчиться в нужного юзера. При этом это не работает с ключом --uid из п.1, т.к. нам надо стартовать из-под рута. Дополнительно — это не решает проблемы с правами на корневой каталог, куда будут смонтированы файлы докера (он все равно будет под рутом). И в опеншифте такой контейнер не запустится.


            Идея 3. Нам вообще не нравится писать колбасу вида
            docker run --rm -v "$PWD":/home/gradle/ -w /home/gradle android-build:5.4.1-28-27 /bin/bash -c "./pre.sh; gradle assembleDebug; ./post.sh" Итого — приходим к необходимости написания какой-то внешней обвязки в виде баш скрипта, алиаса или мейкфайла. Запомним это.


            Идея 4.а Все проблемы с правами на каталог, который создает докер из-за ключа -v. Если перейти на полный синтаксис bind mount, то целевой каталог НЕ СОЗДАЕТСЯ автоматически, а это значит, что если мы создадим его в скрипте из Идея 3 руками, то с правами на каталог будет все ок, а права на файлы будут нормальные — см. Идея 1 и Идея 2


            Идея 4.b Мы можем вообще не париться с вольюмами, а тупо писать файлы в volume, а вытаскивать их из них либо через временный контейнер с нужным юзером, либо через docker cp


            Идея 4. c Ах, еще можно запустить в докер-контейнере тот же тар, а наружу прокинуть файл через пайп (что-то типа docker exec -it blablabla cat FILE | cat > FILE — тот кат, что справа уже выполняется на хосте с привилегиями текущего юзера, т.е. получается нам вообще без разницы, что внутри контейнера происходит)

              0
              Годно. Теперь, если кто-то будет использовать эту статью по назначению, обязательно наткнется на эти залежи и улучшит свой пайплайн ;)

              Сам я же уже давольно давно использую «идею 2» с некоторыми вариациями — всё хорошо. (что до опеншифта, он мне не надыть, так что, проблем не возникает)
                0

                antaresm по поводу прав на каталоги и файлы артефактов есть что добавить?

                  0
                  Я просто на старте контейнера в нем создаю пользователя с uid равным идентификатору с хоста, и дальше выполняю под ним. Естественно, проверяю, запускается ли контейнер в ci окружении или у разработчика на хосте.

                  YMMV ;)
                    0

                    Т.е. если на хосте два юзера с ID 1000 и 1001, то один из них будет в обломе )
                    Хотя для линукс машин, в общем-то, это не очень типичный сценарий, а на маке с докером обычно нет проблем с правами на файлы )))))

                      0
                      > если на хосте два юзера с ID 1000 и 1001, то один из них будет в обломе

                      почему же?! это всё происходит динамически, во время старта контейнера. у каждого пользователя будет контейнер с его uid. мы же про сборочные контейнеры говорим, а не про сферические в вакууме.
                        0

                        Сорри, я не Вам писал, Вы ответили, а я невпопад повторно ответил )
                        Да, Вы правы — если пробрасывать uid в контейнер (мы рассмотрели способы как это сделать), но в статье автора — id user'а в контейнере зафиксирован. Соответственно, как мы выше определили — это норм для ci/cd, но не очень круто, когда разраб хочет "сборочный" контейнер запустить на своем ПК

                    0
                    Если нам нужны непосредственно артефакты как файлы, то у нас сам контейнер запускается под root, поэтому после сборки проблем достать артефакты не возникает;
                    В большинстве случаев же, gradle заливает полученный apk в HockeyApp или GooglePlay.
                      0
                      то у нас сам контейнер запускается под root

                      Ну, Вы имеете в виду, что docker run выполняется от root'а? Тогда да, проблем нет.
                      Как насчет работы на машине разработчика? Или в опеншифте (где рут внутри контейнера запрещен по вполне понятным причинам)

                        0
                        Да, docker run выполняется root'ом
                        Для разработчика локально запустить root тоже не проблема.
                        А для опеншифт как я уже писал мы не храним артефакт, а заливаем его на внешний ресурс
                          0
                          Для разработчика локально запустить root тоже не проблема.

                          Как минимум — лишнее действие. Как максимум — в компании может быть политика ИБ (хотя, конечно, может выглядеть глупо — рута не даем, а докер даем)))


                          А для опеншифт как я уже писал мы не храним артефакт, а заливаем его на внешний ресурс

                          Вы им вообще пользовались, извините? Речь про запуск сборщика в кластере.

                            0
                            не пользовались. поэтому я просто вам описал решение которое у нас на сервере CI используется. основная то проблема в правах, как я понял; Или, извините, я не правильно понял проблему
                              0

                              Да, основная проблема в правах.
                              Но как я выше подчеркнул — ее можно игнорировать, в случае, если вся история происходит в CI/CD конвейере на выделенном инстансе. Но ведь хочется больше!!! Масштабировать процесс на все ) и на машины разраба, и в кластере собирать (чтобы не платить за выделенную машину, а платить за фактические потребленное время сборок) и т.п.

          0

          Хотел добавить, что курлить артефакт из публичного репо без проверки sha — так себе идея.
          Пример — собираете Вы такой свой образ, сидите в Корп сети, а там Корп прокси. Помимо того, что он курочит хттпс, он легко может вместо артефакта zip выдать страницу с ошибкой. Но курл ее честно скачает и положит во временный образ.
          Другой вопрос, что процесс сборки скорее всего свалится на распаковке, но вот такой вектор атаки все равно остаётся...


          Ну, и браво — Вы переизобрели концепцию временных "сборочных" контейнеров. Очень удобное продакшен решение, на самом деле. Учитывая, что его ещё можно отмасштабировать на машину разраба, если он не хочет захламлять основную систему дев-тулзами

            0

            В первом случае: у нас стоит внутренний репозиторий образов. Поэтому в нашем случае проверка излишняя.
            На тему переизобретения: даже не пытаюсь присвоить это себе и в тексте про это ни слова. Просто, как показал опыт — мобильные разработчики обычно далеко от темы контейнеров и Docker и наверняка этот туториал будет много кому как минимум интересен.

              0

              Ссылка так-то на оф.сайт — https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip


              На тему переизобретения: даже не пытаюсь присвоить это себе и в тексте про это ни слова. Просто, как показал опыт — мобильные разработчики обычно далеко от темы контейнеров и Docker и наверняка этот туториал будет много кому как минимум интересен.

              Интересен, интересен. Я просто подчеркиваю, что описана в общем-то действительно неплохая практика.

            0
            yes | /path_to_sdk/sdk/tools/bin/sdkmanager«build-tools;29.0.0» — принимаем лицензию для build-tools
            yes | /path_to_sdk/sdk/tools/bin/sdkmanager «platforms;android-28» — принимаем лицензию для платформы.
            таким образом, можно принимать лицензию автоматически для всех компонентов.
              0
              Или можно все сразу
              RUN yes | sdkmanager --licenses
              0
              Есть ли у вас опыт запуска espresso тестов в docker контейнере (т.е. запуск эмулятора)?
                0
                Нет. Эмуляторы с тестами мы запускаем как обычно на отдельной машине

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

              Самое читаемое