L'équipe de test nous a envoyé la semaine dernière un log de crash TestFlight — la stack trace n'affichait que des adresses hexadécimales du type 0x1042a3f88, et le rapport concernait un build datant de trois versions en arrière. En fouillant dans ~/Library/Developer/Xcode/Archives sur le Mac local, cette archive avait déjà été nettoyée automatiquement par Xcode. Ce genre de situation revient presque chaque trimestre dans l'équipe — les postes de développement locaux manquent d'espace disque, les runners CI sont détruits juste après le build, et les fichiers dSYM, générés une seule fois au moment du build et introuvables ensuite, exigent par nature un nœud d'archivage dédié. Un Mac mini cloud, loué à la semaine ou au mois, avec un disque persistant jamais purgé, vient combler exactement ce vide.
Pourquoi la symbolisation mérite son propre pipeline
Les adresses d'un log de crash ne peuvent être retraduites en noms de fonctions et numéros de ligne qu'à l'aide du dSYM issu exactement du même build, et le lien entre le dSYM et le binaire est verrouillé strictement par un UUID. Utiliser par erreur le dSYM d'une autre version pour symboliser ne provoque aucune erreur — seulement des numéros de ligne faux. C'est le piège le plus facile à négliger, et le plus difficile à détecter après coup.
Les runners des plateformes CI sont généralement des instances jetables, détruites après le build, qui ne conservent aucun dSYM historique ; les postes de développement locaux travaillent chacun de leur côté, et aucune machine individuelle ne réunit jamais toutes les versions de toute l'équipe. La solution est on ne peut plus simple : disposer d'une machine dont le disque n'est jamais purgé et accessible en SSH à tout moment pour exécuter des scripts, servant exclusivement de nœud d'archivage, où chaque dSYM produit par un archivage est stocké classé par numéro de build.
Préparer l'environnement : faire atterrir le dSYM avec les artefacts de build
Commencez par vérifier que le format des informations de débogage du projet Xcode est réglé sur dwarf-with-dsym (c'est la valeur par défaut en configuration Release) — c'est seulement dans ce cas qu'un package .dSYM distinct est produit lors de l'archivage :
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp-$(git rev-parse --short HEAD).xcarchive \
DEBUG_INFORMATION_FORMAT=dwarf-with-dsym
Une fois l'archivage terminé, le dSYM se trouve en réalité caché dans le répertoire .xcarchive/dSYMs/ — ne vous contentez pas de copier le .ipa. C'est le premier piège dans lequel tombent de nombreuses équipes : oublier cette étape lors de l'export du package d'installation, ce qui, après coup, ne peut être réparé qu'en relançant un build sur le même commit.
Conception du répertoire d'archivage et vérification d'UUID
Sur le Mac mini cloud, créez des répertoires classés par numéro de build, et effectuez immédiatement après chaque upload une vérification d'UUID pour garantir que le fichier stocké est bien le bon :
dwarfdump --uuid MyApp.app.dSYM/Contents/Resources/DWARF/MyApp
dwarfdump --uuid MyApp.app/MyApp
Les UUID renvoyés par les deux commandes doivent correspondre caractère par caractère. Il est conseillé de tenir l'historique d'archivage sous forme d'un tableau simple, pour faciliter la traçabilité :
| build | git commit | dSYM UUID(arm64) | Heure d'upload |
|---|---|---|---|
| 214 | a91f3c2 | 6B2E1A4F-... | 07-18 09:12 |
| 215 | c0d7e91 | 9F1D8C33-... | 07-25 14:03 |
| 216 | e3a4f10 | 2C77B0A9-... | 08-01 10:47 |
La structure des répertoires est fixée à /archive/dsym/{build}/, chaque niveau ne contenant qu'un seul dSYM accompagné d'un fichier meta.json consignant le commit et l'UUID. Le script de recherche localise directement l'entrée via le numéro de build, sans avoir à faire de grep manuel à chaque fois.
Script de symbolisation : du log de crash à une stack lisible
Une fois le log de crash récupéré, utilisez atos pour retraduire les adresses une par une — c'est plus rapide pour repérer une frame suspecte que de parcourir un rapport symbolicatecrash complet :
atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
-arch arm64 \
-l 0x1000 \
0x1042a3f88 0x1042a4210
Après -l figure l'adresse de base de chargement du binaire, que l'on peut copier directement depuis la section Binary Images du log de crash ; la liste d'adresses peut être passée en lot, et la sortie fournit alors le nom de la fonction ainsi que le fichier source et le numéro de ligne. Pour traiter un log entier, un petit script shell de quelques lignes suffit pour extraire les adresses ligne par ligne, construire la commande et réinjecter le résultat — nul besoin d'un outil graphique.
Pièges courants et liste de vérification
- Confusion d'architecture : une application tournant sur une puce M dispose d'une seule architecture,
arm64, mais le paramètre-arch x86_64, encore fréquent dans les anciens scripts, fait queatosrenvoie silencieusement l'adresse brute sans effectuer aucune conversion. - Adresse de base de chargement mal calculée : utiliser une valeur par défaut comme 0x100000000 sans la confronter à l'adresse de base réelle indiquée dans la section Binary Images du log est la deuxième cause la plus fréquente de numéros de ligne erronés.
- Droits trop permissifs sur le nœud d'archivage : les dSYM constituent des informations de débogage au niveau du code source ; il est recommandé de ne donner qu'un accès en lecture au répertoire d'archivage au responsable des releases et au compte de service CI, afin d'éviter toute suppression ou écrasement accidentel lorsque la machine est partagée avec d'autres équipes.
Notre expérience interne : avant de disposer d'un nœud d'archivage, retrouver le dSYM correspondant à un log de crash vieux de trois mois demandait en moyenne une demi-journée à deux personnes. Avec un workflow d'archivage établi, ce type de demande se résout désormais en général en moins de cinq minutes.
Intégrer le pipeline de symbolisation dans les releases au quotidien
Ajoutez à la fin de la lane de release fastlane une étape qui synchronise immédiatement le dSYM venant d'être généré vers le nœud d'archivage, plutôt que de le chercher dans l'urgence une fois le problème survenu :
lane :release do
build_app(scheme: "MyApp")
sh("scp -r ../build/MyApp.app.dSYM dev@archive-node:/archive/dsym/#{ENV['BUILD_NUMBER']}/")
end
Cette étape suppose que le nœud d'archivage soit disponible en permanence et dispose d'assez d'espace disque — ce qu'une instance CI temporaire ne peut pas offrir, alors qu'un Mac mini cloud en fonctionnement continu remplit exactement ce rôle. Louer au mois une machine dédiée qui ne fait que tourner les scripts d'archivage et de symbolisation revient bien moins cher que d'acheter du matériel uniquement pour cet usage, et cela n'occupe pas non plus les ressources de la machine de build utilisée pour le packaging.
Questions fréquentes
Pourquoi la symbolisation échoue souvent en local mais réussit sur un nœud d'archivage dédié ?
Les dSYM locaux disparaissent lors des nettoyages Xcode ou des réinstallations système, et aucun poste de l'équipe ne conserve tous les builds historiques. Un nœud d'archivage garde chaque dSYM classé par numéro de build, donc tout ancien rapport de crash trouve son fichier symbole à l'UUID correspondant.
Comment vérifier que le binaire d'un crash log correspond bien au dSYM que j'utilise ?
Exécuter dwarfdump --uuid sur le dSYM et le binaire .app : les UUID doivent correspondre caractère pour caractère. Une divergence ne génère aucune erreur, seulement des numéros de ligne faux — c'est le piège le plus facile à manquer.
Quelle différence entre le dSYM auto-généré par App Store Connect et celui exporté depuis sa propre CI ?
Le contenu est identique, mais le téléchargement depuis App Store Connect est retardé et le nombre de builds conservés limité. Un pipeline maison enregistre le dSYM dès l'archivage et le conserve indéfiniment, utile pour retracer un crash d'une version datant de six mois.
Besoin d'un Mac mini dédié pour faire tourner vos builds ?
Louer un Mac mini sur le portail