Главная » Хабрахабр » [Из песочницы] Автоматическая сборка Unity-проектов для Android и iOS с помощью Gitlab CI

[Из песочницы] Автоматическая сборка Unity-проектов для Android и iOS с помощью Gitlab CI

В этой статье хочу рассказать о подходе к сборке Unity-проектов на android и ios через Gitlab на собственных сборщиках с macOS.

Я работаю в небольшой gamedev компании, и задача автоматизации сборки появилась из-за следующих проблем:

  • 5 распределенных команд должны собирать проекты из любой точки мира
  • должны поддерживаться разные версии юнити
  • сборщик должен обеспечивать как минимум 5 сборок в неделю от каждой команды
  • сертификаты должны храниться централизованно, а не у разработчиков
  • собранные билды должны быть доступны по ссылке в любой точке мира
  • проекты должны проверяться на наличие обязательных библиотек (рекламные sdk и коды, локализация, сохранения)
  • конфигурирование сборки для команд должно производиться в одном месте

Для решения этих проблем уже созданы готовые решения: Unity Cloud Build, TeamCity, Jenkins, Gitlab CI, Bitbucket Pipelines.

TeamCity и Jenkins требуют настройки проекта в админках (это немного усложняет конфигурирование для разработчиков), установку дополнительного программного обеспечения на отдельный сервер и его поддержку. Первый из них, хоть и подготовлен для сборки Unity-проектов, но не позволяет автоматизировать работу с сертификатами, и для каждого проекта их приходится заводить вручную. В итоге, самыми простыми и быстрыми в реализации остались два варианта — Gitlab и Bitbucket.
На момент решения проблемы Bitbucket Pipelines еще не анонсировали, поэтому было принято решение использовать Gitlab.

Для реализации подхода выполнены следующие шаги:

  • Настройка проекта
  • Настройка раннера
  • Создание скриптов сборки

1. Настройка проекта

Бесплатная версия сервиса никак не ограничивает сами репозитории и их количество.
Для каждого проекта включается раннер (сервис, выполняющий команды от gitlab-сервера), работающий на маке.
Конфигурация для сборщика лежит в корне проекта в виде .gitlab-ci.yml файла. Проекты, которые собираются на сборщике мы храним на Gitlab. В нем описывается id приложения, требуемый signing identity (keystore для android и имя аккаунта для ios), требуемая версия Unity, ветка, режим запуска: ручной или автоматический и команда, которая запускает сборку (при необходимости, gitlab поддерживает гораздо больше параметров, документация).

Пример файла конфигурации .gitlab-ci.yml

variables: BUNDLE: com.banana4apps.evolution SIGNING: banana4apps UNITY_VERSION: 2017.1 build:android: script: - buildAndroid.sh $BUNDLE $SIGNING $UNITY_VERSION only: - releaseAndroid when: manual build:ios: script: - buildIOS.sh $BUNDLE $SIGNING $UNITY_VERSION only: - releaseIOS when: manual

2. Настройка раннера

Бесплатная версия ограничивает число часов использования shared раннеров, но позволяет безлимитно использовать собственные раннеры. Gitlab CI работает с общими (shared) и собственными раннерами (документация). Из-за этого пришлось поднимать раннеры на собственных маках. Shared раннеры запускаются на linux, поэтому на них iOS приложения собирать не получится (но Unity запустить получится, на хабре была статья об этом). В приведенном выше примере раннер запускает скрипт buildAndroid.sh или buildIOS.sh (в зависимости от ветки), в котором описаны подготовительные шаги, запуск Unity и уведомление о результате сборки.
Процесс настройки раннера хорошо описан в документации и сводится к запуску gitlab-runner install и gitlab-runner start.
После этого на мак устанавливаются необходимые версии Unity.

3. Создание скриптов сборки

Но алгоритм одинаковый: Для каждой из платформ, ввиду различий процесса сборки, пришлось написать собственный скрипт.

  • проверяем корректность id проекта, наличие сертификатов для нужного signing identity
  • определяем пути до SDK, Java
  • создаем в проекте C# класс с методом для запуска сборки
  • проверяем наличие необходимой версии Unity и запускаем сборку. Если нет, то пытаемся собрать на версии по умолчанию
  • Проверяем наличие apk или Xcode проекта, и если их нет — сигнализируем об ошибке в Slack
  • для iOS: собираем Xcode проект
  • загружаем артефакты (apk, ipa) на сервер (например, Amazon S3)
  • сигнализируем об успешной сборке в Slack и отправляем ссылку на скачивание артефактов

Поэтому скрипт сборки “подкидывает” в проект класс с методами для запуска сборки: Особенность сборки Unity проекта в том, что Unity в batch режиме позволяет выполнить только статический метод класса, имеющегося в проекте.

CustomBuild.cs

public class CustomBuild
/{1}.apk" , outputProjectsFolder, PlayerSettings.applicationIdentifier), target, options); } static void BuildIOS() { BuildTarget target = BuildTarget.iOS; EditorUserBuildSettings.SwitchActiveBuildTarget(target); PlayerSettings.applicationIdentifier = Environment.GetEnvironmentVariable("AppBundle"); PlayerSettings.iOS.appleDeveloperTeamID = Environment.GetEnvironmentVariable("GymTeamId"); BuildPipeline.BuildPlayer(GetScenes(), xcodeProjectsFolder, target, options); } // Добавляем выбранные в настройках сцены в билд static string[] GetScenes() { var projectScenes = EditorBuildSettings.scenes; List<string> scenesToBuild = new List<string>(); for (int i = 0; i < projectScenes.Length; i++) { if (projectScenes[i].enabled) { scenesToBuild.Add(projectScenes[i].path); } } return scenesToBuild.ToArray(); }
}

GetEnvironmentVariable получает значение environment переменных, которые предварительно были указаны в bash-скриптах. Метод Environment.

Пример скрипта сборки для Android

buildAndroid.sh

GREEN='\033[0;32m'
RED='\033[0;33m'
NC='\033[0m' # No Color export COMMIT=$(git log -1 --oneline —no-merges) if [ "$1" = "" ]; then echo -e "${RED}You must provide application Id${NC}" exit 1
fi export ANDROID_HOME=/Library/Android
export OutputDirectory=./
export AppBundle=$1 if [ "$2" = "account1" ]; then export KeystoreName="$CI_DATA_PATH/keystores/account1.keystore" export KeystorePassword="..." export KeyAlias="..." export KeyPassword="..." elif [ "$2" = "account2" ]; then export KeystoreName="$CI_DATA_PATH/keystores/account2.keystore" export KeystorePassword="..." export KeyAlias="..." export KeyPassword="..." else echo "${RED}No keystore config found for $2${NC}" exit 1
fi echo -e "${GREEN}BundleId: ${AppBundle}${NC}"
echo -e "${GREEN}Signing: ${KeyAlias}${NC}" # Копируем файл для запуска сборки
mkdir -p $CI_PROJECT_DIR/Assets/Editor && cp $CI_DATA_PATH/CustomBuild.cs "$_" # Запускаем сборку Unity
if [ "$3" = "5.5" ]; then /Applications/Unity5.5/Unity.app/Contents/MacOS/Unity -buildTarget android -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.Android -quit -logFile /dev/stdout -username "..." -password "..." elif [ "$3" = "2017.1" ]; then /Applications/Unity2017.1/Unity.app/Contents/MacOS/Unity -buildTarget android -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.Android -quit -logFile /dev/stdout -username "..." -password "..." else /Applications/Unity5.6.4/Unity.app/Contents/MacOS/Unity -buildTarget android -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.Android -quit -logFile /dev/stdout -username "..." -password "..."
fi # Сборка успешна, если имеем apk
export APK="${CI_PROJECT_DIR}/${OutputDirectory}/${AppBundle}.${CI_BUILD_ID}.apk"
echo "Testing apk exists: ${APK}..."
if [ -f ${APK} ]; then echo -e "${GREEN}BUILD FOR ANDROID SUCCESS${NC}" # Загрузить apk и дать разрешение на чтение aws s3 cp ${APK} s3://ci-data/android/${AppBundle}.${CI_BUILD_ID}.apk --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers echo "<html><title>Download apk: ${AppBundle}</title><body><a href=\"https://ci-data.s3.amazonaws.com/android/${AppBundle}.${CI_BUILD_ID}.apk\">Install<br><br><strong>${AppBundle}</strong><br><small>${COMMIT}<br>(build ${CI_BUILD_ID} - android)</small></a></body></html>" >> ${CI_PROJECT_DIR}/download.html # Загрузить html и дать разрешение на чтение aws s3 cp ${CI_PROJECT_DIR}/download.html s3://ci-data/android/${AppBundle}.${CI_BUILD_ID}.html --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers # Отправить ссылку в Slack ${CI_DATA_PATH}/notifySlack.sh android success "https://ci-data.s3.amazonaws.com/android/${AppBundle}.${CI_BUILD_ID}.html" exit 0 else echo -e "${RED}BUILD FOR ANDROID FAILED${NC}" ${CI_DATA_PATH}/notifySlack.sh android failure exit 1
fi

Разработчики не могут напрямую влиять на Xcode проект, что вносит ограничения: нельзя напрямую изменять настройки проекта, информацию о сборке.
Также, особенность сборки на iOS в том, что тестовые устройства должны быть зарегистрированы в provisioning профиле приложения. Пример скрипта сборки для iOS
Сборка проектов осуществляется в два шага: формирование Xcode проекта из Unity и сборка Xcode проекта. Этот инструмент создает и синхронизирует сертификаты, профили и позволяет загружать билды и мета-информацию в itunes connect. А чтобы собрать Xcode проект, нужно до сборки создать сертификат, provisioning профиль и id приложения в developer консоли Apple.
Для автоматизации этого процесса используется fastlane.

При сборке Unity проектов без доступа к Xcode есть нюансы:

  • в Unity перед сборкой проекта нужно указать TeamId, который есть в консоли разработчика — это делается через PlayerSettings.iOS.appleDeveloperTeamID
  • в postprocess скрипте проекта необходимо выполнить предварительную обработку Xcode проекта: настроить info.plist, build settings
    Релизная и Ad-Hoc сборка также имеют разные скрипты, отличающиеся формированием результата: релизная грузит архив в itunes connect, а ad-hoc грузит ipa, создает манифест и страницу для скачивания over the air, ссылка на которую рассылается всем заинтересованным лицам.

Сборка Ad-hoc: buildAdhocIOS.sh

GREEN='\033[0;32m'
RED='\033[0;33m'
NC='\033[0m' # No Color export COMMIT=$(git log -1 --oneline --no-merges) if [ "$1" = "" ]; then echo -e "${RED}You must provide application Id${NC}" exit 1
fi if [ "$2" = "account1" ]; then # Описываем аккаунт для fastlane утилит export AccountName="account email" export AccountDesc="account description" export FastlanePassword="..." export GymExportTeamId="..." export FastlaneRepository="fastlane-keys.git" export ProduceTeamName="team name" else echo "${RED}No keystore config found for $2${NC}" exit 1
fi echo -e "${GREEN}BundleId: ${AppBundle}${NC}"
echo -e "${GREEN}Account: ${AccountDesc} (${2})${NC}" # Копируем файл для запуска сборки
mkdir -p $CI_PROJECT_DIR/Assets/Editor && cp $CI_DATA_PATH/CustomBuild.cs "$_" # Запускаем сборку Unity
if [ "$3" = "5.5" ]; then /Applications/Unity5.5/Unity.app/Contents/MacOS/Unity -buildTarget ios -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.IOS -quit -logFile /dev/stdout -username "..." -password "..." elif [ "$3" = "2017.1" ]; then /Applications/Unity2017.1/Unity.app/Contents/MacOS/Unity -buildTarget ios -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.IOS -quit -logFile /dev/stdout -username "..." -password "..." else /Applications/Unity5.6.4/Unity.app/Contents/MacOS/Unity -buildTarget ios -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.IOS -quit -logFile /dev/stdout -username "..." -password "..."
fi # Проверяем, что Unity создал XCode проект
XCODE_FILES="${CI_PROJECT_DIR}/${XcodeDirectory}"
if [ -d ${XCODE_FILES} ]; then # Создаем приложение в Apple Developer Console export PRODUCE_APP_IDENTIFIER=${AppBundle} export PRODUCE_APP_NAME=${AppBundle} export PRODUCE_USERNAME=${AccountName} export PRODUCE_SKU=${AppBundle} # skip_itc не создает приложение в itunes connect - для adhoc это необязательно fastlane produce --app_version "1.0" --language "English" --skip_itc # Скачиваем или создаем code signing keys and profiles cd "${CI_PROJECT_DIR}/${XcodeDirectory}" rm -f Matchfile echo "git_url \"${FastlaneRepository}\"" >> Matchfile echo "app_identifier [\"${AppBundle}\"]" >> Matchfile echo "username \"${AccountName}\"" >> Matchfile # Пароль, которым зашифрован репозиторий с ключами export MATCH_PASSWORD='...' # В зависимости от вида сборки, запрашиваем нужные сертификаты # force_for_new_devices true добавляет все новые тестовые устройства, которые указаны в developer console fastlane match adhoc --force_for_new_devices true # Создаем Gymfile и собираем XCode project и подписываем Ad-Hoc сертификатом rm -f Gymfile echo "export_options(" >> Gymfile echo " manifest: {" >> Gymfile echo " appURL: \"https://ci-data.s3.amazonaws.com/ios/${AppBundle}.${CI_BUILD_ID}.ipa\"," >> Gymfile echo " displayImageURL: \"https://ci-data.s3.amazonaws.com/ios-icon.png\"," >> Gymfile echo " fullSizeImageURL: \"https://ci-data.s3.amazonaws.com/ios-icon-big.png\"" >> Gymfile echo " }," >> Gymfile echo ")" >> Gymfile fastlane gym --scheme "Unity-iPhone" --export_method ${GYM_EXPORT_METHOD} --xcargs "DEVELOPMENT_TEAM=\"${GYM_EXPORT_TEAM_ID}\" PROVISIONING_PROFILE_SPECIFIER=\"match AdHoc ${AppBundle}\" CODE_SIGN_IDENTITY=\"iPhone Distribution: ${AccountDesc}\"" -o "${CI_PROJECT_DIR}/" -n "${AppBundle}.${CI_BUILD_ID}.ipa" # Создаем страницу для скачивания на S3 export IPA="${CI_PROJECT_DIR}/${AppBundle}.${CI_BUILD_ID}.ipa" ls -l "${CI_PROJECT_DIR}/${XcodeDirectory}/*.ipa" echo "Testing ipa exists: ${IPA}..." if [ -f ${IPA} ]; then echo -e "Begin uploading to S3..." aws s3 cp ${CI_PROJECT_DIR}/${AppBundle}.${CI_BUILD_ID}.ipa s3://ci-data/ios/${AppBundle}.${CI_BUILD_ID}.ipa --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers aws s3 cp ${CI_PROJECT_DIR}/manifest.plist s3://ci-data/ios/${AppBundle}.${CI_BUILD_ID}.plist --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers echo "<html><title>Download ipa: ${AppBundle}</title>" >> ${CI_PROJECT_DIR}/download.html echo "<body><a href=\"itms-services://?action=download-manifest&url=https://ci-data.s3.amazonaws.com/ios/${AppBundle}.${CI_BUILD_ID}.plist\">Install<br><br><strong>${AppBundle}</strong><br><small>${COMMIT}<br>(build ${CI_BUILD_ID} - iOS)</small></a></body></html>" >> ${CI_PROJECT_DIR}/download.html aws s3 cp ${CI_PROJECT_DIR}/download.html s3://ci-data/ios/${AppBundle}.${CI_BUILD_ID}.html --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers ${CI_DATA_PATH}/notifySlack.sh ios ad-hoc "https://ci-data.s3.amazonaws.com/ios/${AppBundle}.${CI_BUILD_ID}.html" echo -e "${GREEN}BUILD AD-HOC FOR IOS SUCCESS${NC}" exit 0 else echo -e "${RED}BUILD AD-HOC FOR IOS FAILED${NC}" ${CI_DATA_PATH}/notifySlack.sh ios failure exit 1 fi else echo -e "${RED}BUILD FOR IOS FAILED${NC}" ${CI_DATA_PATH}/notifySlack.sh ios failure
exit 1
fi

Сборка релиза: buildIOS.sh

GREEN='\033[0;32m'
RED='\033[0;33m'
NC='\033[0m' # No Color export COMMIT=$(git log -1 --oneline --no-merges) if [ "$1" = "" ]; then echo -e "${RED}You must provide application Id${NC}" exit 1
fi if [ "$2" = "account1" ]; then # Описываем аккаунт для fastlane утилит export AccountName="account email" export AccountDesc="account description" export FastlanePassword="..." export GymExportTeamId="..." export FastlaneRepository="fastlane-keys.git" export ProduceTeamName="team name" else echo "${RED}No keystore config found for $2${NC}" exit 1
fi echo -e "${GREEN}BundleId: ${AppBundle}${NC}"
echo -e "${GREEN}Account: ${AccountDesc} (${2})${NC}" # Копируем файл для запуска сборки
mkdir -p $CI_PROJECT_DIR/Assets/Editor && cp $CI_DATA_PATH/CustomBuild.cs "$_" # Запускаем сборку Unity
if [ "$3" = "5.5" ]; then /Applications/Unity5.5/Unity.app/Contents/MacOS/Unity -buildTarget ios -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.IOS -quit -logFile /dev/stdout -username "..." -password "..." elif [ "$3" = "2017.1" ]; then /Applications/Unity2017.1/Unity.app/Contents/MacOS/Unity -buildTarget ios -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.IOS -quit -logFile /dev/stdout -username "..." -password "..." else /Applications/Unity5.6.4/Unity.app/Contents/MacOS/Unity -buildTarget ios -projectPath $CI_PROJECT_DIR -batchmode -executeMethod CustomBuild.IOS -quit -logFile /dev/stdout -username "..." -password "..."
fi # Проверяем, что Unity создал XCode проект
XCODE_FILES="${CI_PROJECT_DIR}/${XcodeDirectory}"
if [ -d ${XCODE_FILES} ]; then # Создаем приложение в Apple Developer Console and Itunes Connect export PRODUCE_APP_IDENTIFIER=${AppBundle} export PRODUCE_APP_NAME=${AppBundle} export PRODUCE_USERNAME=${AccountName} export PRODUCE_SKU=${AppBundle} fastlane produce --app_version "1.0" --language "English" # Скачиваем или создаем code signing keys and profiles cd "${CI_PROJECT_DIR}/${XcodeDirectory}" rm -f Matchfile echo "git_url \"${FastlaneRepository}\"" >> Matchfile echo "app_identifier [\"${AppBundle}\"]" >> Matchfile echo "username \"${AccountName}\"" >> Matchfile # Пароль, которым зашифрован репозиторий с ключами export MATCH_PASSWORD='...' # Запрашиваем нужные сертификаты fastlane match appstore # Собираем в XCode fastlane gym --scheme "Unity-iPhone" --xcargs "DEVELOPMENT_TEAM=\"${GymExportTeamId}\" PROVISIONING_PROFILE_SPECIFIER=\"match AppStore ${AppBundle}\" CODE_SIGN_IDENTITY=\"iPhone Distribution: ${AccountDesc}\"" -o "${CI_PROJECT_DIR}/" -n "${AppBundle}.${CI_BUILD_ID}.ipa" # Загружаем в itunes connect export IPA="${CI_PROJECT_DIR}/${AppBundle}.${CI_BUILD_ID}.ipa" ls -l "${CI_PROJECT_DIR}/${XcodeDirectory}/*.ipa" echo "Testing ipa exists: ${IPA}..." if [ -f ${IPA} ]; then rm -f Deliverfile echo "app_identifier \"${AppBundle}\"" >> Deliverfile echo "username \"${AccountName}\"" >> Deliverfile echo "ipa \"${IPA}\"" >> Deliverfile echo "submit_for_review false" >> Deliverfile echo "force true" >> Deliverfile fastlane deliver echo -e "${GREEN}BUILD FOR IOS SUCCESS${NC}" exit 0 else echo -e "${RED}BUILD FOR IOS FAILED${NC}" ${CI_DATA_PATH}/notifySlack.sh ios failure exit 1 fi else echo -e "${RED}BUILD FOR IOS FAILED${NC}" ${CI_DATA_PATH}/notifySlack.sh ios failure exit 1
fi

Как с этой системой работают другие

  • Разработчик добавляет в корень проекта шаблонный файл .gitlab-ci.yml, включает раннер в настройках проекта на gitlab.com, пушит код в нужную ветку.
  • Геймдизайнер и тестировщик получают в Slack уведомление об успешной сборке и ссылку на скачивание apk и ipa архивов.
  • Я слежу за сборками, вижу логи и могу помогать разработчикам разобраться с ошибками. Логи и запущенные сборки можно увидеть прямо на gitlab. Из минусов — сейчас в интерфейсе нельзя увидеть очередь сборки для определенного раннера.

Интерфейс просмотра логов сборки:

image

Результаты

Таким образом, получившаяся система является простой в использовании, позволяет добавлять проверки и валидации со стороны сервера (code style, тесты), при этом менеджеры видят ссылки на сборки в Slack и нет проблем со сборкой на iOS.

Из минусов — необходима ее поддержка для добавления новых версий Unity, signing identity и обеспечения работоспособности маков.

Скорость сборки зависит от характеристик раннера и количества ассетов в проекте, ведь они импортируются каждый раз заново и она варьируется в пределах 3 — 30 минут для Android и 10 — 60 для iOS. На текущий момент у нас работают два раннера (около двух лет), через систему прошло более 4000 сборок.


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

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] Как мы создали технологичный продукт и провалились на дно

Который не был готов меняться и делать резкие движения для того, чтобы подстроится под меняющийся рынок. Хочу поделиться с Вами историей хорошей жизни и долгой, медленной и мучительной смерти одного, некогда крупного, портала недвижимости. Это история о важном значении той ...

Человек-функция или перестаньте нанимать технологии

Не думал что соберусь писать об этом статью и тем более на Хабр, но, как говорится, «с этим надо что-то делать». Наболело. За 10 лет своей карьеры сначала Системным Администратором, потом Системным Инженером и DevOps-ом, успев побыть простым исполнителем, тех- ...