Особенности сборки и доставки iOS-приложений

  • Tutorial
В этой статье Максим Шестаков, DevOps Engineer в студии Plarium Krasnodar, делится опытом сборки и доставки пользователям приложений для iOS, который накопился в процессе отладки CI/CD.



Подготовка


Каждый человек, так или иначе связанный с разработкой приложений для устройств Apple, уже успел оценить спорное удобство инфраструктуры. Сложности встречаются повсюду: начиная с меню профиля разработчика и заканчивая инструментами отладки и сборки.

Статей об «азах» предостаточно в сети, поэтому постараемся выделить главное. Вот что нужно для успешной сборки приложения:

  • аккаунт разработчика;
  • устройство на базе macOS, выступающее в роли билд-сервера;
  • сгенерированный сертификат разработчика, который будет далее использоваться для подписи приложения;
  • созданное приложение с уникальным ID (следует отметить важность Bundle Identifier, потому что применение wildcard ID делает невозможным использование многих функций приложения, например: Associated Domains, Push Notifications, Apple Sign In и прочих);
  • профиль подписи приложения.

Сертификат разработчика следует сгенерировать через Keychain на любом устройстве на базе macOS. Очень важным является тип сертификата. В зависимости от среды приложения (Dev, QA, Staging, Production) он будет различаться (Development или Distribution), так же как и тип профиля подписи приложения.

Основные типы профилей:

  • Development — предназначен для подписи приложения команды разработчиков, используется Development-сертификат (имя вида iPhone Developer: XXXXX);
  • Ad Hoc — предназначен для подписи тестового приложения и внутренней проверки QA-отделом, используется Distribution-сертификат разработчика (имя вида iPhone Distribution: XXXXX);
  • App Store — релизный билд для внешнего тестирования через TestFlight и выгрузки в App Store, используется Distribution-сертификат разработчика.

При генерации профилей Development и Ad Hoc также указывается список устройств, на которые можно установить билд, что позволяет дополнительно разграничить доступ для пользователей. В профиле App Store нет списка устройств, так как разграничением доступа при закрытом бета-тестировании занимается TestFlight, о котором будет рассказано позже.

Для наглядности можно представить профиль разработчика в виде таблички ниже. Так проще понять, какие параметры для сборки нам нужны и откуда их брать.



Сборка


Чтобы было проще разделять сборки по проекту и среде, используем имена профилей вида ${ProjectName}_${Instance}, то есть имя проекта + инстанс (зависит от среды приложения: Dev, QA, GD, Staging, Live и так далее).

При импорте на билд-сервер профиль меняет название на уникальный ID и перемещается в папку /Users/$Username/Library/MobileDevice/Provisioning Profiles (где $Username соответствует имени учетной записи пользователя билд-сервера).

Существует два способа сборки файла *.ipa — устаревший (PackageApplication) и современный (через создание XcAchive и экспорт). Первый способ считается устаревшим, так как с версии 8.3 модуль упаковки app-файла убран из дистрибутива Xcode. Для его использования надо скопировать модуль из старого Xcode (версии 8.2 и более ранних) в папку:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/

И затем выполнить команду:

chmod +x /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/*

Далее нужно собрать *.app-файл приложения:

xcodebuild \
-workspace $ProjectDir/$ProjectName.xcworkspace \
-scheme $SchemeName \
-sdk iphoneos \
build \
-configuration Release \
-derivedDataPath build \
CODE_SIGN_IDENTITY=”$DevAccName”\
PROVISIONING_PROFILE=”$ProfileId”
DEPLOYMENT_POSTPROCESSING=YES \
SKIP_INSTALL=YES \
ENABLE_BITCODE=NO

Где:

-workspace — путь к файлу проекта.

-scheme — используемая схема, указанная в проекте.

-derivedDataPath — путь выгрузки собранного приложения (*.app).

CODE_SIGN_IDENTITY — имя аккаунта разработчика, которое можно проверить в Keychain (iPhone Developer: XXXX XXXXXXX, без TeamID в скобках).



PROVISIONING_PROFILE — ID профиля для подписи приложения, который можно получить командой:

cd "/Users/$Username/Library/MobileDevice/Provisioning Profiles/" && find *.mobileprovision -type f | xargs grep -li ">${ProjectName}_${Instance}<" | sed -e 's/.mobileprovision//'

Если в приложении используется дополнительный профиль (например, для Push Notifications), то вместо PROVISIONING_PROFILE указываем:

APP_PROFILE=”$AppProfile” \
EXTENSION_PROFILE=”$ExtProfile” \

Далее полученный файл *.app следует упаковать в *.ipa. Для этого можно использовать команду вида:

/usr/bin/xcrun --sdk iphoneos PackageApplication \
-v $(find "$ProjectDir/build/Build/Products/Release-iphoneos" -name "*.app") \
-o "$ProjectDir/$ProjectName_$Instance.ipa"

Однако данный способ считается устаревшим с точки зрения Apple. Актуальным является получение *.ipa путем экспорта из архива приложения.

Для начала нужно собрать архив командой:

xcodebuild \
-workspace $ProjectDir/$ProjectName.xcworkspace \
-scheme $SchemeName \
-sdk iphoneos \
-configuration Release \
archive \
-archivePath $ProjectDir/build/$ProjectName.xcarchive \
CODE_SIGN_IDENTITY=”$DevAccName” \
PROVISIONING_PROFILE=”$ProfileId”
ENABLE_BITCODE=NO \
SYNCHRONOUS_SYMBOL_PROCESSING=FALSE

Отличия заключаются в методе сборки и опции SYNCHRONOUS_SYMBOL_PROCESSING, которая отключает выгрузку символов во время сборки.

Далее нам надо сгенерировать файл с настройками экспорта:

ExportSettings="$ProjectDir/exportOptions.plist"

cat << EOF > $ExportSettings
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<false/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<false/>
<key>method</key>
<string>$Method</string>
<key>provisioningProfiles</key>
<dict>
<key>$BundleID</key>
<string>$ProfileId</string>
</dict>
<key>signingCertificate</key>
<string>$DevAccName</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>$TeamID</string>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
EOF

Где:

$Method — метод доставки, соответствует типу профиля подписи приложения, то есть для Development значение будет development, для Ad Hoc — ad-hoc, а для App Store — app-store.

$BundleID — ID приложения, который указан в настройках приложения. Проверить можно командой:

defaults read $ProjectDir/Info CFBundleIdentifier

$DevAccName и $ProfileId — настройки имени разработчика и ID профиля подписи, которые использовались ранее и должны совпадать со значениями в настройках экспорта.

$TeamID — десятизначный ID в скобках после имени разработчика, пример: iPhone Developer: …… (XXXXXXXXXX); можно проверить в Keychain.

Далее с помощью команды экспорта получаем необходимый файл *.ipa:

xcodebuild \
-exportArchive \
-archivePath $ProjectDir/build/$ProjectName.xcarchive \
-exportPath $ProjectDir \
-exportOptionsPlist $ExportSettings

Доставка


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

Для распространения билдов Development и Ad Hoc существует множество сервисов вроде HockeyApp, AppBlade и прочих, однако в рамках данной статьи речь пойдет об автономном сервере для раздачи приложений.

Установка приложения для iOS проходит в 2 этапа:

  1. Получение манифеста установки приложения через Items Service.
  2. Установка файла *.ipa согласно информации, указанной в манифесте, через HTTPS.

Таким образом, нам для начала надо сгенерировать манифест установки (тип файла *.plist) командой:

cat << EOF > $manifest
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>$ipaUrl</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>$BundleID</string>
<key>bundle-version</key>
<string>$AppVersion</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>$ProjectName_$Instance</string>
<key>subtitle</key>
<string>$Instance</string>
</dict>
</dict>
</array>
</dict>
</plist>
EOF

Как видим, манифест содержит практически все параметры, участвующие в сборке приложения.

Версию приложения ($AppVersion) можно проверить командой:

defaults read $ProjectDir/Info CFBundleVersion

Параметр $ipaUrl содержит прямую ссылку на скачивание файла *.ipa. С седьмой версии iOS приложение должно быть установлено через HTTPS. В восьмой версии немного изменился формат манифеста: были удалены блоки с настройками иконок приложения вида

<images>
   <image>...</image>
</images>

Таким образом, для установки приложения достаточно простой html-страницы со ссылкой вида:

itms-services://?action=download-manifest&url=https://$ServerUrl/$ProjectName/$Instance/iOS/$AppVersion/manifest.plist

Для нужд отделов разработки и тестирования компания Plarium создала свое приложение установки билдов, которое дает нам:

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

Тестирование


Теперь речь пойдет о предрелизном тестировании приложения с помощью TestFlight.

Обязательными условиями для загрузки являются тип профиля подписи App Store и наличие сгенерированных API-ключей.

Есть несколько способов загрузки приложения:

  • через Xcode (Organizer),
  • через altool,
  • через Application Loader для старых версий Xcode (теперь Transporter).

Для автоматической загрузки используется altool, в котором тоже есть два способа авторизации:

  • App-Specific Password,
  • API Key.

Более предпочтительной является загрузка приложения с помощью API Key.

Для получения API Key переходим по ссылке и генерируем ключ. Кроме самого ключа в формате *.p8, нам понадобятся два параметра: IssuerID и KeyID.



Далее скачанный ключ импортируем на билд-сервер:

mkdir -p ~/.appstoreconnect/private_keys
mv ~/Downloads/AuthKey_${KeyID}.p8 ~/.appstoreconnect/private_keys/

Перед загрузкой приложения в TestFlight нужно выполнить валидацию приложения, делаем это командой:

xcrun altool \
--validate-app \
-t ios \
-f $(find "$ProjectDir" -name "*.ipa") \
--apiKey “$KeyID” \
--apiIssuer “$IssuerID” 

Где apiKey и apiIssuer имеют значения полей со страницы генерации API-ключа.

Далее при успешной валидации выполняем загрузку приложения командой --upload-app c теми же параметрами.

Приложение будет проверено Apple в течение одного-двух дней и после станет доступным внешним тестировщикам: им пришлют на почту ссылки для установки.

Другим способом загрузки приложения через altool является использование App-Specific Password.

Для получения App-Specific Password нужно перейти по ссылке и сгенерировать его в разделе Security.



Далее следует создать в Keychain запись билд-сервера с этим паролем. С 11 версии Xcode это можно сделать командой:

xcrun altool --store-password-in-keychain-item "Altool" -u "$DeveloperName" -p $AppPswd

Где:

$DeveloperName — имя аккаунта iOS-разработчика, используемое для логина в сервисы Apple.

$AppPswd — сгенерированный App-Specific Password.

Далее получаем значение параметра asc-provider и проверяем успешность импорта пароля командой:

xcrun altool --list-providers -u "$DeveloperName" -p "@keychain:Altool"

Получаем вывод:

Provider listing:
- Long Name - - Short Name -
XXXXXXX        XXXXXXXXX

Как видим, искомое значение Short Name (asc-provider) совпадает с параметром $TeamID, который мы использовали при сборке приложения.

Для валидации и загрузки приложения в TestFlight применяем команду:

xcrun altool \
--(validate|upload)-app \  
-f $(find "$ProjectDir" -name "*.ipa") \
-u "$DeveloperName" \
-p "@keychain:Altool" \

В качестве значение параметра -p можно взять значение $AppPswd в незашифрованном (явном) виде.

Однако, как уже было сказано, с точки зрения работоспособности для авторизации altool лучше выбрать API Key, так как в разных версиях Xcode встречаются те или иные проблемы («не видит» Keychain, ошибки авторизации при выгрузке и прочее).

На этом, собственно, все. Желаю всем причастным успешных сборок и беспроблемных релизов в App Store.
Plarium
Разработчик мобильных и браузерных игр

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

    0
    спасибки
      0
      Рады, если будет полезно.
      +1

      Самый главный минус всего этого — сборка только под маком. Очень много ограничений по автоматизации в CI из-за этого.

        0
        При отсутствии «железного» MacOS сервера можно использовать временное решение на базе системы виртуализации (VirtualBox). Однако быстродействие такого «сервера» будет весьма посредственным.
          0

          Это не всегда подходит из-за лицензии

            0
            Добрый день
            А есть инструкции как установить MacOS на VirtualBox?
              0
              Добрый!
              В интернете много, обычно идут в комплекте с готовым образом ОС.
          0
          Круто, спасибо за статью. Интересно послушайть, как вы это соединили вместе с Unity. Какой CI используете? что используете для написания скриптов для pipeline? Интересно сколько у вас развернуто Remote\Local агентов? И какая статистика в целом вашего BuildPipeline(кол-во билдов в день, к примеру). Очень интересно послушать, как обстоят дела у больших ребят из геймдева)
            0
            «Интересно послушайть, как вы это соединили вместе с Unity?»
            Ответ: методом допиливания сборки через -executeMethod -batchmode

            «Какой CI используете?»
            Ответ: TeamCity

            «Что используете для написания скриптов для pipeline?»
            Ответ: Notepad++

            «Интересно сколько у вас развернуто Remote\Local агентов?»
            Ответ: используются только Remote агенты

            «И какая статистика в целом вашего BuildPipeline(кол-во билдов в день, к примеру)»
            Ответ: по статистике информации не дадим, но отметим, что накануне релиза число билдов возрастает ощутимо
              +1
              Круто, спасибо за столь развернутый ответ :)
                0
                Не за что) Обращайтесь
            0
            Подскажите а чем не устраивает меню Product-Archive в xcode?
              0
              Мы не используем «меню», все сборки происходят автоматически в рамках pipeline.
                0
                Пользуетесь ли возможностями гейм центра? Есть ли у вас реал тайм матчи по сети? Если есть, как вы решаете проблему латентности пинга и обрыва соединения у одного из клиентов?
                  0
                  Гейм центр для реал тайм матчей по сети нами не используется.
              0
              Вы пробовали fastlane?
                0
                Не пробовали, так как устраивает текущий pipeline.
                  0

                  https://docs.fastlane.tools/actions/testflight/
                  Проблему закрыли бы одной строчкой. Можно заморочиться с репозиторием сертификатов конечно ещё, но все равно было бы проще)

                0
                Есть вопрос

                Вы вообще не заливаете bitcode на сервера Apple?
                altool заливает только ipa, во всех конфигах увас для биткод стоит false

                Хотел начать использовать altool, но нам нужно лить билд с биткодом, а он вроде как не делает этого (

                PS
                Статься хорошая, хорошо раскрыли тему штатных средств для CI от Apple!
                Спасибо
                  0
                  Вы вообще не заливаете bitcode на сервера Apple?

                  Нет, не заливаем.

                  во всех конфигах у вас для биткод стоит false

                  Это позволяет сократить размер билда.

                  Статья хорошая, хорошо раскрыли тему штатных средств для CI от Apple!
                  Спасибо

                  Спасибо вам за отзыв. Рады, что материал полезен.

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

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