1. Облачные серверы
  2. Блог
  3. Символизация краш-логов и конвейер архивации dSYM

Символизация краш-логов и конвейер архивации dSYM

DevOps и CI/CD ·~4 мин чтения

Символизация краш-логов и конвейер архивации dSYM

Тестовая команда на прошлой неделе прислала краш-лог из 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 в портале