Réparer un échec de démarrage d’iOS Simulator sur un Mac cloud

Réparer un échec de démarrage d’iOS Simulator sur un Mac cloud

Dans une CI distante, il arrive qu’un échec soit particulièrement difficile à reproduire : Xcode a terminé la compilation, mais iOS Simulator reste bloqué sur Booting. Il arrive aussi que l’appareil apparaisse comme Shutdown alors que la commande d’installation signale toujours un échec de connexion au service. Redémarrer directement le Mac cloud rétablit souvent la situation de façon temporaire, mais efface les traces de l’incident sans expliquer pourquoi il se reproduit. Une méthode plus fiable consiste à examiner séparément le runtime, l’état de l’appareil et l’exécution concurrente des jobs.

Commencer par identifier la couche en cause

Les pannes du simulateur se situent généralement à l’un de ces trois niveaux : runtime Xcode indisponible, répertoire d’appareils endommagé ou accès simultané de plusieurs jobs au même appareil. Commencez par relever le chemin de Xcode et l’inventaire des appareils, sans lancer immédiatement erase all.

set -euo pipefail

xcode-select -p
xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices
xcrun simctl list devices unavailable

Si un runtime apparaît comme unavailable, vérifiez d’abord que la version de Xcode actuellement sélectionnée contient l’iOS Runtime requis par le projet. Si plusieurs versions de Xcode sont installées, définissez explicitement le répertoire des outils de développement, puis relancez les vérifications :

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcrun simctl list runtimes

Ne déduisez pas l’identifiant du runtime à partir du nom de l’application Xcode. Les scripts d’automatisation doivent lire les éléments réellement disponibles dans simctl list -j et refuser tout runtime dont la valeur est isAvailable=false.

Recueillir les preuves avant tout nettoyage destructif

Lorsqu’un appareil reste bloqué, conservez l’inventaire JSON et les journaux récents. Même si l’appareil est ensuite recréé, ces éléments permettent de distinguer une anomalie du service CoreSimulator, l’arrêt du processus de démarrage ou un plantage de l’application elle-même.

mkdir -p artifacts/simulator

xcrun simctl list -j > artifacts/simulator/list.json
xcrun simctl diagnose \
  > artifacts/simulator/diagnose.txt 2>&1 || true

xcrun simctl spawn booted log show \
  --last 5m \
  --style compact \
  --predicate 'process == "SpringBoard" OR process == "launchd_sim"' \
  > artifacts/simulator/boot.log 2>&1 || true

Si aucun appareil booted n’est disponible, l’échec de la dernière commande est normal. Le script doit néanmoins conserver les deux premiers résultats. Avant l’archivage, les journaux doivent également être expurgés afin d’éviter que des répertoires de travail, des paramètres contenant des jetons ou des chemins vers des éléments de signature ne soient enregistrés durablement dans les artefacts de CI.

erase, delete et la suppression directe des répertoires d’appareils sont des opérations destructives. Les exécuter avant d’avoir recueilli les preuves aboutit généralement au constat « cela fonctionne après une nouvelle tentative », et non à l’identification d’une cause racine qu’il est possible de corriger.

Créer un Device Set distinct pour chaque job

Le Device Set par défaut se trouve dans le répertoire de l’utilisateur et reste accessible à tous les jobs concurrents. Si un job exécute shutdown all, un autre peut perdre son appareil en plein test. La solution ne consiste pas à multiplier les tentatives, mais à attribuer un répertoire distinct à chaque job.

JOB_KEY="${CI_JOB_ID:-local}-$$"
DEVICE_SET="$PWD/.simulators/$JOB_KEY"
mkdir -p "$DEVICE_SET"

DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"
RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-18-0"

UDID="$(
  xcrun simctl --set "$DEVICE_SET" create \
    "ci-$JOB_KEY" "$DEVICE_TYPE" "$RUNTIME_ID"
)"

xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcrun simctl --set "$DEVICE_SET" bootstatus "$UDID" -b

Le type d’appareil et le runtime de cet exemple servent uniquement à illustrer le format des paramètres. Les valeurs réelles doivent être sélectionnées dans l’inventaire JSON obtenu à l’étape précédente. Si la création de l’appareil échoue, le script ne doit pas revenir silencieusement au Device Set par défaut, car l’isolation disparaîtrait précisément au moment où elle est la plus nécessaire.

Limiter le nettoyage à son propre répertoire

À la fin du job, arrêtez d’abord ses propres appareils, puis supprimez le répertoire correspondant. N’exécutez jamais de commande globale delete all sur un runner partagé.

xcrun simctl --set "$DEVICE_SET" shutdown all || true
rm -rf "$DEVICE_SET"

Étendre la validation du démarrage jusqu’à l’application

bootstatus -b indique uniquement que le système a terminé son démarrage. Il ne garantit pas que l’application à tester puisse être installée et exécutée. Un smoke test complet doit couvrir au moins quatre points :

Vérification Commande ou signal À conserver en cas d’échec
Démarrage de l’appareil bootstatus -b diagnostic et journaux de démarrage
Installation de l’app simctl install chemin de l’app, code de sortie
Lancement de l’app simctl launch bundle identifier, sortie du processus
Préparation des tests fichier d’état ou sonde de santé échéance et dernier état
APP_PATH="$PWD/build/Sample.app"
BUNDLE_ID="com.example.Sample"

xcrun simctl --set "$DEVICE_SET" install "$UDID" "$APP_PATH"
xcrun simctl --set "$DEVICE_SET" launch \
  --console-pty "$UDID" "$BUNDLE_ID"

Le pipeline doit définir un délai d’expiration explicite pour la phase de démarrage. En cas de dépassement, il faut d’abord recueillir les journaux, puis arrêter l’appareil. Un nombre fixe de nouvelles tentatives sans condition ne fait que prolonger l’attente lorsqu’il s’agit d’une erreur déterministe du runtime.

Définir un ordre de récupération progressif

Les opérations de récupération doivent commencer par l’étape ayant le moins d’impact :

  1. Vérifier à nouveau la compatibilité entre DEVELOPER_DIR, le runtime et le type d’appareil.
  2. Arrêter puis redémarrer l’appareil appartenant au job en cours.
  3. Supprimer l’appareil défaillant dans le même Device Set isolé, puis le recréer.
  4. Nettoyer les entrées confirmées comme invalides dans simctl list devices unavailable.
  5. N’envisager le redémarrage des services CoreSimulator ou de toute la machine que si aucun autre job n’est en cours.

S’il faut systématiquement atteindre la cinquième étape, vérifiez si le runner utilise encore le Device Set partagé par défaut, si le disque approche de sa capacité maximale et si des sous-processus du simulateur subsistent après l’annulation de jobs. Les tâches distantes sur RunnerVM doivent suivre les mêmes principes : après avoir vérifié dans la console les configurations actuellement disponibles, intégrez le chemin du Device Set au cycle de vie du job au lieu de considérer l’état du simulateur comme une ressource permanente de la machine.

Rendre les échecs comparables

Enfin, enregistrez dans un résultat structuré la version de Xcode, l’identifiant du runtime, le type d’appareil, l’UDID, la durée de démarrage et l’étape de l’échec. En regroupant les échecs successifs selon ces champs, il devient possible de déterminer s’ils se concentrent sur un runtime précis, une catégorie d’appareils ou certains jobs concurrents.

Une récupération fiable du simulateur ne s’achève pas lorsque l’interface finit par s’ouvrir. Elle est terminée lorsque les commandes peuvent être réexécutées de manière reproductible, que l’application peut être installée et lancée, que les preuves de l’incident ont été archivées et que l’état privé du job a été nettoyé. Si le problème réapparaît, le diagnostic peut alors reprendre à partir d’une étape connue au lieu de repartir de suppositions.

Questions fréquentes

Faut-il effacer immédiatement un simulateur bloqué sur Booting ?

Non. Enregistrez d’abord la liste des appareils et les journaux utiles, puis recréez un appareil équivalent dans un jeu isolé. N’utilisez erase que si l’ancien état de test est inutile.

Plusieurs tâches CI peuvent-elles partager le jeu d’appareils par défaut ?

Ce partage est déconseillé, car une tâche peut arrêter ou supprimer l’appareil d’une autre. Un répertoire de jeu d’appareils par tâche garantit une propriété claire.

Un bootstatus réussi suffit-il pour lancer les tests ?

Non. Le contrôle doit aussi installer l’application produite, lancer son identifiant de bundle et vérifier le statut de sortie de chaque commande.

Runner M4

Faites fonctionner votre prochaine compilation sur un Mac physique dédié

Choisissez un nœud Mac dans le cloud adapté à la durée de votre tâche, puis gérez vos commandes, informations de connexion et tickets d’assistance depuis la console.

Choisir une offre et commander