Тестовая команда на прошлой неделе прислала краш-лог из TestFlight, где весь стек состоял из шестнадцатеричных адресов вроде 0x1042a3f88, а сам краш относился к сборке, вышедшей три версии назад. Вы перерыли ~/Library/Developer/Xcode/Archives на локальном Mac — тот архив давно был автоматически подчищен Xcode. Такое случается в командах едва ли не каждый квартал: на рабочих машинах разработчиков места мало, CI-раннеры одноразовые и уничтожаются после сборки, а dSYM — это файл, который создаётся один раз в момент сборки и потом уже никак не восстановить. Именно поэтому для него нужен отдельный узел архивации. Облачный Mac mini, арендованный по неделям или месяцам, с диском, который никто не чистит, идеально закрывает эту дыру.
Почему символизация заслуживает отдельного конвейера
Адреса в краш-логе превращаются в имена функций и номера строк только в паре с dSYM именно той сборки, которая их сгенерировала. Привязка dSYM к бинарнику жёстко зафиксирована через UUID: если подставить dSYM от другой версии, символизация не выдаст ошибку — она просто выдаст неверные номера строк. Это самая незаметная и самая труднообнаруживаемая постфактум ловушка.
Раннеры на CI-платформах обычно эфемерны — сборка прошла, инстанс уничтожен, никакой истории dSYM не остаётся. Локальные машины разработчиков живут своей жизнью, и ни на одной из них не соберётся полный комплект версий всей команды. Решение простое: нужна машина, диск которой никто не чистит и на которую всегда можно зайти по SSH и запустить скрипт, — выделенный узел архивации, где dSYM каждого archive складывается по номеру сборки.
Подготовка окружения: пусть dSYM появляется вместе со сборкой
Сначала убедитесь, что в Xcode-проекте формат отладочной информации выставлен как dwarf-with-dsym (для конфигурации Release это значение по умолчанию) — только тогда archive выдаст отдельный пакет .dSYM:
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp-$(git rev-parse --short HEAD).xcarchive \
DEBUG_INFORMATION_FORMAT=dwarf-with-dsym
После завершения archive dSYM фактически лежит в каталоге .xcarchive/dSYMs/ — не копируйте только .ipa. Именно на этом моменте многие команды спотыкаются впервые: при экспорте установочного пакета этот шаг пропускают, а исправить задним числом можно только повторным запуском сборки с тем же коммитом.
Структура архивного каталога и проверка UUID
На облачном Mac mini создавайте каталоги по номеру сборки и сразу после каждой загрузки проверяйте UUID, чтобы убедиться, что сохранён именно тот файл:
dwarfdump --uuid MyApp.app.dSYM/Contents/Resources/DWARF/MyApp
dwarfdump --uuid MyApp.app/MyApp
UUID в выводе обеих команд должен совпадать посимвольно. Журнал архивации удобно вести в виде простой таблицы для быстрого поиска:
| build | git commit | dSYM UUID (arm64) | время загрузки |
|---|---|---|---|
| 214 | a91f3c2 | 6B2E1A4F-... | 07-18 09:12 |
| 215 | c0d7e91 | 9F1D8C33-... | 07-25 14:03 |
| 216 | e3a4f10 | 2C77B0A9-... | 08-01 10:47 |
Структура каталогов фиксирована: /archive/dsym/{build}/, в каждом уровне лежит один dSYM и файл meta.json с коммитом и UUID. Поисковый скрипт находит нужную версию прямо по номеру сборки, без ручного grep по всему архиву.
Скрипт символизации: от краш-лога к читаемому стеку
Получив краш-лог, восстанавливайте адреса поштучно через atos — это быстрее, чем разбирать весь отчёт symbolicatecrash, если нужно проверить один подозрительный фрейм:
atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
-arch arm64 \
-l 0x1000 \
0x1042a3f88 0x1042a4210
После -l указывается базовый адрес загрузки бинарника — его можно взять прямо из секции Binary Images краш-лога. Список адресов можно передавать пачкой, на выходе получите имена функций и номера строк исходного файла. Для обработки целого лога хватает небольшого shell-скрипта: построчно вытащить адреса, собрать команду и записать результат обратно в файл — никакие GUI-инструменты не нужны.
Частые ошибки и чек-лист
- Путаница с архитектурой: приложения, работающие на чипах серии M, собраны только под
arm64, но унаследованный от старых скриптов параметр-arch x86_64заставляетatosмолча возвращать исходные адреса без какого-либо преобразования. - Неверный базовый адрес загрузки: подстановка значения по умолчанию вроде 0x100000000 без сверки с реальным базовым адресом из секции Binary Images — вторая по частоте причина неверных номеров строк.
- Слишком широкие права на узле архивации: dSYM — это отладочная информация уровня исходного кода, поэтому каталог архива стоит открывать на чтение только релиз-менеджеру и сервисному аккаунту CI, чтобы файлы не удалили или не перезаписали случайно, если машина используется совместно с другими командами.
Наш внутренний опыт таков: без узла архивации восстановление dSYM под трёхмесячный краш-лог отнимало у двух человек в среднем полдня. С отлаженным процессом архивации такие запросы обычно закрываются за пять минут.
Встраиваем конвейер символизации в обычный релизный процесс
Добавьте в конце release-lane fastlane шаг, который сразу синхронизирует только что созданный dSYM на узел архивации — вместо того чтобы искать его в панике, когда проблема уже случилась:
lane :release do
build_app(scheme: "MyApp")
sh("scp -r ../build/MyApp.app.dSYM dev@archive-node:/archive/dsym/#{ENV['BUILD_NUMBER']}/")
end
Этот шаг требует, чтобы узел архивации был постоянно доступен и располагал достаточным местом на диске — временный CI-инстанс с этим не справится. Постоянно работающий облачный Mac mini подходит для этой роли идеально: одна выделенная машина, арендованная помесячно специально под архивацию и символизацию, обходится куда дешевле покупки собственного железа под единственную эту задачу и при этом не занимает ресурсы сборочной машины.
Часто задаваемые вопросы
Почему символизация часто не работает локально, но получается на выделенном узле архивации?
Локальные dSYM пропадают при очистке Xcode или переустановке системы, а ни на одном компьютере команды нет всех исторических сборок. Узел архивации хранит dSYM каждой сборки по номеру бессрочно, поэтому любой старый краш-лог находит подходящий файл символов по UUID.
Как убедиться, что бинарник из краш-лога совпадает с имеющимся dSYM?
Сравнить UUID dSYM и бинарника .app командой dwarfdump --uuid — они должны совпадать символ в символ. При несовпадении ошибки не будет, просто номера строк окажутся неверными — это самая незаметная ловушка.
В чём разница между dSYM, автоматически созданным App Store Connect, и тем, что экспортирован из своего CI?
Содержимое одинаковое, но загрузка из App Store Connect происходит с задержкой и хранится ограниченное число сборок. Собственный конвейер сохраняет dSYM сразу при архивации и хранит его сколько угодно, что важно при разборе краша из релиза полугодовой давности.
Нужен выделенный Mac mini для сборки ваших проектов?
Арендовать Mac mini в портале