Технический прогон

Первичная настройка Xcode для CI на облачном Mac

Первичная настройка Xcode для CI на облачном Mac

Даже если к облачному Mac уже можно подключиться по SSH, а xcodebuild -version корректно выводит версию, первое задание без участия пользователя всё равно может остановиться на подтверждении лицензии, установке дополнительных компонентов или переключении пути к инструментам. Особенно неприятно, что такие сбои часто проявляются только на новых узлах, после обновления Xcode или пересоздания runner. Однократный ручной запуск графического интерфейса при этом временно маскирует проблему. Надёжнее считать первичную настройку отдельным этапом подготовки узла, а не исправлять окружение во время рабочей сборки.

Разделите инициализацию узла и задания сборки

Инициализация узла может изменять состояние системы, требует прав администратора и выполняется один раз для каждой версии Xcode. Задания сборки, напротив, должны быть повторяемыми, не требовать интерактивного ввода и по возможности обходиться без повышения привилегий. Если объединить эти процессы в одном CI-скрипте, возникают три проблемы: несколько заданий одновременно устанавливают компоненты, sudo в фоновом режиме ожидает ввода, а после сбоя невозможно определить, вызван ли он ошибкой в коде или незавершённой настройкой окружения.

Жизненный цикл runner рекомендуется разделить на три этапа:

  1. Установить или переключить Xcode, подтвердить лицензию и завершить первичную настройку компонентов.
  2. Выполнить предварительную проверку только для чтения и записать фактически используемый каталог Developer и версии инструментов.
  3. Разрешить runner принимать задания только после успешной предварительной проверки.

Возможность открыть Xcode не является критерием готовности. Узел можно считать готовым, только если пользователь runner способен в неинтерактивной Shell найти инструменты компиляции, получить сведения об SDK и выполнить минимальный анализ проекта.

Закрепите фактически используемый путь Xcode

Если на машине установлено несколько версий Xcode, не полагайтесь только на глобальную настройку xcode-select. Она влияет на другие задания на том же физическом узле, а после переключения версии легко получить ситуацию, когда в журнале указана одна версия, но фактически вызывается другая. Для CI лучше закреплять DEVELOPER_DIR на уровне задания.

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

printf 'developer_dir=%s
' "$DEVELOPER_DIR"
xcodebuild -version
xcrun --find clang
xcrun --find simctl
xcodebuild -showsdks

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

Не заменяйте проверку в Shell состоянием GUI

Переменные окружения, Shell по умолчанию и контекст прав доступа в сеансе удалённого рабочего стола могут отличаться от окружения службы runner. Все проверки следует выполнять от имени того же пользователя, который запускает сборку. В частности, нужно убедиться, что результат xcrun --find находится внутри ожидаемого DEVELOPER_DIR, а не просто проверить наличие каталога приложения.

Выполните первичную настройку один раз

На этапе подготовки узла сначала проверьте состояние первичного запуска, а затем завершите подтверждение лицензии и подготовку компонентов в процессе инициализации с правами администратора:

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

if ! xcodebuild -checkFirstLaunchStatus; then
  sudo xcodebuild -license accept
  sudo xcodebuild -runFirstLaunch
fi

xcodebuild -checkFirstLaunchStatus

Важно не столько содержание команд, сколько место их выполнения. Они не должны входить в сборочный скрипт, запускаемый при каждом коммите. Команда runFirstLaunch может изменять общие компоненты. Если два задания выполнят её одновременно, это не только займёт лишнее время, но и может привести к тому, что одно из заданий увидит незавершённое состояние.

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

Защитите параллельную инициализацию атомарной блокировкой

При автоматическом масштабировании или одновременном запуске нескольких служб скрипт инициализации может быть вызван параллельно, даже если он рассчитан на однократное выполнение. Штатных инструментов macOS достаточно, чтобы реализовать атомарную блокировку через создание каталога:

set -euo pipefail

lock_dir="/tmp/xcode-bootstrap.lock"

if ! mkdir "$lock_dir" 2>/dev/null; then
  echo "Xcode bootstrap is already running" >&2
  exit 75
fi

cleanup() {
  rmdir "$lock_dir" 2>/dev/null || true
}
trap cleanup EXIT

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
xcodebuild -checkFirstLaunchStatus

Операция mkdir атомарна в пределах одной файловой системы, поэтому успешно выполнить её сможет только один процесс. Планировщик может интерпретировать код завершения 75 как временный сбой и повторить попытку позднее, не помечая узел как постоянно неисправный для сборок.

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

Добавьте предварительную проверку перед приёмом заданий

После успешной проверки состояния первичного запуска выполните ещё одну приёмочную проверку, не изменяющую систему. Как минимум она должна охватывать идентификацию Xcode, запрос SDK, инструменты симулятора и точку входа в проект:

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

xcodebuild -checkFirstLaunchStatus
xcodebuild -version
xcodebuild -showsdks >/tmp/xcode-sdks.txt
xcrun simctl list runtimes
xcodebuild -list -workspace "Example.xcworkspace"

В последней строке укажите реальный workspace или project из репозитория. Такая проверка выполняется быстрее полного archive, но позволяет заранее обнаружить отсутствие точки входа в проект, невыполненные предварительные условия для разрешения зависимостей или неверный рабочий каталог runner.

Какие данные сохранять при сбое

В диагностическом журнале следует сохранять как минимум DEVELOPER_DIR, вывод xcodebuild -version, xcrun --find clang, xcrun simctl list runtimes и код завершения неудачной команды. Не выводите в журнал полный набор переменных окружения: среди них могут быть токены, пароли для подписи или учётные данные репозитория.

Обычно диагностику проводят в следующем порядке: сначала проверяют путь, затем состояние первичного запуска, после этого наличие нужных SDK и runtime и только в конце переходят к конфигурации проекта. После переключения версии Xcode весь процесс необходимо повторить для нового каталога Developer, даже если предыдущая версия уже была инициализирована. По завершении сохраните вместе версии скрипта инициализации и Xcode. Тогда при пересоздании runner окружение можно будет восстановить в том же порядке, не полагаясь на то, что кто-то ранее вручную открывал определённый интерфейс.

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

Почему Xcode открывается вручную, но CI сообщает о незавершённом первом запуске?

Интерактивная сессия и runner могут использовать разные установки Xcode. Проверку нужно выполнять с тем же DEVELOPER_DIR, который задан в задании CI.

Стоит ли запускать sudo xcodebuild -runFirstLaunch в каждом задании CI?

Нет. Команду выполняют один раз при подготовке узла или смене версии Xcode. В заданиях оставляют только проверки без изменения системы.

Нужна ли повторная инициализация после смены версии Xcode?

Да. Для нового каталога Developer отдельно проверяют лицензию, компоненты инструментов и доступные runtime симуляторов.

MacVPSGo Облачный Mac

Нужна выделенная машина для сборки на Apple Silicon?

Арендуйте выделенный физический узел на день, неделю, месяц или квартал для сборки в Xcode, iOS CI, удалённой разработки и автоматизации.

Выбрать конфигурацию и заказать