Сборка и распространение XCFramework на облачном Mac: бинарный кэш для интеграции нескольких репозиториев

DevOps и CI/CD ·~4 мин чтения

Сборка и распространение XCFramework на облачном Mac: бинарный кэш для интеграции нескольких репозиториев

Сборка и распространение XCFramework на облачном Mac: бинарный кэш для интеграции нескольких репозиториев

На прошлой неделе три репозитория закоммитили изменения одновременно, и в CI один и тот же внутренний фреймворк обработки аудио пересобирался три раза подряд, каждый раз тратя почти восемь минут на archive. Когда третья сборка провалилась с ошибкой протухшего сертификата подписи, дежурный коллега спросил в чате: «Мы же его вчера уже собирали, почему каждый репозиторий делает это заново?» Этот вопрос бьёт по больному месту многих команд — общий бинарный фреймворк должен собираться один раз, но из-за отсутствия механизма кэширования превращается в дублирующую работу, которую каждый пайплайн выполняет сам по себе.

На выделенном облачном Mac от HireVPS мы пересобрали этот процесс с нуля: единая компиляция мультислайсового XCFramework, подпись, вычисление контрольной суммы и выкладка артефакта в лёгкий локальный бинарный кэш, из которого несколько репозиториев забирают готовый продукт напрямую, без повторной сборки каждым из них. Ниже — конкретная реализация.

Почему стоит кэшировать XCFramework самостоятельно

Swift Package Manager поддерживает binaryTarget, который позволяет напрямую ссылаться на предварительно собранный XCFramework — сама по себе это не новость, но чтобы это действительно работало, нужно выполнить условие: «артефакт должен быть надёжным, отслеживаемым и доступным для совместного использования». Если каждый репозиторий в CI гоняет собственный archive, вы получаете несколько бинарников, которые «теоретически одинаковы, но фактически никем не проверены на идентичность» — и как только в одной из сборок появится небольшое отличие в окружении (например, обновилась минорная версия Xcode), разбираться с этим будет крайне неприятно. Только централизованная сборка и единая точка раздачи гарантируют, что все нижестоящие потребители получают строго один и тот же артефакт.

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

Преимущество ресурсов облачного Mac

У этого процесса есть два жёстких требования к машине: во-первых, нужна выделенная графическая и командная среда macOS для полноценного запуска xcodebuild archive; во-вторых, при компиляции нескольких архитектурных слайсов дисковый I/O нагружается прилично, и на машине с общими ресурсами задачи начинают тормозить друг друга. На HireVPS машина M4 или M4 Pro арендуется по дням: выбираете узел, получаете отдельные SSH-креды, права root доступны сразу — не нужно делить процессорное время с чужими задачами, и многочасовую мультиархитектурную сборку не прервёт «соседняя» задача. Какая конкретно модель и узел доступны сейчас — проверяется в панели управления.

Сборка мультислайсового XCFramework

XCFramework, который одновременно подходит для реального устройства и симулятора, должен содержать минимум два слайса: ios-arm64 (реальное устройство) и ios-arm64-simulator (симулятор на Apple Silicon). Если в команде есть Intel Mac, на котором запускаются тесты в симуляторе, не забудьте добавить ios-x86_64-simulator — иначе на такой машине при первом же тесте вылезет ошибка несовпадения архитектуры.

Архивируем оба слайса по отдельности

xcodebuild archive \
  -scheme AudioCore \
  -destination "generic/platform=iOS" \
  -archivePath build/AudioCore-iOS.xcarchive \
  SKIP_INSTALL=NO \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

xcodebuild archive \
  -scheme AudioCore \
  -destination "generic/platform=iOS Simulator" \
  -archivePath build/AudioCore-iOSSim.xcarchive \
  SKIP_INSTALL=NO \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

Объединяем в XCFramework

xcodebuild -create-xcframework \
  -framework build/AudioCore-iOS.xcarchive/Products/Library/Frameworks/AudioCore.framework \
  -framework build/AudioCore-iOSSim.xcarchive/Products/Library/Frameworks/AudioCore.framework \
  -output build/AudioCore.xcframework

После сборки упакуйте результат в zip — этот файл и есть финальный артефакт для распространения, дальше руками его больше не трогайте.

Подпись и контрольная сумма

Внутренние фреймворки тоже стоит подписывать — это упрощает проверку происхождения на стороне потребителя:

codesign --sign "Apple Distribution: Your Team" \
  --timestamp \
  build/AudioCore.xcframework

zip -r AudioCore-1.4.0.xcframework.zip build/AudioCore.xcframework

После подписи и упаковки вычислите контрольную сумму встроенной командой SwiftPM — это значение нужно прописать в Package.swift нижестоящего репозитория:

swift package compute-checksum AudioCore-1.4.0.xcframework.zip

Самая частая причина сбоя проверки контрольной суммы — правка содержимого zip после того, как сумма уже вычислена, либо смена инструмента архивации, из-за чего меняется внутренний порядок файлов. Правило простое: после вычисления контрольной суммы zip-файл замораживается и больше не изменяется.

Разворачиваем локальный бинарный кэш

Сложный сервис не нужен — достаточно каталожной структуры со статической раздачей файлов:

Уровень Содержимое Пример пути
Имя фреймворка Отдельный каталог для каждого внутреннего фреймворка /cache/AudioCore/
Номер версии Подкаталог для каждой версии /cache/AudioCore/1.4.0/
Артефакт zip-архив + текстовый файл с контрольной суммой AudioCore-1.4.0.xcframework.zip, checksum.txt

В Package.swift нижестоящего репозитория ссылка выглядит так:

.binaryTarget(
    name: "AudioCore",
    url: "https://cache.internal.example/AudioCore/1.4.0/AudioCore-1.4.0.xcframework.zip",
    checksum: "контрольная сумма, вычисленная на предыдущем шаге"
)

По объёму хранилища: команде среднего размера достаточно держать 3-4 ключевых фреймворка, каждый с историей последних 10 версий; один архив обычно занимает 20-80 МБ, общий объём укладывается в несколько ГБ, а версии, к которым не обращались более 90 дней, можно периодически удалять.

Интеграция в CI нескольких репозиториев

CI каждого нижестоящего репозитория больше не запускает xcodebuild archive — достаточно swift package resolve, чтобы забрать нужную версию, и время сокращается с нескольких минут компиляции до нескольких секунд загрузки. При выпуске новой версии просто создаётся новый каталог с номером версии в кэше и обновляется ссылка на checksum в соответствующем репозитории — код сервера кэша трогать не нужно.

Предупреждение о частой ошибке: никогда не давайте нескольким репозиториям одновременно ссылаться на изменяемый тег типа «latest». Механизм контрольных сумм требует однозначного соответствия URL и содержимого — если разрешить перезапись файлов под одним и тем же номером версии, проверка целостности SwiftPM теряет смысл.

Чек-лист

Часто задаваемые вопросы

Какие срезы нужны XCFramework для устройства и симулятора одновременно?

Минимум нужны ios-arm64 для реального устройства и ios-arm64-simulator для симулятора на Apple Silicon; если в команде есть Intel Mac с симулятором, добавьте ios-x86_64-simulator — иначе на этой машине тесты сразу упадут из-за несовпадения архитектуры.

Почему не проходит проверка контрольной суммы binaryTarget?

Чаще всего причина — ручное изменение zip-файла после упаковки или использование другого инструмента сжатия, изменившего порядок файлов внутри архива; правильный путь — один раз выполнить swift package compute-checksum на финальном zip-файле для распространения и больше его не трогать.

Сколько места нужно закладывать под локальный сервер бинарного кэша?

Для команды среднего размера, хранящей последние 10 версий 3-4 ключевых фреймворков, один сжатый артефакт обычно занимает 20-80 МБ, так что нескольких гигабайт достаточно; разместите кэш на отдельном разделе SSD облачного Mac и регулярно удаляйте версии, не запрошенные более 90 дней.

HireVPS Cloud Mac

Попробуйте выделенный облачный Mac mini уже сегодня

Аренда от одного дня, доступ по SSH/VNC за 2 минуты, конфигурацию можно повысить в любой момент.

Арендовать Mac mini