Инженерная статья

Сборка iOS Framework на Kotlin Multiplatform в облачном Mac

Сборка iOS Framework на Kotlin Multiplatform в облачном Mac

То, что репозиторий Kotlin Multiplatform успешно собирается локально, не означает, что после переноса на облачный Mac результат останется тем же. Чаще всего различия связаны не с бизнес-логикой, а с сочетанием версий Java, Gradle Wrapper, Kotlin/Native, CocoaPods и Xcode, а также с ошибочным смешиванием артефактов для физических устройств и симуляторов. Надёжный подход — сначала зафиксировать цепочку инструментов, а затем по отдельности проверить общий модуль, Framework и итоговый workspace Xcode.

Сначала зафиксируйте входные данные сборки

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

sw_vers
uname -m
xcodebuild -version
xcode-select -p
java -version
./gradlew --version
bundle exec pod --version

В репозиторий должны быть добавлены gradlew, gradle/wrapper/gradle-wrapper.properties, файлы блокировки зависимостей, Podfile.lock и файл блокировки зависимостей Ruby. В CI используйте ./gradlew, а не глобальную версию Gradle, случайно установленную на машине. CocoaPods также следует запускать через bundle exec pod, чтобы разные задания не загружали разные версии gem-пакетов.

Если требуется переключить версию Xcode, явно задайте её в начале задания и сразу же проверьте результат:

sudo xcode-select -s /Applications/Xcode.app
xcodebuild -version

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

Разделите цели для устройства и симулятора

На Mac с Apple Silicon симулятор iOS обычно тоже использует архитектуру arm64, но это не та же целевая платформа, что и физический iPhone. Если проверять только архитектуру процессора, на этапе компоновки могут возникнуть труднообъяснимые ошибки.

Для общего модуля можно явно сохранить две цели:

kotlin {
    iosArm64()
    iosSimulatorArm64()

    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
        .configureEach {
            binaries.framework {
                baseName = "SharedKit"
                isStatic = true
            }
        }
}

Сначала соберите общий модуль отдельно, чтобы сократить цепочку диагностики:

./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 \
  --no-daemon --stacktrace

./gradlew :shared:linkReleaseFrameworkIosArm64 \
  --no-daemon --stacktrace

Переходите к уровням CocoaPods и Xcode только после успешного выполнения обеих задач. Если нужной задачи нет, проверьте путь к модулю, объявление цели и имя Framework. Не обходите проблему конфигурации копированием уже созданного артефакта.

Разрешите CocoaPods и Xcode использовать только зафиксированные результаты

CI не должен обновлять Pods при каждой сборке. Сначала создайте или проверьте podspec, а затем установите зависимости в строгом соответствии с Podfile.lock:

./gradlew :shared:podspec
bundle check
bundle exec pod install --project-directory=iosApp

После этого обязательно собирайте workspace, а не файл проекта:

xcodebuild \
  -workspace iosApp/App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.build/DerivedData" \
  CODE_SIGNING_ALLOWED=NO \
  build

CODE_SIGNING_ALLOWED=NO подходит для проверки сборки под симулятор, но не заменяет настройку подписи, необходимую для архивирования приложения под физическое устройство. Если проект использует собственные конфигурации, убедитесь также, что соответствующие файлы .xcconfig не задают пути поиска Framework через локальные абсолютные пути.

Сужайте область поиска с помощью поуровневых проверок

Уровень проверки Рекомендуемая команда Что проверять в первую очередь при ошибке
Исходный код Kotlin ./gradlew :shared:compileKotlinIosSimulatorArm64 expect/actual, видимость зависимостей
Компоновка Framework linkDebugFrameworkIosSimulatorArm64 целевая платформа, нативные зависимости
Интеграция Pods bundle exec pod install файлы блокировки, окружение Ruby, podspec
Сборка Xcode xcodebuild -workspace Scheme, пути поиска, конфигурация сборки

Кешируйте загрузки, но не используйте общую рабочую область с правом записи

Каталоги ~/.gradle/caches и ~/.konan обычно стоит кешировать, поскольку их повторная загрузка обходится дорого. С Pods и DerivedData следует обращаться осторожнее. Если параллельные задания совместно используют доступный для записи каталог DerivedData, одно задание может прочитать промежуточные файлы из другой ветки. Самый надёжный вариант — создавать отдельный каталог для каждого задания и удалять его после завершения.

Ключ кеша должен включать как минимум:

  • хеши gradle-wrapper.properties и файлов блокировки зависимостей
  • хеши libs.versions.toml, Podfile.lock и файла блокировки зависимостей Ruby
  • вывод xcodebuild -version
  • основную версию Java и версию плагина Kotlin
  • целевую конфигурацию Debug или Release

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

Определяйте причину по виду ошибки и сохраняйте диагностические данные

При появлении сообщения building for iOS Simulator, but linking in object file built for iOS в первую очередь проверьте, для какой цели был создан Framework, а не переустанавливайте CocoaPods. При ошибке framework not found проверьте развёрнутые значения путей поиска в Build Settings, фактическое расположение Framework и то, выполняется ли этап скрипта до компиляции.

Ключевые диагностические данные можно сохранить как артефакты задания:

xcodebuild -showBuildSettings \
  -workspace iosApp/App.xcworkspace \
  -scheme App > .build/build-settings.txt

find shared -type d -name '*.framework' -print \
  > .build/framework-inventory.txt

Если локальная сборка проходит, а удалённая завершается ошибкой, сравните xcodebuild -version, java -version, хеши файлов блокировки, переменные окружения и время создания workspace. Не начинайте с полной очистки кешей: это уничтожит диагностические данные и может временно скрыть проблему, не объяснив её причину.

Минимальный набор проверок перед слиянием

Запрос на слияние должен как минимум проверять сборку общего модуля для симулятора, сборку workspace Xcode и компоновку Release Framework для физического устройства. Параллельные задания должны использовать отдельные каталоги DerivedData, журналы — сохранять версии инструментов и полную команду, завершившуюся ошибкой, а артефакты сборки — именоваться с учётом платформы. При соблюдении этих требований границы ответственности между уровнями Kotlin, Pods и Xcode становятся достаточно прозрачными, чтобы большинство сбоев можно было локализовать за один запуск конвейера.

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

Можно ли использовать arm64 Framework для устройства в Apple Silicon Simulator?

Нет. Архитектура команд может совпадать, но целевые платформы различаются. Необходимо отдельно собирать iosArm64 и iosSimulatorArm64.

Что кешировать при сборке Kotlin Multiplatform в CI?

В первую очередь кешируют загрузки Gradle и инструменты Kotlin/Native. В ключ кеша включают lock-файлы, Gradle Wrapper, версии Xcode и Java.

Выделенный физический узел

Перенесите задачи сборки на выделенный облачный Mac

Выберите VMOrbit M4 или VMOrbit M4 Pro и узел с учётом расположения команды. Фактическая доступность и сведения о выдаче отображаются в консоли в реальном времени.

Выбрать облачный Mac