Das Testteam hat uns letzte Woche ein TestFlight-Crash-Log vor die Tür geworfen — im Stack-Trace nichts als Hex-Adressen wie 0x1042a3f88, und gemeldet wurde das Ganze für einen Build, der schon drei Versionen zurückliegt. Ein Blick in ~/Library/Developer/Xcode/Archives auf dem lokalen Mac zeigt: Das damalige Archiv hat Xcode längst automatisch aufgeräumt. So etwas passiert im Team fast jedes Quartal einmal — lokale Entwicklerrechner haben zu wenig Speicherplatz, CI-Runner werden nach dem Build sofort wieder eingestampft, und dSYM-Dateien, die "einmalig beim Build entstehen und danach nirgendwo mehr auftauchen", brauchen von Natur aus einen dedizierten Archivknoten. Ein Cloud-Mac-mini, wöchentlich oder monatlich gemietet, mit dauerhaftem Speicher, der nie automatisch geleert wird, schließt genau diese Lücke.
Warum die Symbolisierung eine eigene Pipeline braucht
Adressen in einem Crash-Log lassen sich nur mit dem dSYM aus genau demselben Build wieder in Funktionsnamen und Zeilennummern zurückverwandeln, und die Bindung zwischen dSYM und Binary wird strikt über eine UUID festgelegt. Nimmt man versehentlich das dSYM einer anderen Version zur Symbolisierung, gibt es keinen Fehler — nur falsche Zeilennummern. Das ist die Falle, die am leichtesten übersehen wird und im Nachhinein am schwersten zu entdecken ist.
Runner auf CI-Plattformen sind in der Regel Wegwerf-Instanzen, die nach dem Build zerstört werden und keine historischen dSYMs aufheben; lokale Entwicklerrechner arbeiten jeder für sich, und kein einzelner Rechner hat je alle Versionen des ganzen Teams beisammen. Die Lösung ist denkbar simpel: eine Maschine, deren Speicher nie geleert wird und auf die man jederzeit per SSH zugreifen kann, um Skripte auszuführen — und die jedes dSYM, das bei einem Archive-Vorgang entsteht, nach Build-Nummer sortiert ablegt und ausschließlich als Archivknoten dient.
Umgebung vorbereiten: dSYM zusammen mit den Build-Artefakten ablegen
Als Erstes sicherstellen, dass im Xcode-Projekt das Debug Information Format auf dwarf-with-dsym steht (in der Release-Konfiguration ist das der Standard) — nur dann entsteht beim Archivieren ein eigenständiges .dSYM-Paket:
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp-$(git rev-parse --short HEAD).xcarchive \
DEBUG_INFORMATION_FORMAT=dwarf-with-dsym
Nach Abschluss des Archivierens liegt das dSYM tatsächlich im Verzeichnis .xcarchive/dSYMs/ versteckt — nicht nur die .ipa kopieren. Der erste Fehler, in den viele Teams laufen, ist genau das: Beim Export des Installationspakets wird dieser Schritt übersehen, und im Nachhinein lässt sich das nur durch einen erneuten Build mit demselben Commit reparieren.
Archivstruktur und UUID-Prüfung
Auf dem Cloud-Mac-mini nach Build-Nummer Verzeichnisse anlegen und nach jedem Upload sofort eine UUID-Prüfung durchführen, um sicherzustellen, dass die richtige Datei abgelegt wurde:
dwarfdump --uuid MyApp.app.dSYM/Contents/Resources/DWARF/MyApp
dwarfdump --uuid MyApp.app/MyApp
Die von beiden Befehlen ausgegebenen UUIDs müssen zeichengenau übereinstimmen. Es empfiehlt sich, die Archivhistorie als einfache Tabelle zu pflegen, um später leicht zurückverfolgen zu können:
| build | git commit | dSYM UUID(arm64) | Upload-Zeit |
|---|---|---|---|
| 214 | a91f3c2 | 6B2E1A4F-... | 07-18 09:12 |
| 215 | c0d7e91 | 9F1D8C33-... | 07-25 14:03 |
| 216 | e3a4f10 | 2C77B0A9-... | 08-01 10:47 |
Die Verzeichnisstruktur ist fest auf /archive/dsym/{build}/ festgelegt, jede Ebene enthält genau ein dSYM plus eine meta.json mit Commit und UUID. Das Suchskript findet den Eintrag direkt über die Build-Nummer, ohne dass man jedes Mal manuell grep bemühen muss.
Symbolisierungsskript: vom Crash-Log zum lesbaren Stack
Sobald das Crash-Log vorliegt, lässt sich mit atos Adresse für Adresse zurückübersetzen — das findet einen verdächtigen Frame schneller, als einen kompletten symbolicatecrash-Report zu durchforsten:
atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
-arch arm64 \
-l 0x1000 \
0x1042a3f88 0x1042a4210
Hinter -l steht die Lade-Basisadresse des Binarys, die sich direkt aus dem Abschnitt Binary Images im Crash-Log abschreiben lässt; die Adressliste kann man im Batch übergeben, die Ausgabe liefert dann Funktionsname plus Quelldatei und Zeilennummer. Für ein komplettes Log genügt ein paar Zeilen Shell-Skript, das zeilenweise Adressen extrahiert, den Befehl zusammensetzt und das Ergebnis zurückschreibt — ein grafisches Tool braucht es dafür nicht.
Häufige Fallen und Checkliste
- Architektur-Verwechslung: Eine App, die auf einem M-Chip läuft, hat nur eine einzige Architektur,
arm64— aber der in älteren Skripten oft noch vorhandene Parameter-arch x86_64führt dazu, dassatosstillschweigend die Rohadresse zurückgibt, ohne irgendetwas umzuwandeln. - Falsch berechnete Lade-Basisadresse: Einfach einen Standardwert wie 0x100000000 zu nehmen, ohne ihn gegen die tatsächliche Basisadresse im Abschnitt Binary Images im Log abzugleichen, ist die zweithäufigste Ursache für falsche Zeilennummern.
- Zu weit gefasste Rechte am Archivknoten: dSYMs sind quellcodenahe Debug-Informationen. Es empfiehlt sich, dem Release-Verantwortlichen und dem CI-Service-Account nur Lesezugriff auf das Archivverzeichnis zu geben, damit bei gemeinsamer Nutzung der Maschine durch mehrere Teams niemand versehentlich löscht oder überschreibt.
Unsere interne Erfahrung: Ohne Archivknoten hat es früher im Schnitt zwei Personen einen halben Tag gekostet, für ein drei Monate altes Crash-Log das passende dSYM mühsam zusammenzusuchen. Mit einem festen Archivierungs-Workflow liefert eine solche Anfrage heute praktisch innerhalb von fünf Minuten ein Ergebnis.
Die Symbolisierungs-Pipeline in den Release-Alltag einbinden
Am Ende der fastlane-Release-Lane einen zusätzlichen Schritt einbauen, der das gerade erzeugte dSYM sofort zum Archivknoten synchronisiert — statt erst im Problemfall hektisch danach zu suchen:
lane :release do
build_app(scheme: "MyApp")
sh("scp -r ../build/MyApp.app.dSYM dev@archive-node:/archive/dsym/#{ENV['BUILD_NUMBER']}/")
end
Dieser Schritt setzt voraus, dass der Archivknoten dauerhaft online ist und genug Speicherplatz hat — das kann eine temporäre CI-Instanz nicht leisten, ein dauerhaft laufender Cloud-Mac-mini übernimmt genau diese Rolle. Eine feste Maschine im Monatsabo, die ausschließlich für Archivierung und Symbolisierungsskripte läuft, ist deutlich günstiger, als dafür eigene Hardware anzuschaffen — und belastet auch nicht die Build-Maschine, die zum Paketieren gebraucht wird.
Häufig gestellte Fragen
Warum schlägt die Symbolisierung lokal oft fehl, funktioniert aber auf einem dedizierten Archivknoten?
Lokale dSYMs verschwinden bei Xcode-Cleanups oder Neuinstallationen, und kein einzelner Rechner im Team hat alle historischen Builds. Ein Archivknoten speichert jedes dSYM nach Build-Nummer dauerhaft, sodass jeder alte Crash-Report die passende UUID-Symboldatei findet.
Wie prüfe ich, ob die Binary im Crash-Log zum vorliegenden dSYM passt?
Mit dwarfdump --uuid die UUID von dSYM und .app-Binary vergleichen — sie müssen zeichengenau übereinstimmen. Bei einer Abweichung entstehen ohne Fehlermeldung falsche Zeilennummern, das ist die am leichtesten übersehene Falle.
Was unterscheidet das automatisch generierte dSYM von App Store Connect von einem selbst exportierten aus der eigenen CI?
Inhaltlich sind sie identisch, aber der Download bei App Store Connect ist verzögert und die Aufbewahrung begrenzt. Eine eigene Pipeline speichert das dSYM sofort beim Archivieren und hält es beliebig lange vor, was bei Crashes aus einem sechs Monate alten Release entscheidend ist.
Sie brauchen einen dedizierten Mac mini für Ihren Build-Workflow?
Jetzt bestellen