Fiabiliser la destination Xcode en CI sur Mac cloud

Fiabiliser la destination Xcode en CI sur Mac cloud

Après plusieurs semaines d’exécution continue sur un Mac cloud, une même commande xcodebuild test peut soudainement signaler que la destination n’est plus disponible ou sélectionner un simulateur homonyme associé à un runtime récemment installé. Le projet n’a pourtant pas changé : c’est la résolution du périphérique cible par Xcode qui a dérivé. Pour fiabiliser la CI, il ne faut pas y inscrire définitivement un UDID, mais distinguer clairement l’archivage, la validation de la compilation et les tests sur simulateur, puis résoudre et consigner la cible au début de chaque tâche.

Distinguer d’abord les trois types de cibles de build

Dans Xcode, la Destination indique la plateforme et le périphérique visés par l’action en cours. Elle ne se confond pas avec le Scheme. Un Scheme partagé peut servir à créer une archive iOS générique, à tester sur un simulateur précis ou à valider uniquement la compilation, mais ces trois opérations ne doivent pas reposer sur une même commande ambiguë.

TâcheCible recommandéeUDID précis requis
Création d’une Archivegeneric/platform=iOSNon
Tests sur simulateuriOS Simulator précisOui
Validation de la compilationCible générique ou précise selon la plateforme de l’artefactSelon le contenu des tests

Utiliser name=iPhone 16 pour une tâche d’archivage introduit inutilement une dépendance au runtime du simulateur. À l’inverse, si une tâche de test indique seulement platform=iOS Simulator, Xcode peut trouver plusieurs candidats. Son choix final dépend alors des runtimes installés et de l’ordre de création des périphériques.

Une cible générique sert à démontrer que le projet peut produire un artefact pour la plateforme concernée. Une cible précise sert à démontrer que les tests peuvent s’exécuter dans un environnement de périphérique déterminé. Une même Destination ne doit pas porter ces deux significations à la fois.

Vérifier les destinations disponibles avant la tâche

Commencez par fixer le chemin de Xcode, puis demandez au projet de déclarer lui-même les destinations disponibles. Cette méthode permet de distinguer un Scheme qui ne prend pas en charge la plateforme d’une machine qui ne possède aucun périphérique correspondant, sans supprimer immédiatement les caches.

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
WORKSPACE="App.xcworkspace"
SCHEME="App"

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -showdestinations

Cette sortie doit être conservée comme pièce jointe de la tâche. Si aucun iOS Simulator n’apparaît dans la liste, vérifiez d’abord les Supported Destinations du Scheme, la plateforme cible et le Target de test, au lieu d’exécuter plusieurs fois simctl erase all. Si le périphérique existe mais porte l’état unavailable, vérifiez que le runtime correspondant reste pris en charge par la version actuelle de Xcode.

Consigner également l’identité de la chaîne d’outils

Les problèmes de Destination coïncident souvent avec un changement de la version de Xcode sélectionnée par défaut. Chaque exécution doit au minimum consigner les informations suivantes :

xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices available

Ces sorties ne contiennent pas les secrets du projet et peuvent donc être archivées comme données de diagnostic du build. Lorsque plusieurs Mac cloud exécutent des tâches en parallèle, elles permettent aussi de déterminer si l’échec se limite à une chaîne d’outils particulière.

Résoudre dynamiquement le simulateur au lieu de figer son UDID

Un UDID appartient à une instance de périphérique sur l’hôte actuel. Il peut changer après la suppression et la recréation d’un simulateur, le remplacement d’un runtime ou la réinitialisation d’un nœud de travail. La CI doit rechercher le périphérique par son nom et son état de disponibilité, puis exiger un résultat unique.

Le script suivant sélectionne un iPhone 16 disponible dans les données JSON de simctl. Dans un pipeline réel, l’identifiant du runtime cible doit également être passé en paramètre afin d’éviter toute ambiguïté lorsque des périphériques homonymes existent sous deux versions du système.

SIM_NAME="${SIM_NAME:-iPhone 16}"

SIM_UDID="$(
  xcrun simctl list devices available -j |
  python3 -c '
import json, sys
name = sys.argv[1]
data = json.load(sys.stdin)
matches = []
for runtime, devices in data["devices"].items():
    for device in devices:
        if device.get("isAvailable") and device.get("name") == name:
            matches.append((runtime, device["udid"]))
if len(matches) != 1:
    for runtime, udid in matches:
        print(f"{runtime} {udid}", file=sys.stderr)
    raise SystemExit(f"expected one simulator named {name}, found {len(matches)}")
print(matches[0][1])
' "$SIM_NAME"
)"

printf 'Resolved simulator: %s (%s)\n' "$SIM_NAME" "$SIM_UDID"

Le point essentiel est d’échouer immédiatement lorsque plusieurs candidats correspondent. Prendre silencieusement le premier élément du tableau permet certes de poursuivre la tâche, mais transforme une ambiguïté de l’environnement en résultats de test difficiles à reproduire.

Séparer les commandes de test et d’archivage

Une fois l’UDID obtenu, démarrez le périphérique et attendez la fin de son initialisation avant de lancer les tests. bootstatus -b empêche ceux-ci de commencer alors que les services système ne sont pas encore prêts.

xcrun simctl boot "$SIM_UDID" 2>/dev/null || true
xcrun simctl bootstatus "$SIM_UDID" -b

xcodebuild test \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath "Artifacts/TestResults.xcresult"

La commande d’archivage, quant à elle, ne fait aucune référence au simulateur :

xcodebuild archive \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "Artifacts/App.xcarchive"

Cette séparation clarifie également les responsabilités : l’étape de test gère l’état du simulateur, tandis que l’étape d’archivage ne s’occupe que du projet, de la configuration de signature et du chemin de sortie. Même si les deux étapes s’exécutent sur le même Mac cloud SetMini, elles doivent conserver deux sections de journal distinctes.

Transformer la résolution en enregistrement vérifiable

La réussite de la commande ne suffit pas. Le pipeline doit écrire dans un fichier texte ordinaire ou dans un document JSON le Scheme, DEVELOPER_DIR, le nom du périphérique, son UDID, le runtime, le type d’action et le chemin du paquet de résultats. Lors d’un diagnostic, comparez d’abord ces éléments factuels avant d’examiner les modifications du code.

À la fin de la tâche, il est recommandé de n’arrêter que le périphérique démarré par celle-ci :

cleanup() {
  if [[ -n "${SIM_UDID:-}" ]]; then
    xcrun simctl shutdown "$SIM_UDID" 2>/dev/null || true
  fi
}
trap cleanup EXIT

Sur un nœud partagé, n’arrêtez pas tous les simulateurs sans condition et n’utilisez pas erase all comme procédure de nettoyage habituelle. La première pratique interrompt les tâches parallèles ; la seconde masque l’origine d’un état contaminé et rallonge les initialisations suivantes.

La vérification finale tient en cinq points : utiliser une cible générique pour l’archivage ; utiliser pour les tests l’UDID résolu dynamiquement pendant la tâche ; exiger un candidat unique ; consigner Xcode et le runtime avant l’exécution ; ne gérer à la sortie que le périphérique appartenant à la tâche. Avec ces règles, la Destination cesse d’être un état implicite de l’environnement et devient une entrée de build vérifiable et reproductible.

Questions fréquentes

Faut-il choisir un simulateur pour archiver une application iOS ?

Non. Utilisez generic/platform=iOS pour l’archivage afin de ne pas lier la tâche à un simulateur susceptible de changer après une mise à jour.

Peut-on conserver durablement l’UDID d’un simulateur dans la CI ?

Ce n’est pas recommandé. Résolvez l’UDID au début de chaque tâche selon le modèle et le runtime, puis consignez la valeur effectivement sélectionnée.

Que faire si plusieurs simulateurs portent le même nom ?

Filtrez les appareils disponibles et le runtime voulu. S’il reste plusieurs candidats, arrêtez la tâche et affichez la liste au lieu de choisir silencieusement le premier.

Nœud physique dédié

Intégrez vos étapes de développement dans un Mac cloud accessible à tout moment

Choisissez parmi deux configurations Apple Silicon et quatre centres de données. Vos ressources restent dédiées, sans partage avec d’autres clients. La disponibilité réelle est indiquée en temps réel dans la console.

Choisir une formule