Fiche technique

Initialiser Xcode pour la CI sur Mac cloud

Initialiser Xcode pour la CI sur Mac cloud

Un Mac cloud peut déjà être accessible en SSH et renvoyer correctement la version avec xcodebuild -version, tout en bloquant dès la première tâche non supervisée sur l’acceptation de la licence, l’installation de composants supplémentaires ou la sélection du chemin des outils. Le problème est d’autant plus difficile à repérer qu’il apparaît souvent uniquement après l’ajout d’un nouveau nœud, une mise à niveau de Xcode ou la reconstruction d’un runner. Ouvrir une fois l’interface graphique peut alors masquer temporairement le défaut. Une approche plus fiable consiste à traiter l’initialisation de Xcode comme une étape autonome de mise en service du nœud, plutôt que de laisser les builds de production corriger l’environnement en cours d’exécution.

Séparer l’initialisation du nœud des tâches de build

L’initialisation du nœud peut modifier l’état du système, nécessite des droits d’administration et ne doit être exécutée qu’une seule fois pour chaque version de Xcode. Les tâches de build doivent, à l’inverse, être reproductibles, non interactives et éviter autant que possible les commandes avec élévation de privilèges. Mélanger ces deux phases dans un même script CI entraîne trois problèmes : plusieurs tâches peuvent installer des composants simultanément, une commande sudo non supervisée peut rester en attente d’une saisie et, en cas d’échec, il devient difficile de déterminer si le problème vient du code ou d’un environnement incomplètement préparé.

Il est recommandé de diviser le cycle de vie du runner en trois étapes :

  1. Installer ou sélectionner Xcode, puis accepter la licence et initialiser les composants requis au premier lancement.
  2. Exécuter un contrôle préalable en lecture seule et consigner le répertoire développeur ainsi que les versions d’outils réellement utilisés.
  3. N’autoriser le runner à recevoir des tâches qu’après la réussite de ce contrôle.

« Pouvoir ouvrir Xcode » ne constitue pas un critère de validation. Le véritable critère est que l’utilisateur du runner puisse, depuis un shell non interactif, localiser les outils de compilation, interroger les SDK et analyser un projet minimal.

Fixer le chemin de la version de Xcode réellement utilisée

Lorsque plusieurs versions de Xcode sont installées sur une machine, il ne faut pas dépendre uniquement du réglage global de xcode-select. Celui-ci affecte les autres tâches exécutées sur le même nœud physique et peut, après un changement de version, donner l’impression que les journaux indiquent une version alors que les commandes en utilisent une autre. Pour la CI, il est préférable de fixer DEVELOPER_DIR au niveau de chaque tâche.

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

printf 'developer_dir=%s
' "$DEVELOPER_DIR"
xcodebuild -version
xcrun --find clang
xcrun --find simctl
xcodebuild -showsdks

Si le nom du répertoire de l’application contient un numéro de version, enregistrez ce chemin dans la configuration du runner ou dans un fichier d’environnement suivi par le contrôle de version, au lieu de le dupliquer dans plusieurs scripts. Lors d’une mise à jour de Xcode, un seul point d’entrée doit être modifié, et le contrôle préalable doit afficher le chemin utilisé. Les journaux permettent ainsi de vérifier d’abord l’identité de la chaîne d’outils avant d’analyser le projet lui-même.

Ne pas remplacer les vérifications du shell par l’état de l’interface graphique

Les variables d’environnement, le shell par défaut et le contexte d’autorisation d’une session de bureau à distance peuvent différer de ceux du service runner. Toutes les vérifications doivent être effectuées avec le même utilisateur que celui qui exécute les builds. Il faut notamment confirmer que les résultats de xcrun --find se trouvent sous le DEVELOPER_DIR attendu, et ne pas se contenter de vérifier l’existence du répertoire de l’application.

Exécuter une seule fois les étapes de premier lancement

Pendant la préparation du nœud, vérifiez d’abord l’état du premier lancement, puis utilisez un processus d’initialisation disposant des droits d’administration pour accepter la licence et préparer les composants :

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

if ! xcodebuild -checkFirstLaunchStatus; then
  sudo xcodebuild -license accept
  sudo xcodebuild -runFirstLaunch
fi

xcodebuild -checkFirstLaunchStatus

Le point essentiel n’est pas la commande elle-même, mais l’endroit où elle est exécutée. Elle ne doit pas figurer dans le script de build déclenché à chaque commit. runFirstLaunch peut modifier des composants partagés ; si deux tâches concurrentes l’exécutent simultanément, elles perdent du temps et l’une d’elles risque de lire un état encore incomplet.

L’acceptation de la licence ne doit pas non plus dépendre d’un clic dans une boîte de dialogue. L’environnement automatisé doit exécuter explicitement la commande, vérifier son code de sortie et empêcher le nœud d’accepter des tâches en cas d’échec. Si une commande d’administration exige une saisie interactive, celle-ci doit être traitée pendant la mise en service du nœud, et non laissée à un runner d’arrière-plan.

Protéger l’initialisation concurrente avec un verrou atomique

Lors d’une mise à l’échelle automatique ou du démarrage simultané de plusieurs services, un script conçu pour ne s’exécuter qu’une seule fois peut tout de même être appelé en parallèle. Les outils fournis par défaut avec macOS permettent de créer un verrou atomique au moyen d’un répertoire :

set -euo pipefail

lock_dir="/tmp/xcode-bootstrap.lock"

if ! mkdir "$lock_dir" 2>/dev/null; then
  echo "Xcode bootstrap is already running" >&2
  exit 75
fi

cleanup() {
  rmdir "$lock_dir" 2>/dev/null || true
}
trap cleanup EXIT

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
xcodebuild -checkFirstLaunchStatus

Sur un même système de fichiers, mkdir est atomique : un seul processus peut réussir. Le code de sortie 75 peut être interprété par la couche d’orchestration comme un échec temporaire à réessayer ultérieurement, plutôt que comme une panne de build permanente du nœud.

Il faut également gérer les anciens verrous laissés par une interruption anormale. Ils peuvent être supprimés au démarrage du nœud après vérification qu’aucun processus d’initialisation n’est encore actif, ou être placés dans un emplacement vidé après un redémarrage. Ne supprimez jamais un verrou uniquement parce qu’il existe : vous pourriez interrompre une initialisation toujours en cours.

Mettre en place un contrôle préalable avant d’accepter des tâches

Une fois l’état du premier lancement validé, effectuez un contrôle de réception qui ne modifie pas le système. Il doit au minimum vérifier l’identité de Xcode, l’interrogation des SDK, les outils du simulateur et le point d’entrée du projet :

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

xcodebuild -checkFirstLaunchStatus
xcodebuild -version
xcodebuild -showsdks >/tmp/xcode-sdks.txt
xcrun simctl list runtimes
xcodebuild -list -workspace "Example.xcworkspace"

La dernière ligne doit être remplacée par le workspace ou le projet réellement présent dans le dépôt. Cette vérification est plus rapide que l’exécution immédiate d’une archive complète, tout en permettant de détecter à l’avance un point d’entrée absent, des prérequis non satisfaits avant la résolution des dépendances ou un répertoire de travail incorrect pour le runner.

Quelles informations conserver en cas d’échec

Les journaux de diagnostic doivent au minimum conserver DEVELOPER_DIR, xcodebuild -version, xcrun --find clang, xcrun simctl list runtimes et le code de sortie de la commande ayant échoué. N’y affichez pas l’ensemble des variables d’environnement, car elles peuvent contenir des jetons, des mots de passe de signature ou des identifiants d’accès au dépôt.

L’ordre de diagnostic habituel consiste à vérifier d’abord le chemin, puis l’état du premier lancement, ensuite les SDK et runtimes requis, et seulement enfin la configuration du projet. Après un changement de version de Xcode, cette procédure doit être réexécutée pour le nouveau répertoire Developer, même si l’ancienne version avait déjà été initialisée. Une fois l’opération terminée, consignez ensemble la version du script d’initialisation et celle de Xcode. Le runner pourra ainsi être reconstruit en suivant exactement les mêmes étapes, sans dépendre d’une interface qu’une personne aurait ouverte manuellement auparavant.

Questions fréquentes

Pourquoi Xcode s’ouvre-t-il en session graphique alors que la CI signale un premier lancement incomplet ?

La session graphique et le runner peuvent résoudre des installations Xcode différentes. Exécutez la vérification avec le DEVELOPER_DIR réellement utilisé par la CI.

Faut-il lancer sudo xcodebuild -runFirstLaunch dans chaque tâche CI ?

Non. Exécutez-le une fois pendant la préparation du nœud ou après un changement de version. Les builds ordinaires doivent rester sur des contrôles en lecture seule.

Une nouvelle initialisation est-elle nécessaire après avoir changé de version Xcode ?

Oui. Contrôlez séparément le nouveau répertoire Developer, car la licence, les composants et les runtimes de simulateur peuvent différer.

MacVPSGo Mac dans le cloud

Besoin d’une machine de build Apple Silicon dédiée ?

Louez un nœud physique dédié à la journée, à la semaine, au mois ou au trimestre pour vos builds Xcode, votre CI iOS, votre développement à distance et vos tâches d’automatisation.

Choisir une configuration et commander