- PVSM.RU - https://www.pvsm.ru -
В этой статье хочу рассказать о подходе к сборке Unity-проектов на android и ios через Gitlab на собственных сборщиках с macOS.
Я работаю в небольшой gamedev компании, и задача автоматизации сборки появилась из-за следующих проблем:
Для решения этих проблем уже созданы готовые решения: Unity Cloud Build, TeamCity, Jenkins, Gitlab CI, Bitbucket Pipelines.
Первый из них, хоть и подготовлен для сборки Unity-проектов, но не позволяет автоматизировать работу с сертификатами, и для каждого проекта их приходится заводить вручную. TeamCity и Jenkins требуют настройки проекта в админках (это немного усложняет конфигурирование для разработчиков), установку дополнительного программного обеспечения на отдельный сервер и его поддержку. В итоге, самыми простыми и быстрыми в реализации остались два варианта — Gitlab и Bitbucket.
На момент решения проблемы Bitbucket Pipelines еще не анонсировали, поэтому было принято решение использовать Gitlab.
Для реализации подхода выполнены следующие шаги:
Проекты, которые собираются на сборщике мы храним на Gitlab. Бесплатная версия сервиса никак не ограничивает сами репозитории и их количество.
Для каждого проекта включается раннер (сервис, выполняющий команды от gitlab-сервера), работающий на маке.
Конфигурация для сборщика лежит в корне проекта в виде .gitlab-ci.yml файла. В нем описывается id приложения, требуемый signing identity (keystore для android и имя аккаунта для ios), требуемая версия Unity, ветка, режим запуска: ручной или автоматический и команда, которая запускает сборку (при необходимости, gitlab поддерживает гораздо больше параметров, документация [1]).
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
Gitlab CI работает с общими (shared) и собственными раннерами (документация [2]). Бесплатная версия ограничивает число часов использования shared раннеров, но позволяет безлимитно использовать собственные раннеры. Shared раннеры запускаются на linux, поэтому на них iOS приложения собирать не получится (но Unity запустить получится, на хабре была статья об этом). Из-за этого пришлось поднимать раннеры на собственных маках. В приведенном выше примере раннер запускает скрипт buildAndroid.sh или buildIOS.sh (в зависимости от ветки), в котором описаны подготовительные шаги, запуск Unity и уведомление о результате сборки.
Процесс настройки раннера хорошо описан в документации и сводится к запуску gitlab-runner install и gitlab-runner start.
После этого на мак устанавливаются необходимые версии Unity.
Для каждой из платформ, ввиду различий процесса сборки, пришлось написать собственный скрипт. Но алгоритм одинаковый:
Особенность сборки Unity проекта в том, что Unity в batch режиме позволяет выполнить только статический метод класса, имеющегося в проекте. Поэтому скрипт сборки “подкидывает” в проект класс с методами для запуска сборки:
public class CustomBuild
{
static string outputProjectsFolder = Environment.GetEnvironmentVariable("OutputDirectory");
static string xcodeProjectsFolder = Environment.GetEnvironmentVariable("XcodeDirectory");
static void BuildAndroid()
{
BuildTarget target = BuildTarget.Android;
EditorUserBuildSettings.SwitchActiveBuildTarget(target);
PlayerSettings.applicationIdentifier = Environment.GetEnvironmentVariable("AppBundle");
PlayerSettings.Android.keystoreName = Environment.GetEnvironmentVariable("KeystoreName");
PlayerSettings.Android.keystorePass = Environment.GetEnvironmentVariable("KeystorePassword");
PlayerSettings.Android.keyaliasName = Environment.GetEnvironmentVariable("KeyAlias");
PlayerSettings.Android.keyaliasPass = Environment.GetEnvironmentVariable("KeyPassword");
BuildPipeline.BuildPlayer(GetScenes(), string.Format("{0}/{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();
}
}
Метод Environment.GetEnvironmentVariable получает значение environment переменных, которые предварительно были указаны в bash-скриптах.
Пример скрипта сборки для Android
GREEN='33[0;32m'
RED='33[0;33m'
NC='33[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
Пример скрипта сборки для iOS
Сборка проектов осуществляется в два шага: формирование Xcode проекта из Unity и сборка Xcode проекта. Разработчики не могут напрямую влиять на Xcode проект, что вносит ограничения: нельзя напрямую изменять настройки проекта, информацию о сборке.
Также, особенность сборки на iOS в том, что тестовые устройства должны быть зарегистрированы в provisioning профиле приложения. А чтобы собрать Xcode проект, нужно до сборки создать сертификат, provisioning профиль и id приложения в developer консоли Apple.
Для автоматизации этого процесса используется fastlane [3]. Этот инструмент создает и синхронизирует сертификаты, профили и позволяет загружать билды и мета-информацию в itunes connect.
При сборке Unity проектов без доступа к Xcode есть нюансы:
GREEN='33[0;32m'
RED='33[0;33m'
NC='33[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
GREEN='33[0;32m'
RED='33[0;33m'
NC='33[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
Интерфейс просмотра логов сборки:

Таким образом, получившаяся система является простой в использовании, позволяет добавлять проверки и валидации со стороны сервера (code style, тесты), при этом менеджеры видят ссылки на сборки в Slack и нет проблем со сборкой на iOS.
Из минусов — необходима ее поддержка для добавления новых версий Unity, signing identity и обеспечения работоспособности маков.
На текущий момент у нас работают два раннера (около двух лет), через систему прошло более 4000 сборок. Скорость сборки зависит от характеристик раннера и количества ассетов в проекте, ведь они импортируются каждый раз заново и она варьируется в пределах 3 — 30 минут для Android и 10 — 60 для iOS.
Автор: muxapet
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/280039
Ссылки в тексте:
[1] документация: https://docs.gitlab.com/ee/ci/yaml/
[2] документация: https://docs.gitlab.com/runner/
[3] fastlane: https://fastlane.tools/
[4] Источник: https://habr.com/post/358448/?utm_source=habrahabr&utm_medium=rss&utm_campaign=358448
Нажмите здесь для печати.