В отчёте CI число 65 может быть последней строкой, хотя первый полезный сбой произошёл намного раньше: при выборе Scheme, разрешении зависимостей, запуске Simulator или подписи. Документация Apple отдельно описывает запуск тестов и интерпретацию результатов, но сам exit code 65 не является диагнозом конкретной причины (официальное руководство по результатам тестирования). Поэтому не следует начинать с одной «магической» команды очистки: сначала сохраните полный журнал и пакет xcresult, затем определите стадию отказа.
Эта статья предназначена для независимых разработчиков, у которых проект собирается в графическом интерфейсе, но падает через SSH или CI. Она также полезна небольшим командам, запускающим тесты на Simulator, и специалистам, которые выполняют Archive, подпись и публикацию без постоянного интерактивного сеанса.
[ SECTION_01 ] Сначала определите действие, которое завершилось сбоем
xcodebuild выполняет разные задачи. Один и тот же код завершения может появиться после совершенно разных операций. В журнале нужно искать не последнюю строку с 65, а первый содержательный маркер ошибки.
| Действие | Что проверять в первую очередь | Какой артефакт сохранить |
|---|---|---|
| Build | project или workspace, Scheme, Configuration, SDK, компиляцию и линковку | полный стандартный вывод и журнал сборки |
| Test | Destination, доступность Simulator, запуск приложения и тестовый процесс | xcresult и сообщения о запуске тестов |
| Archive | успешное создание архива, подписываемый target и окружение ключей | архив, журнал Archive и xcresult, если он создаётся |
| exportArchive | профиль, сертификат, Team ID, параметры экспорта | журнал экспорта и итоговый пакет |
Перед повторным запуском зафиксируйте команду, рабочий каталог, значение DEVELOPER_DIR, выбранную конфигурацию и целевую платформу. Имена проекта, Scheme, пути, Bundle ID, Team ID, имя сертификата и идентификатор устройства в копии журнала следует заменить на нейтральные значения.
Для тестового действия результат особенно важно сохранять в формате xcresult. Он содержит структуру тестового запуска, сообщения и вложения, которые часто исчезают в кратком выводе CI. Apple описывает разбор результатов и работу с тестовыми вложениями в руководстве по запуску тестов и интерпретации результатов (документация Apple о тестовых результатах).
Важно: строка
** TEST FAILED **илиexit code 65сообщает о неуспешном завершении действия, но не отвечает на вопрос, где именно произошёл сбой. Причину нужно искать выше — по первому коду ошибки, отсутствующему файлу, недоступной цели или отказу подписи.
Как сохранить полный журнал в CI
Сначала включите строгий режим завершения команды, чтобы оболочка не скрыла ошибку предыдущего этапа. Затем сохраните вывод в отдельный файл и передайте путь к результату тестирования. Конкретные значения Scheme, пути и идентификаторы должны быть параметрами CI, а не зашитыми в скрипт.
Пример для сборки:
set -o pipefail
xcodebuild \
-workspace "Project.xcworkspace" \
-scheme "AppScheme" \
-configuration "Release" \
-sdk iphoneos \
clean build \
2>&1 | tee "build.log"
status=${PIPESTATUS[0]}
printf 'xcodebuild status: %s\n' "$status"
exit "$status"
Пример для тестов с результатом:
set -o pipefail
xcodebuild \
-workspace "Project.xcworkspace" \
-scheme "AppScheme" \
-destination 'platform=iOS Simulator,id=REDACTED' \
test \
-resultBundlePath "TestResults.xcresult" \
2>&1 | tee "test.log"
status=${PIPESTATUS[0]}
exit "$status"
Если CI не сохраняет файлы после падения, диагностический материал будет потерян именно в момент, когда он нужен. В настройках задания должны архивироваться build.log, test.log, xcresult, сведения о коммите и безопасная копия использованных параметров. Секреты, токены и содержимое профилей в артефакты добавлять нельзя.
[ SECTION_02 ] Scheme и вход проекта часто ломают удалённую сборку
Почему Xcode собирает проект, а xcodebuild возвращает exit code 65
Графический Xcode может использовать выбранную пользователем Scheme, локальный рабочий каталог, сохранённые параметры и уже открытый workspace. CI запускает только то, что явно указано в команде. Если в автоматизации передан .xcodeproj, хотя зависимости подключены через workspace, система может не увидеть нужные targets или package-интеграцию.
Проверка должна начинаться с входного файла:
xcodebuild -list -project "Project.xcodeproj"
xcodebuild -list -workspace "Project.xcworkspace"
Затем сравниваются списки Scheme и конфигураций. Scheme, используемая в CI, должна существовать в репозитории и быть доступной для автоматического запуска. Для командной работы её обычно требуется сделать общей и проверить соответствующий файл в каталоге проекта. Apple объясняет, как настраивать Scheme, её действия и параметры (официальная документация по Scheme).
Важна и Configuration. Локальный запуск может использовать Debug, а удалённое задание — Release. Различия в условной компиляции, путях к ресурсам и настройках подписи тогда выглядят как случайная ошибка xcodebuild. Если параметры вынесены в файл конфигурации, проверьте, что CI действительно передаёт тот же файл и не подменяет переменные окружения. Формат и подключение Build Configuration описаны в документации Apple (настройка файла конфигурации сборки).
Один из самых полезных экспериментов — запустить одну и ту же обезличенную команду в интерактивном терминале и в автоматическом сеансе. Если команда успешна только при открытом Xcode, проблема находится не в исходном коде как таковом, а в окружении, правах, рабочем каталоге или доступности пользовательских данных.
[ SECTION_03 ] Зависимости, скрипты и компоновка требуют отдельной ветки диагностики
Код 65 может появиться после ошибки разрешения пакета, компилятора, обработки ресурсов, линкера или Run Script. Эти этапы нельзя объединять в одну категорию «сломался Xcode».
| Признак в журнале | Вероятный слой | Проверка без разрушительной очистки |
|---|---|---|
| пакет не разрешается или репозиторий недоступен | зависимости | lock-файл, URL, доступ к приватному репозиторию и состояние кэша |
| ошибка Swift или Objective-C | компиляция | исходный файл, активная Configuration, SDK и флаги компилятора |
| отсутствует ресурс или команда обработки завершилась ошибкой | ресурсы и скрипты | рабочий каталог, права, интерпретатор и входные файлы |
| символ не найден или библиотека не подключена | линковка | target membership, архитектура и список linked frameworks |
| Run Script завершился ненулевым статусом | пользовательский сценарий | переменные окружения, абсолютные пути и доступность утилит |
Сначала сравните lock-файлы и коммит. Затем проверьте, под каким пользователем работает CI, какой PATH он получает и существует ли нужный интерпретатор. Скрипт, который успешно выполняется в оболочке разработчика, может не найти утилиту в фоне. Отдельно проверяется аутентификация приватных зависимостей: SSH-ключ или токен должен быть доступен процессу сборки, но не выводиться в журнал.
Полная очистка Derived Data иногда скрывает симптом, но не исправляет неверный путь, устаревший lock-файл или сломанный скрипт. Более надёжная проверка — чистый checkout того же коммита в отдельном каталоге. Только после этого можно сравнивать результат с изменённым рабочим деревом. Если ошибка исчезает лишь после ручных локальных изменений, воспроизводимость проекта ещё не доказана.
[ SECTION_04 ] Симулятор и тестовый запуск нужно отделять от сборки
Ошибка может возникнуть до создания тестового продукта, при выборе Destination, во время запуска приложения или после истечения времени ожидания. Команда test объединяет несколько этапов, поэтому полезно разнести их:
xcodebuild \
-workspace "Project.xcworkspace" \
-scheme "AppScheme" \
-destination 'platform=iOS Simulator,id=REDACTED' \
build-for-testing \
-resultBundlePath "BuildForTesting.xcresult"
После успешной сборки тестовый продукт запускается отдельно:
xcodebuild \
-workspace "Project.xcworkspace" \
-scheme "AppScheme" \
-destination 'platform=iOS Simulator,id=REDACTED' \
test-without-building \
-resultBundlePath "TestWithoutBuilding.xcresult"
Такой разрыв показывает, где появляется xcodebuild exit code 65: при подготовке продукта или уже при работе тестов. Если падает первая команда, проверяются компиляция, ресурсы и зависимости. Если первая проходит, а вторая нет, внимание переносится на Simulator, запуск приложения, тестовые данные и тайм-аут.
В удалённой среде проверьте, установлен ли нужный runtime и существует ли выбранный идентификатор устройства. Не следует передавать идентификатор, который был создан на другом хосте. Также нужно установить, доступна ли графическая сессия. Некоторые сценарии тестирования зависят от сервисов пользователя и ведут себя иначе в SSH-сеансе без активного входа.
Переменные тестового окружения должны быть видны в журнале в обезличенном виде: имя переменной можно сохранить, а секретное значение — заменить маской. Справочник Apple по переменным тестовой среды помогает отличить настройки теста от параметров самого приложения (справочник переменных окружения тестов).
Как понять, что проблема именно в Simulator
У проекта и удалённой среды должны совпадать платформа, доступный runtime и выбранная цель. Если Xcode показывает рабочее устройство, это ещё не доказывает, что тот же Destination доступен процессу CI. В качестве проверки полезно вывести список доступных целей через встроенную справку и команды диагностики текущей установки, не подставляя в журнал личные пути.
Не увеличивайте тайм-аут вслепую. Сначала определите, стартует ли приложение и появляется ли тестовый процесс. Бесконечное ожидание может маскировать отсутствие Simulator, зависший сервис или ошибку доступа к пользовательской сессии.
[ SECTION_05 ] Archive, подпись и exportArchive — разные состояния
Успешная компиляция не означает, что архив можно экспортировать. Для Archive нужно подтвердить создание архива. Для подписи — наличие идентификатора с закрытым ключом. Для экспорта — соответствие профиля, entitlements, Team ID и метода распространения.
Apple описывает создание подписанного кода для распространения на Mac и связанные этапы подписи в отдельном руководстве (документация о Distribution-подписи). Для профилей важно проверять не только имя файла, но и его содержимое: техническая записка TN3125 объясняет внутреннюю структуру Provisioning Profile и проверяемые параметры (Apple TN3125 о Provisioning Profile).
В Keychain должна находиться полноценная signing identity с закрытым ключом. Один сертификат без ключа не решает задачу. При работе через SSH или фоновый агент дополнительно проверяются разблокировка связки ключей и разрешение процесса использовать ключ. Перенос подписывающих идентификаторов между машинами должен выполняться контролируемо; Apple отдельно описывает синхронизацию командных сертификатов (обмен командными сертификатами).
Нельзя без предупреждения удалять сертификаты, сбрасывать Keychain или заменять профили. Перед изменением фиксируются текущая identity, профиль, target и способ отката. Если локальная подпись работает, а фоновая нет, первым делом сравниваются пользователь, сессия и права доступа, а не удаляются все ключи.
Для Archive и экспорта полезно записывать параметры в отдельный конфигурационный файл. Это уменьшает риск, что интерактивный Xcode и CI используют разные значения. При нестандартном Archive-процессе Apple рекомендует учитывать точки расширения и порядок обработки архива (документация о настройке Archive).
[ SECTION_06 ] Проверка среды должна завершаться повторяемым прогоном
После исправления нельзя ограничиваться повтором в старом рабочем каталоге. Нужна контрольная последовательность:
- [ ] Зафиксировать commit, входной файл, Scheme, Configuration, SDK и Destination.
- [ ] Сохранить исходный журнал,
xcresultи точную команду запуска. - [ ] Выполнить чистый checkout того же commit на удалённом Mac.
- [ ] Повторить только проблемное действие: Build, Test, Archive или exportArchive.
- [ ] Сравнить пользователя,
DEVELOPER_DIR,PATH, рабочий каталог и переменные CI. - [ ] Для Test отдельно выполнить
build-for-testing, затемtest-without-building. - [ ] Для публикационной цепочки отдельно подтвердить Archive, подпись и экспорт.
- [ ] Переподключиться к сеансу или повторить задание после перезапуска хоста.
- [ ] Проверить, что журналы сохранились и не содержат секретов.
- [ ] Записать условие отката: прежний профиль, старый runner или предыдущая конфигурация.
Эта проверка разделяет проектную и инфраструктурную причину. Если один и тот же commit стабильно падает на разных хостах одинаковым образом, исправлять нужно проект или его зависимости. Если ошибка появляется только у определённого пользователя, после разрыва SSH или на конкретной машине, вероятнее проблема удалённой среды.
| Наблюдение | Решение | Оценка уверенности |
|---|---|---|
| сбой повторяется на чистом checkout и разных хостах | анализировать код, Scheme, зависимости или скрипт | высокая |
| Build проходит, но Test не запускает Simulator | проверять Destination, runtime и графическую сессию | высокая |
| Archive создаётся, exportArchive падает | проверять профиль, identity и параметры экспорта | высокая |
| интерактивный запуск успешен, SSH-запуск падает | сравнивать пользователя, Keychain и переменные среды | высокая |
| ошибка исчезает после ручной очистки, но не воспроизводится на чистом checkout | не считать очистку исправлением; искать состояние кэша | средняя |
| сбой появляется только на одном хосте | изолировать или перестроить среду после сохранения доказательств | высокая |
Оценка здесь относится не к вероятности конкретной ошибки, а к силе диагностического вывода. Код 65 сам по себе не позволяет выбрать строку таблицы. Её выбирают по журналу и повторяемому эксперименту.
Если требуется сравнить рабочие способы организации узла, полезно заранее определить требования к удалённой среде для сборки iOS. Для команд, где тесты запускаются постоянно, отдельные критерии понадобятся при проектировании сервера автоматизированного тестирования iOS: доступность Simulator, сохранение результатов, права Keychain и восстановление после перезапуска важнее одного удачного запуска.
[ SECTION_07 ] Когда менять удалённый Mac, а не проект
Замена или изоляция хоста оправдана, если доказаны сразу несколько признаков: тот же commit собирается в другой среде, ошибка зависит от пользователя или сеанса, а восстановление локального состояния не имеет понятного и безопасного отката. До миграции следует экспортировать обезличенные логи, список параметров, сведения о Scheme и инструкции по подписи.
Постоянный узел особенно полезен, когда Archive выполняется ночью, тесты должны запускаться без личного ноутбука, а команда не хочет смешивать рабочие и подписывающие ключи. Но аренда Mac не заменяет исправление проекта. Если сбой вызван неверной Scheme или ошибкой компилятора, новый хост лишь воспроизведёт его в другом месте.
При выборе удалённого Mac проверьте: можно ли получить root-доступ, как подключаться по SSH и VNC, сохраняются ли артефакты, кто отвечает за восстановление хоста и допускает ли тариф постоянный CI-процесс. Условия конкретного узла следует сверять на странице подходящего Mac mini для удалённой разработки, а не переносить параметры одного окружения на другое без проверки.
Если текущий вариант — личный Mac разработчика, у него есть несколько реальных слабых мест: он недоступен во время поездки или сна, его SSH-сеанс может отличаться от графического пользователя, локальные кэши скрывают ошибки чистой сборки, а подпись смешивается с повседневной работой. Если используется общий CI-хост, добавляются конкуренция за Simulator, неясное состояние Keychain и сложное восстановление после обновления. В такой ситуации аренда Mac через NOVAKVM даёт более предсказуемый путь для контрольного прогона: отдельная удалённая машина позволяет выполнить тот же commit, сохранить артефакты и проверить, относится ли xcodebuild exit code 65 к проекту или к прежней среде. Это не лучший вариант для нагрузки, которой требуется собственное физическое устройство, или для постоянной работы на годы без изменения конфигурации, но для временного расследования и изолированного iOS-пайплайна он практичнее покупки отдельного компьютера.
После проектной диагностики стоит запустить один контрольный Build, Test или Archive на изолированном удалённом Mac. Если результат отличается, зафиксируйте различия среды и настройте постоянный узел по тем же правилам сохранения журналов и подписывающих данных. જાણવા-niň