Число 65 в завершении xcodebuild сообщает только о неуспешном действии, но не называет его причину — это следует проверять по полному журналу и результату тестирования, а не по последней строке CI (описание командной строки xcodebuild от Apple). Поэтому на этой неделе не удаляйте DerivedData и не пересоздавайте узел сразу: сохраните исходную команду, файл xcresult, окружение и найдите первый отказавший этап. Затем сравните Scheme, зависимости, Destination, подпись и состояние удалённого Mac; к перестройке узла переходите только тогда, когда ошибка не воспроизводится в чистой среде.
Кому нужен этот разбор
Эта инструкция предназначена для разработчика, у которого проект собирается в локальном Xcode, но удалённый Mac CI возвращает xcodebuild Exit Code 65.
Она также пригодится DevOps-инженеру, который обслуживает общий macOS-узел и должен удерживать одинаковыми Xcode, права, зависимости и параметры запуска. Релизный инженер найдёт здесь границу между ошибкой компиляции, тестирования, подписи и архивации.
Временная шкала диагностики: от факта сбоя до подтверждённого исправления
Не превращайте расследование в последовательность случайных очисток. Разделите работу на контрольные точки:
- Фиксация: сохраните commit, точную команду
xcodebuild, рабочий каталог, путь к Xcode, аккаунт CI и момент запуска. - Локализация: определите первое действие, которое завершилось ошибкой: разрешение пакетов, компиляция, запуск тестов, подпись или экспорт.
- Минимальный прогон: уберите только необязательные параметры, но сохраните тот же проект, Scheme и Destination.
- Исправление: меняйте одну причину за раз и записывайте, какой симптом исчез.
- Повторная проверка: выполните исходную команду, затем минимальную команду и повторный запуск после перезапуска узла.
Такой порядок важнее самого кода завершения. Последующие сообщения часто являются следствием первого сбоя: например, тестовая цель не запускается потому, что приложение не было собрано. Сообщение оболочки CI о ненулевом статусе — это ещё один слой упаковки, а не доказательство конкретной ошибки Xcode.
Первый этап: извлеките реальную ошибку из журнала
Сначала сохраните весь stdout и stderr, а не только финальные строки. В команде должны быть видны фактические значения:
xcodebuild \
-workspace "App.xcworkspace" \
-scheme "AppScheme" \
-configuration "Release" \
-destination "platform=iOS Simulator,id=DEVICE_IDENTIFIER" \
-resultBundlePath "artifacts/result.xcresult" \
test
Названия workspace, Scheme, устройства и каталогов здесь условные. Параметры -workspace, -scheme, -configuration, -destination и -resultBundlePath относятся к командному интерфейсу xcodebuild; их назначение сверяйте с техническим описанием командной строки Apple.
Ищите первое сообщение с формулировкой error:, отказ команды скрипта или недоступную цель. Затем отделяйте его от:
- повторного сообщения о той же проблеме;
- ошибок зависимых целей;
- финального
** TEST FAILED **; - строки CI-оболочки о коде завершения.
xcresult нельзя заменять коротким текстовым логом. В нём могут находиться сведения о тестовой сессии, цели, устройстве и диагностике. Apple описывает чтение результатов тестирования и структуру диагностических данных в документации по интерпретации результатов Xcode.
Локальная сборка против удалённого запуска: где расходятся входные данные
Фраза «локально всё работает» не доказывает эквивалентность двух запусков. В локальном Xcode часть значений берётся из выбранной схемы, графической сессии, локальной связки ключей и уже загруженных пакетов. CI может использовать другой workspace, аккаунт, каталог, SDK или набор переменных.
Scheme, workspace и Build Settings
Проверьте следующие пары:
| Объект проверки | Локальный запуск | Удалённый Mac CI | Что подтверждает проблему |
|---|---|---|---|
| Файл проекта | .xcodeproj или .xcworkspace |
Файл, указанный в скрипте | Разные графы целей и пакетов |
| Scheme | Выбранная в Xcode | Переданная через -scheme |
Scheme не существует или не shared |
| Configuration | Например, Debug или Release |
Значение из команды или CI | Отличаются флаги, подпись или скрипты |
| SDK и Destination | Выбор Xcode | Значение -destination |
Цель недоступна на узле |
| Build Settings | Проект и конфигурация | Проект плюс параметры команды | Команда переопределяет проект |
Сначала выведите доступные Scheme и Destination, затем раскройте фактические настройки:
xcodebuild -list -workspace "App.xcworkspace"
xcodebuild -showBuildSettings \
-workspace "App.xcworkspace" \
-scheme "AppScheme" \
-configuration "Release"
Командная строка имеет приоритет над значениями, заданными в проекте и конфигурации. Поэтому ищите в CI такие переопределения, как CODE_SIGN_STYLE, PRODUCT_BUNDLE_IDENTIFIER, SDKROOT, SWIFT_VERSION или собственные переменные проекта. Правила областей и приоритетов описаны в документации Apple по Build Settings.
Не начинайте с удаления кеша. Если -showBuildSettings уже показывает неверную конфигурацию, очистка лишь удалит полезные следы и не исправит источник расхождения.
Зависимости и Run Script: ошибка может произойти до компилятора
Если первым отказало разрешение Swift Package, проверьте Package.resolved, доступ к приватному репозиторию, SSH-ключ CI и known_hosts. Удалённая машина может видеть тот же репозиторий, но работать под другим пользователем с другим HOME и другим каталогом .ssh.
Для воспроизводимости зафиксируйте состояние зависимостей в репозитории. Apple отдельно рассматривает сборку Swift Package и приложений с такими пакетами в руководстве по зависимостям в CI. Это не означает, что любой сетевой отказ является ошибкой Xcode: сначала подтвердите, какая команда разрешения пакета завершилась первой.
У Run Script Phase проверяйте:
- рабочий каталог;
- используемую оболочку;
- значения переменных среды;
- наличие входного файла;
- права на создание выходного файла;
- код завершения самого скрипта.
Скрипт, который завершился с ошибкой или не создал заявленный результат, способен остановить сборку до компиляции исходников. В таком случае исправлять Swift-код или менять Simulator преждевременно.
Важно: успешное разрешение пакетов ещё не означает успешную сборку, а запуск Simulator не подтверждает работоспособность тестовой цепочки. На каждом этапе сохраняйте отдельное доказательство.
Сборка против тестирования: как Simulator превращает другой сбой в Exit Code 65
Когда приложение компилируется, но тестовая сессия не начинается, проверяйте не только наличие Simulator. Сопоставьте платформу, установленный runtime, идентификатор устройства и поддержку выбранной Scheme. Destination, подходящий для одной цели, может быть недоступен для другой.
Используйте тот же Destination в диагностическом и контрольном запусках:
xcodebuild \
-workspace "App.xcworkspace" \
-scheme "AppScheme" \
-destination "platform=iOS Simulator,id=DEVICE_IDENTIFIER" \
-resultBundlePath "artifacts/test-result.xcresult" \
test
В xcresult ищите, была ли создана тестовая сессия, какое устройство выбрано и на каком этапе появилась ошибка. Apple описывает результаты тестирования как источник сведений о тестах, действиях и диагностике, поэтому финальная строка Exit Code 65 не должна быть единственным артефактом расследования (документация о результатах Xcode).
Полезное различие выглядит так:
| Наблюдаемый симптом | Вероятная зона проверки | Следующий тест |
|---|---|---|
| Компилятор остановился до запуска приложения | Исходники, зависимости, Build Settings | Повторить build с той же Scheme |
| Приложение собрано, но тестовая сессия не создана | Destination, runtime, тестовая цель | Проверить список доступных целей и xcresult |
| Simulator стартует, но тесты не выполняются | Scheme, тестовый bundle, окружение процесса | Повторить test с тем же устройством |
| Сбой появляется на подписи перед запуском | Сертификат, профиль, связка ключей | Выполнить отдельный подписывающий прогон |
| CI показывает только код 65 | Недостаточно диагностических артефактов | Сохранить полный лог и результат |
Вопрос о том, почему запуск Simulator способен закончиться этим кодом, решается не сбросом устройства, а определением границы отказа. Если xcresult показывает, что тестовый процесс не был запущен, сначала проверяйте Destination и runtime. Если тест начался и упал внутри приложения, причина уже находится в тестовой логике или среде выполнения.
Подпись и архив: отдельная граница между сборкой и выпуском
Проверяйте подпись только тогда, когда первый подтверждённый отказ указывает на сертификат, профиль, Team или связку ключей. Автоматическая подпись, успешно работающая в графическом Xcode под вашей учётной записью, не доказывает, что CI-пользователь имеет доступ к закрытому ключу.
Сопоставьте:
- Team и идентификатор приложения;
- способ подписи для выбранной конфигурации;
- сертификат и соответствующий закрытый ключ;
- provisioning profile;
- связку ключей, доступную именно пользователю CI;
- состояние разблокировки связки в момент выполнения.
Для архива и экспорта сохраняйте разные артефакты:
xcodebuild archive \
-workspace "App.xcworkspace" \
-scheme "AppScheme" \
-configuration "Release" \
-archivePath "artifacts/App.xcarchive"
Путь и имена здесь приведены как заменяемые примеры. Диагностические символы и настройки сборки также следует сохранять отдельно: Apple указывает, что параметры сборки, связанные с отладочной информацией, влияют на состав результатов и последующую диагностику (документация Apple о включении отладочной информации).
Не используйте отключение подписи как универсальный ремонт. Для локального build это может убрать один барьер, но для архива или распространения создаст другой, а исходную проблему с сертификатом оставит нерешённой. После исправления выполните отдельно сборку, архивирование и экспорт, сохраняя результат каждого действия.
Действия на удалённом Mac: ремонт, изоляция или пересоздание
После проверки проекта сравните среду выполнения. Зафиксируйте:
xcode-select -p
xcodebuild -version
printenv DEVELOPER_DIR
whoami
pwd
df -h
Пути, версии и имена пользователей зависят от вашего узла. Команда xcode-select и переменная DEVELOPER_DIR должны быть согласованы с тем Xcode, который ожидает CI. Apple описывает настройку инструментов командной строки в официальной документации Xcode.
Проверьте также:
- права выполнения под фактическим CI-аккаунтом;
- свободное место и доступность рабочего каталога;
- остатки предыдущей сборки;
- разницу между графической и SSH-сессией;
- поведение после перезапуска;
- наличие нескольких Xcode и неявное переключение инструментария.
Для удалённой работы важно различать интерактивную и автоматическую сессию. SSH-подключение, окружение оболочки и доступ к GUI-ресурсам могут отличаться; рекомендации Apple по автоматизации тестов и SSH приведены в документе о тестовых сессиях.
Условия принятия решения
Используйте следующую развилку, а не правило «ошибка есть — узел заменить»:
- Если исходная команда и минимальный запуск падают на одном и том же первом действии, выбирайте исправление проекта или CI-конфигурации.
- Если проблема связана только с одним рабочим каталогом, а чистая копия проходит, очистите или изолируйте этот каталог, предварительно сохранив
xcresultи лог. - Если разные проекты на узле сталкиваются с разными отказами, а
DEVELOPER_DIR, права или доступ к связке меняются между сессиями, изолируйте инструменты и аккаунты. - Если ошибка исчезает после перезапуска, но возвращается при следующем запуске, не объявляйте восстановление завершённым: ищите зависимость от состояния узла.
- Если чистая рабочая копия, исходная команда и повтор после перезапуска дают одинаково стабильный результат, пересоздание узла не оправдано.
- Если только новый чистый узел воспроизводимо исправляет сбой, переносите проект и фиксируйте различия среды, а старый узел не удаляйте до завершения проверки.
Для команд, которым нужен управляемый macOS-узел с полными правами и возможностью повторить один и тот же запуск, можно рассмотреть удалённый Mac для разработки и CI. Такой вариант полезен именно как контролируемая среда для проверки гипотезы, а не как замена расследованию.
Матрица повторной проверки: когда считать Exit Code 65 устранённым
Соберите один набор доказательств до изменения и один после него:
| Проверка | До исправления | После исправления | Критерий |
|---|---|---|---|
| Исходная команда | Полный лог и xcresult |
Повторный полный лог и xcresult |
Первый отказ исчез |
| Минимальная команда | Та же Scheme и цель | Те же параметры | Результат стабилен |
| Рабочая копия | Состояние каталога зафиксировано | Чистая копия проверена | Нет скрытого остатка |
| Учётная запись | Пользователь CI и окружение | Те же права | Нет зависимости от GUI |
| Узел после перезапуска | Состояние до перезапуска | Повтор после перезапуска | Сбой не возвращается |
Если нужен временный узел для воспроизведения, сравните не только тариф, но и возможность сохранить одинаковую среду между запусками. Параметры аренды и доступные варианты следует проверять на странице планов VMSPIN, а не переносить характеристики из чужих конфигураций.
Linux-сервер или локальная Windows-машина могут оставаться удобными для большей части backend- и кроссплатформенной работы, но они не заменяют macOS там, где требуется реальный Xcode, Apple SDK, Simulator или подпись Apple-платформы. Виртуальная или неподдерживаемая среда добавляет отдельные риски совместимости, доступа к ключам и стабильности, поэтому для краткой проверки CI она не всегда лучше управляемого реального Mac.
Когда текущий узел постоянно теряет окружение, требует ручного входа в GUI, скрывает xcresult или не позволяет отделить проектную ошибку от состояния машины, вы тратите время не на сборку, а на восстановление инфраструктуры. В такой ситуации аренда Mac у VMSPIN может быть разумнее покупки отдельного Mac mini для временного проекта: вы получаете удалённый доступ, можете повторить тот же репозиторий и сначала проверить воспроизводимость, не превращая неисправный CI в долгосрочную аппаратную закупку. Для перехода к конкретной среде используйте варианты заказа удалённого Mac только после того, как определите нужные Scheme, Destination и требования к подписи.