Сегодня для управления внешними зависимостями мы используем Carthage, это один из популярных в iOS-среде инструментов. Он умеет собирать зависимости из кэша, но не управлять его организацией и хранением. Для этого нужно задействовать сторонние инструменты, и мы расскажем, как решали задачу по работе с удалёнными зависимостями. Наш опыт может быть полезен всем, кто захочет пройти этот тернистый путь интеграции remote cache через связку Rome + Carthage + S3.

С чего всë начиналось


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

Во-вторых, из-за хранения Carthage под контролем версий репозиторий разросся до огромного размера. Его приходилось клонировать при каждой сборке CI, и это могло занять до пяти минут.

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

Наконец, ещё одним недостатком было то, что нельзя было делать pull request-ы в зависимую библиотеку (которая может принадлежать любому из проектов), пересобирать основной проект, в который интегрирована зависимая библиотека, и запускать там интеграционные тесты. Вместо этого нужно было проходить полный flow со всеми проверками для получения необходимой версии проекта.

Что решили делать?


Мы не видели другого решения, кроме как вывести все зависимости из-под контроля версий и хранить их в удалённом кеше. Но прежде чем бросаться писать код мы составили список требований к этому инструменту. Ведь кроме хранения и совместного использования нужно было учесть, что у каждого проекта разработка ведётся на своей версии Xcode, которая включает в себя свой toolchain. Соответственно, для каждой версии toolchain должен быть свой кеш Carthage.

И ещё одно, главное требование к будущему решению: оно должно было не усложнить устоявшийся flow разработки, а только ускорить его. Чтобы разработчики обновляли кеш только в тех случаях, когда это действительно необходимо, и чтобы никаких дополнительных усилий для этого не требовалось, либо они должны быть минимальны.


Для хранения кеша Carthage мы решили использовать уже имеющееся корпоративное S3-хранилище. Оставалось определиться с промежуточным звеном между Carthage и S3. Можно было написать свои скрипты на Ruby, но это требовало много времени, поэтому стали искать готовое решение. Наш выбор пал на библиотеку Rome, которая уже из коробки покрывала большинство наших потребностей.

Проблемы при интеграции Rome


Мы поискали примеры интеграции Rome, но в сети есть лишь короткие простенькие руководства по установке, без разбора возможных проблем. Поэтому пришлось набираться опыта самостоятельно.

Работа с XCFramework


Первая проблема, с которой мы столкнулись, это работа с XCFramework-ами, на которые мы перевели все свои Carthage-зависимости. Оказалось, что к моменту нашего внедрения Rome не поддерживала работу с ними, хотя авторы уже готовили к релизу новую версию с поддержкой XCFramework. Сам Rome написан на Haskell и поддерживается контрибьюторами, но ничто не мешает сделать свои изменения и собрать версию до официального релиза по прилагаемой инструкции. Что мы и сделали, но позднее для порядка перешли уже на официальный релиз с поддержкой этого формата. Не нужно боятся вносить или предлагать open source-проектам свои изменения, — ведь если не мы, то кто?

Работа с S3-хранилищем


У Rome есть внутренний движок для работы с AWS, но нам он не подошёл: он использовал URL (endpoint) на основе региона AWS, без поддержки кастомизации. К тому же у нас была собственная конфигурация корпоративного S3 и хотелось хранить в проекте такую конфигурацию, чтобы разработчикам не приходилось настраивать у себя отдельно.

К счастью, Rome может использовать сторонний движок для работы с удалённым или локальным хранилищем, чем мы и решили воспользоваться. Но для этого нам нужен был инструмент, который:

  • Имел собственную конфигурацию для работы с S3, не затрагивающую стандартные .aws/*-файлы (чтобы можно было хранить отдельно и не вызывать конфликтов настроек у разработчиков).
  • Позволял скачивать и закачивать файлы по указанному пути, а также мог загружать только изменившиеся файлы (не перекачивать одинаковые).
  • Имел простые настройки.

Под эти требования подошла утилита s3cmd. Мы использовали её в нашем движке как инструмент для передачи файлов в S3-хранилище. Rome предоставлял необходимые пути для удалённых и локальных файлов, а s3cmd по ним скачивала или закачивала.

Сравнение имён в Cartfile, Rome и хранилище


Теперь нам нужно было увязать работу компонентов, а именно:

  • Получать список всех Carthage-зависимостей.
  • Определять, какие из них уже лежат локально, а какие доступны для скачивания из удалённого хранилища.
  • Докачивать недостающие зависимости или собирать их самостоятельно.

То есть нужно было организовать структуру хранения в кеше с учётом версии toolchain, и для каждой версии однозначно определять путь необходимого кеша. Rome умеет делать это автоматически, но исходя из их описания:

This is particularly useful if dependencies are not on GitHub or don't respect the «Organization/FrameworkName» convention.

«Только для библиотек со структурой Organization/Framework Name, или зависимости должны быть на GitHub». Часть наших зависимостей не подходила ни под одно условие, и нам пришлось расписывать в Romefile секцию repositoryMap. Вручную править не хотелось, поэтому мы написали генератор, который проходит по всем .version-файлам папки Carthage/Builds, создаёт структуру библиотек с их зависимостями и генерирует Romefile. Благодаря этой автоматизации мы исключили возможность что-то пропустить.

Сборка кеша под нужную версию toolchain


Мы используем определённую версию Xcode. И если кому-то из разработчиков нужно собрать приложение под другую версию, то приходится пересобрать все зависимости, на что уходит минимум час. Поэтому нам хотелось сделать так, чтобы можно было однократно пересобрать и загрузить в удалённое хранилище обновлённые библиотеки, которые разработчики смогут потом скачать. Rome позволяет делать это через кеш-префикс — root-папку, от которой будет строиться дерево всех зависимостей в кеше. И написали скрипт, генерирующий префикс:

#!/bin/sh
set -e
# get cache_prefix, примеры:
# - swiftlang-1300.0.31.4_clang-1300.0.29.6
# - swiftlang-5.6.0.323.62_clang-1316.0.20.8
version=$(swift --version 2>/dev/null | head -n 1 | sed 's/.*(\(.*\)).*/\1/' | tr ' ' '_')
echo "$version"

За основу взяли текущую версию Swift, которая покрывала нашу версию toolchain, и теперь мы используем префикс для скачивания или закачивания зависимостей в S3-хранилище. При этом мы сохраняем разные версии библиотек под разные версии Xcode.

Параллельный режим загрузки зависимостей


Нужно было добиться максимальной скорости скачивания зависимостей, чтобы разработчиков ограничивало только их интернет-соединение. Для этого мы выбрали параллельный (concurrent) режим загрузки. К сожалению, он работал нестабильно: иногда возникали конфликты файлов, скачивание прерывалось, какие-то папки не создавались и терялась часть зависимостей. Причина была в том, что какая-нибудь библиотека могла лежать в нескольких Carthage-файлах, то есть использовалась сразу в нескольких зависимостях. При загрузке система проверяет, лежит ли эта библиотека по определённому пути, и когда к ней обращалось сразу несколько процессов, один из них мог сказать, что файл уже существует; либо говорил, что нужного пути нет и скачать туда библиотеку он не может. В любом случае загрузка прерывалась.

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

Безопасное хранение учётных данных для доступа к репозиториям зависимостей на CI-узлах


Избавившись от проблем с хранением и загрузкой зависимостей, мы озаботились информационной безопасностью. В Carthage есть приватные репозитории, которые совместно используются несколькими командами. И в случае пересборки зависимостей Carthage должен иметь доступ к репозиторию для скачивания исходников. С разработчиками это решается просто: создаёшь задачу, в ней указываешь, куда тебе нужен доступ, и получаешь его. Но ведь есть ещё и многочисленные CI-машины, которым слишком хлопотно выдавать доступ, да ещё и с поддержкой.

Все наши приватные репозитории хранятся в GitLab, поэтому проблему мы решали через Deploy keys, которые добавили в каждый проект и раскатили на все CI-машины. Это позволило всем иметь один общий read-доступ к приватным репозиториям. В CI нужен лишь доступ на чтение — Carthage скачивает зависимости без ввода пароля, — а разработчикам нужен доступ ещё и на запись, поэтому им мы оставили учётные данные.

Автоматизация работы с кешем


На этом мы закончили решать проблемы и занялись автоматизацией. Необходимо было бесшовно для разработчиков переехать на удалённый кеш. Сначала нужно было внедрить проверку зависимостей, ведь мы убрали Carthage из контроля версий и не хранили их локально. Рассмотрели вариант со скриптом проверки в фазе сборки. Нам он не подошёл, потому что при сборке весь кеш должен быть загружен в проект, ведь неизвестно, какие ещё скрипты могут запускаться для создания путей. Тогда мы обратили внимание на Pre-actions Xcode.

Настройка Pre-actions в Xcode


Pre-actions — это функция Xcode, позволяющая выполнять дополнительные действия перед началом Build-фазы. Для всех таргетов мы добавили в эту фазу вызов проверяющего скрипта (параллельная сборка могла запустить любой таргет первым). Задача скрипта — убедится, что мы можем начинать фазу сборки, или же запустить скрипт на скачивание недостающих зависимостей, если таковые есть. А поскольку Pre-action не может отловить stdout-поток выполнения скриптов, мы журналируем в файл вызов всех проверок для контролирования процесса:

exec > $PROJECT_DIR/carthage-cache-verify.log 2>&1
source $PROJECT_DIR/tools/Rome/rome-launcher.sh


Задача rome-launcher.sh — убедиться в валидности кэша и запустить его скачивание, если произошла ошибка валидации:

#!/bin/sh

set -e
# Данный лаунчер вписан в pre-actions Xcode и запускается каждый раз при "build clean" и "build"
cd "${PROJECT_DIR}"
if ${PROJECT_DIR}/tools/Rome/rome-cache-download-verify.swift; then
    echo "Done carthage validation"
else
    echo "Run catrhage download"
    ${PROJECT_DIR}/tools/Rome/rome-download.sh
fi

За проверку отвечает rome-cache-download-verify.swift, а за скачивание — rome-download.sh.

Проверка на наличие изменений в конфигурационных файлах Carthage


В системе контроля версий мы оставили только файлы Cartfile и Cartfile.resolved, а все остальные Carthage-файлы внесли в gitignore. Для фиксации состояния собранного кеша мы добавили в проект конфигурационный файл, который хранит в себе MD5-хеши Cartfile и Cartfile.resolved, а также наш prefix. Этот файл тоже занесли в gitignore, он автоматически генерируется после скачивания всех зависимостей (или их сборки) и фиксирует их состояние на локальной машине. Это позволило при переходе между ветками в git или rebase сравнивать состояние Carthage-файлов и не запускать лишний раз скрипты на скачивание зависимостей, если были изменения в этих файлах или в префиксе.

Дополнительные проверки скачанных зависимостей


Кроме изменений конфигурации мы добавили и другие проверки:

  • Наличие папки Carthage.
  • Наличие всех библиотек, которые прописаны в Romefile.

Это покрывало большинство ситуаций, когда мог быть изменён кеш.

Генерирование Romefile


Мы живём не в идеальном мире, поэтому столкнулись с тем, что некоторые зависимости у нас в Carthage прописаны в другом формате, отличающемся от принятого: вместо названия библиотек были просто ссылки на GitLab, хеш коммита. Поэтому Rome не могла автоматически найти все зависимости и соотнести с деревом Carthage. Пришлось сделать генератор для Romefile, благо Rome позволяет самому указать дерево зависимостей через Repository map.

По Romefile мы проверяем, каких библиотек нам не хватает: при запуске из кода в Pre-actions смотрим, лежат ли в папке Carthage/Builds все зависимости, которые указаны в Repository map. Если нет, то нужно обратиться к S3 и скачать необходимые файлы. А если вдруг окажется, что какой-то зависимости нет в удалённом хранилище, то можно собрать её локально; но это на крайний случай, ведь мы стараемся облегчить жизнь разработчикам.

Настройка Job в Jenkins


При использовании Pre-actions основной процесс сборки у нас не поменялся: везде были проверки кеша и скачивание зависимостей. Но когда другие команды меняли общие зависимости и хотели протестировать изменения в нашем проекте, им приходилось идти по долгому нашему flow для получения необходимой версии приложения для тестирования, что отнимает очень много времени. Поэтому мы сделали отдельную Job для Jenkins, которая позволяет собрать проект с текущими настройками, указав конкретный Cartfile и Cartfile.resolved, и получить готовую сборку с новыми зависимостями, чтобы протестировать её перед вливанием.

Интеграция в систему code-review (Gerrit)


И последняя наша задача по автоматизации: что бы разработчик ни делал на своей машине, он может просто обновить у себя Cartfile и в нашей системе code review командой !dp запустить вторую Job, которая собирает зависимости и отправляет их в удалённое S3-хранилище. То есть пользователь может вносить любые изменения, потом выполняет одну команду и скоро получает кеш изменённых библиотек. Теперь другим разработчикам достаточно просто обновить свои Cartfile и скачать новый кеш; или если при rebase, merge или смене ветки будут изменения в Cartfile, то заново собирать ничего не нужно, достаточно докачать изменённые файлы.

Резюме


Мы смогли связать Carthage + Rome + S3 с помощью утилит и наших скриптов и заставить это работать как единое целое.

Мы рассказали об интеграции библиотеки Rome и описали, как решали имевшиеся у нас проблемы. Разработчики довольны, их работе наши нововведения не помешали, а только облегчили её. Мы ещё не внедрили совместное использование Rome с другими командами, но уже заложили для этого фундамент.

Комментарии (4)


  1. pingwinator
    13.05.2022 21:53

    а зачем делать

    Сборка кеша под нужную версию toolchain

    если у вас есть уже

    Работа с XCFramework

    ? достаточно сделать всего 1 билд и кешировать его

    как бы они уже бинарно совместимые с будущими версиями сдк. пруф. первые 3 мин, но в целом видео полезное.


    1. Northex Автор
      14.05.2022 09:45

      Для поддержания бинарной совместимости необходимо собирать зависимости с включенным флагом:
      BUILD_LIBRARY_FOR_DISTRIBUTION
      (почитать про это можно здесь)
      Так же включение данного флага накладывает определенные границы по совместимости. В нашем случае проще выполнить одну команду и собрать все зависимости и автоматически залить их на s3, чем в ручном режиме пробовать включать стабильность модуля для каждой зависимости. Так же у есть библиотеки, которые не работают с данным флагом


  1. mikhailmaslo
    13.05.2022 23:32

    Спасибо за статью! В Joom решали очень похожую задачу - кэширование carthage-зависимостей и проверку на актуальность Carthage/Build

    В префикс кэша мы дополнительно включаем конфигурацию: Debug / Release. Из-за этого, когда возникает задача проверки актуальности зависимостей в Build , то надо узнать как эти зависимости собраны через Release или Debug конфигурацию (иначе можно, например, собрать Release Candidate с зависимостями собранными с Debug конфигурацией)

    Сталкивались ли с такой задачей? Как решали?


    1. Northex Автор
      14.05.2022 09:47

      Вам, спасибо!
      С подобным не сталкивались, т.к. все зависимости собраны с Release конфигурацией, поэтому мы проверяем только наличие данной библиотеки в папке Carthage/Build