Разработка Napoleon IT
584

Автоматизация iOS-сборок в конструкторе приложений, или Как перестать делать сборки 24/7 и начать разрабатывать

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

В закладки
Приложение собирается менеджером проекта, без программиста

Все iOS-разработчики знают, что сборка приложения и загрузка его в AppStore Connect — далеко не самый быстрый процесс, он запросто может съесть у вас до получаса рабочего времени. А если речь идёт о платформе, то для каждого клиента нужно собирать и загружать в Connect своё приложение, причём не просто собрать, а ещё и кучу всего: поменять иконку, название, идентификатор и т.д. Именно о таких вещах и пойдёт речь.

Общее

Для начала вкратце опишем, как сейчас у нас построен процесс сборки приложения на нашем конструкторе.

  • Менеджером в административной панели заполняется конфигурация для конкретного приложения и запускается процесс его сборки.
  • Конфигурация, заполненная менеджером загружается в формате JSON, скачиваются необходимые ресурсы, применяются в проекте и происходит сама сборка приложения. Все это происходит на облачном Mac Mini с помощью Gitlab CI.
  • Полученная сборка заливается в AppStore Connect для релиза либо отдается клиенту через сервис TestFlight.

Иконка приложения

Чтобы добавить иконку в iOS-приложение, вам необходимо иметь ее в разрешениях для всех устройств, которые поддерживает ваше приложение. в IDE Xcode это выглядит вот так (и это только для iPhone, не беря в расчет iPad, Apple Watch и т. д.):

Естественно, никто не будет загружать иконку приложения в каждом разрешении, поэтому менеджер в административной панели загружает единственную иконку размером 1024x1024 px. Далее необходимо получить из единственного файла изображения всех необходимых размеров.

Тут нам на помощь приходит файл Contents.json, который лежит в папке с иконкой. По сути экран, который мы видим в Xcode на скриншоте выше, управляется обычным JSON-файлом с такой структурой:

"images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x", "unassigned": true }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" } ] "info" : { "version" : 1, "author" : "xcode" }

Поэтому все, что нам нужно сделать для достижения нашей цели – корректно распарсить данный JSON и создать иконки нужных размеров. Сделаем это с помощью вот такого простенького скрипта:

APP_ICON_NAME="Назание иконки размером 1024х1024 px (без расширения файла)" APP_ICON_ASSETS_FOLDER="Путь до папки с иконкой проекта (обычно Assets.xcassets/Appicon.appiconset" CONFIG_FILE="Название файла конфигурации (обычно Contents.json)" # Пробегаем по каждому json-объекту в файле Contents.json echo $OLD_CONFIG_FILE_CONTENT | jq -c '.images[]' | while read config; do # Проверяем, используется ли изображение, описанное данным объектом unassigned=$(echo $config | jq '.unassigned') [ $unassigned = true ] && continue # Получаем ширину, высоту и масштаб изображения (*) widthpt=$(echo $config | jq '.size' | grep -oE '[0-9]+(\.[0-9]+)?' | head -n 1) heightpt=$(echo $config | jq '.size' | grep -oE 'x[0-9]+(\.[0-9]+)?' | head -n 1 | tail -c +2) scale=$(echo $config | jq '.scale' | grep -oE '[0-9]' | head -n 1) # Составляем название изображения name="$APP_ICON_NAME-$widthpt"x"$heightpt@$scale"x".png" # Копируем само изображение в папку cp $APP_ICON_NAME $APP_ICON_ASSETS_FOLDER/$name # Конвертируем высоту и ширину изображения из pt в px (*) # и обновляем изображение с учетом полученного разрешения widthpx=$(echo $widthpt*$scale | bc) heightpx=$(echo $heightpt*$scale | bc) sips -z $widthpx $heightpx $APP_ICON_ASSETS_FOLDER/$name > /dev/null # Добавляем файл Contents.json config=$(echo $config | jq ". + {\"filename\": \"$name\"}") config="[$config]" final=$(cat $TEMP_CONFIG_FILE | jq ".images |= . + $config") echo $final | jq '.' > $TEMP_CONFIG_FILE done # Заменяем содержание файла Contents.json нашей конфигурацией cat $TEMP_CONFIG_FILE | jq '.' > $APP_ICON_ASSETS_FOLDER/$CONFIG_FILE

(*) При разработке для iOS по-умолчанию используется размер в поинтах (pt) – специальной размерности, которая не зависит от плотности пикселей на экране. Так, для обычного дисплея (@1x) и для retina-дисплея (@2x и @3x) размер изображения будет разный в пикселях, но одинаковый в поинтах. Подробнее об этом можно почитать, например, здесь.

Code Signing и сборка приложения

В разработке под iOS есть один малоприятный аспект - каждое собираемое приложение должно быть подписано специальным сертификатом разработчика.

Если вкратце описать этот процесс, то он выглядит следующим образом:

  • Необходимо создать файл запроса на создание сертификата у себя на компьютере
  • На портале разработчика с помощью этого файла нужно сгенерировать и загрузить публичную часть сертификата (.cer). Комбинация из публичной части сертификата и приват
  • На основе этого сертификата на портале разработчика также нужно сгенерировать специальный файл – Provisioning Profile, который будет уникальный для каждого приложения вашего аккаунта.
  • С использованием комбинации сертификата и профайла приложения, можно собрать приложение и загрузить его в AppStore Connect.

К счастью, для автоматизации всех этих шагов мне на помощь пришел замечательный сервис Fastlane, одной из возможностей которого является как раз автоматическая подпись приложения.

У нас используется методы cert и sigh. С помощью этих методов автоматически генерируются нужные файлы для подписи приложения и сохраняются в соответствующих местах, чтобы они были доступны при сборке приложения. Не будем подробно описывать, как работает Fastlane под капотом, интересующиеся могут заглянуть в исходный код, они лежат на гитхабе.

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

Выглядит это тоже очень компактно:

lane :uploadapp do |options| cert sigh( app_identifier: options[:bundle] ) # Этот шаг необходим, если у вас при ручной сборке включена опция Automatic signing disable_automatic_code_signing(path: "Название .xcodeproj файла") update_project_provisioning( xcodeproj: "Название .xcodeproj файла", # Здесь необязательно указывать полное имя профайла, достаточно указать Developer или Distribution code_signing_identity: "iPhone Distribution" ) build_app( workspace: "Название .xcworkspace файла", scheme: "Scheme проекта, которую необходимо собирать (указана слева от выбора устройства, на которое собирается приложение)", # Нужно ли выполнить clean project перед сборкой проекта clean: true ) upload_to_testflight( skip_waiting_for_build_processing: true, app_identifier: options[:bundle] ) end

С помощью настройки Fastlane, приведенной выше, проверяется, присутствуют ли необходимые файлы для подписи приложения (сертификат и профайл), в случае их отсутствия, они автоматически сгенерируются и подставятся в проект. После этого проект автоматически соберется и загрузится в AppStore Connect.

Все части этого процесса подробно описаны в документации Fastlane, там же приведен список параметров, которые вы можете добавить в каждую функцию в зависимости от специфики вашего проекта.

Вызывается эта процедура следующей командой, которая просто добавляется в ваш bash-скрипт:

fastlane uploadapp

Версионирование

Еще одной важной настройкой при выкладке приложения в AppStore является его версия. Версия iOS-приложения состоит из двух частей – сама версия (строка вида 1.2.1) и билд приложения (обычно это просто целое число).

Для проставления версий мы также используем Fastlane, с помощью него можно можно получить последний загруженный в AppStore билд (для конкретной версии или для последней загруженной), но, к сожалению, нельзя получить саму версию, поэтому версионирование у нас работает следующим образом:

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

Выглядит это следующим образом:

Пусть текущая версия платформы - 1.2. При выпуске нового приложения его версия будет 1.2.1 (build 1). Если потом мы захотим обновить приложение на той же версии платформы, то версия будет 1.2.2 (build 2). Если обновим версию платформы до 1.3, а потом решим обновить это приложение на новой версии платформы, то версия будет 1.3.1 (build 1).

В итоге мы получаем следующий механизм проставления версий для приложения.

  • Получаем версию платформы из проекта и получаем номер билда последней загруженной в AppStore Connect версии. Если приложение загружается первый раз, то app_store_build_number завершится с ошибкой. Обработаем эту ошибку и присвоим билду значение по умолчани
core_version = get_version_number(xcodeproj: "Platform.xcodeproj") old_app_build_number = 0 begin old_app_build_number = app_store_build_number( live: false, app_identifier: options[:bundle] ) rescue old_app_build_number = 1 end

2) Составляем версию приложения на основе полученного номера билда и проверяем, на актуальной ли версии платформы было собрано это приложение. Проверка довольно простая – мы пытаемся получить номер билда для этой версии и увеличиваем его на 1. Если для проверяемой версии не было загружено ни одной сборки приложения, то придет 0 из-за параметра initial_build_number, который так же увеличится на 1.

client_version_with_old_build = core_version.to_s + "." + old_app_build_number.to_s new_build_number = app_store_build_number( live: false, version: client_version_with_old_build, app_identifier: options[:bundle], initial_build_number: 0 ) + 1

3) Составляем новую версию приложения и обновляем версию и билд проекта для его последующей сборки и загрузки.

new_client_version = core_version.to_s + "." + new_build_number.to_s increment_version_number( version_number: new_client_version, xcodeproj: "Platform.xcodeproj" ) increment_build_number( build_number: new_build_number )

Заключение

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

Спасибо за внимание!

ESHOP - конструктор мобильных приложений для магазинов

Материал опубликован пользователем. Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Napoleon IT", "author_type": "self", "tags": [], "comments": 3, "likes": 10, "favorites": 13, "is_advertisement": false, "subsite_label": "dev", "id": 63924, "is_wide": false, "is_ugc": true, "date": "Tue, 07 May 2019 12:35:41 +0300" }
{ "id": 63924, "author_id": 242343, "diff_limit": 1000, "urls": {"diff":"\/comments\/63924\/get","add":"\/comments\/63924\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/63924"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 235819, "last_count_and_date": null }

3 комментария 3 комм.

Популярные

По порядку

Комментарий удален

Комментарий удален

1

Fastlane работает следующим образом:
Сначала он пробегается по каждому сертификату из developer.apple и смотрит, установлен ли данный сертификат на машине, на которой осуществляется сборка
Если он не находит ни одного такого совпадения, он пытается сгенерировать новый
Если уже достигнуто максимальное количество сгенерированных сертификатов (для релизных сертификатов - 3), то он выдает ошибку, сборка приложения останавливается. Такую проблему уже приходится решать вручную

Ответить
0

Спасибо за разъяснения. Эпоха уходит, теперь разработчик уже не сможет отмазаться что "оно билдится". А сам сервис по итогу будет внутренним, скажем для менеджера, или же будет отдан самому клиенту?

Ответить
0

Пока что сервис только для внутреннего использования, но мы видим потенциал и для дальнейшего пользования клиентами продукта

Ответить

Комментарий удален

0
{ "page_type": "article" }

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "bscsh", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-223676-0", "render_to": "inpage_VI-223676-0-1104503429", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=bugf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudx", "p2": "ftjf" } } }, { "id": 16, "label": "Кнопка в шапке мобайл", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byzqf", "p2": "ftwx" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvc" } } }, { "id": 19, "label": "Тизер на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "p1": "cbltd", "p2": "gazs" } } } ]
Голосовой помощник выкупил
компанию-создателя
Подписаться на push-уведомления
{ "page_type": "default" }