Notes d’ingénierie Oakmini

Figer Xcode pour des builds reproductibles sur un Mac cloud

DevOps et CI/CD ·~6 min de lecture

Figer Xcode pour des builds reproductibles sur un Mac cloud

Lorsqu’un même Mac cloud héberge à la fois une branche stable, la branche de développement courante et une branche de validation de mise à niveau, la variable la plus souvent négligée n’est pas le code, mais la version de Xcode réellement active. Si une tâche modifie la sélection globale, les pipelines suivants peuvent basculer silencieusement vers un autre compilateur Swift ou un autre SDK. Le problème se manifeste généralement par un build qui réussit en local mais échoue à distance, ou par des résultats différents produits à un jour d’intervalle pour un même commit.

Traiter la chaîne d’outils comme une dépendance du projet

Xcode ne doit pas être considéré comme une simple icône installée sur la machine. Sa version doit être consignée explicitement, au même titre qu’un environnement d’exécution. Conservez des répertoires versionnés afin qu’une nouvelle installation ne remplace pas directement la précédente :

/Applications/Xcode_16.1.app
/Applications/Xcode_16.2.app
/Applications/Xcode_16.3.app

Commencez par inventorier les installations présentes et la sélection globale active :

find /Applications -maxdepth 1 -name 'Xcode*.app' -print
xcode-select --print-path
xcodebuild -version
xcrun --find swift
swift --version

La documentation doit au minimum indiquer la version de Xcode, son numéro de build, la version de Swift et le SDK utilisé par le projet. La mention « utiliser la dernière version » ne permet ni de reproduire un ancien build, ni de déterminer si un échec vient du code ou d’une évolution de la chaîne d’outils.

Figer une version ne revient pas à refuser les mises à niveau, mais à en faire des changements techniques observables et réversibles.

Sélectionner Xcode selon la portée de la tâche

La méthode de sélection doit dépendre de la portée concernée. Une valeur globale par défaut convient à l’administration manuelle, tandis qu’une variable d’environnement définie au niveau de chaque tâche est préférable pour l’automatisation.

Méthode Portée Usage recommandé Risque en cas d’exécution parallèle
xcode-select Machine entière Administration par une seule personne, version par défaut commune Affecte les autres tâches
DEVELOPER_DIR Processus courant et processus enfants CI, scripts, projets parallèles Faible
Affectation temporaire avant une commande Une seule commande Validation rapide Faible

Pour modifier la version globale par défaut, exécutez :

sudo xcode-select --switch /Applications/Xcode_16.2.app/Contents/Developer

Évitez d’exécuter cette commande de manière répétée dans la CI. Définissez plutôt la variable suivante au début de la tâche :

export DEVELOPER_DIR="/Applications/Xcode_16.2.app/Contents/Developer"
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version

DEVELOPER_DIR est transmis aux processus enfants. Les appels suivants à xcrun, swift et xcodebuild résolvent donc leurs outils depuis le même répertoire Developer. Chaque pipeline peut définir son propre chemin sans entrer en concurrence pour modifier un état global.

Empêcher les dérives silencieuses avec un script de prévalidation

Afficher les versions dans les journaux ne suffit pas. La prévalidation doit interrompre immédiatement la tâche en cas d’écart, plutôt que de ne révéler le problème qu’après plusieurs minutes de compilation. Le script suivant fige Xcode 16.2 et vérifie le répertoire de l’application, la version réellement utilisée, le SDK et le chemin de Swift :

#!/bin/bash
set -euo pipefail

XCODE_APP="/Applications/Xcode_16.2.app"
EXPECTED_VERSION="16.2"

test -x "$XCODE_APP/Contents/Developer/usr/bin/xcodebuild"
export DEVELOPER_DIR="$XCODE_APP/Contents/Developer"

ACTUAL_VERSION="$(xcodebuild -version | awk 'NR == 1 {print $2}')"
test "$ACTUAL_VERSION" = "$EXPECTED_VERSION"

echo "developer=$DEVELOPER_DIR"
echo "xcode=$ACTUAL_VERSION"
echo "swift=$(xcrun --find swift)"
echo "sdk=$(xcrun --sdk iphoneos --show-sdk-version)"

xcodebuild -checkFirstLaunchStatus

Placez ce script dans le dépôt, par exemple sous ci/verify-toolchain.sh, afin que la version requise soit examinée avec le code. Lors d’une mise à niveau, modifiez à la fois le script et la documentation du projet. L’historique de ces changements sera plus fiable que des opérations manuelles effectuées directement sur la machine.

Si le projet exige une version précise du SDK, ajoutez une vérification exacte. Ne déduisez pas le SDK de la version majeure de Xcode : plusieurs versions d’une même série peuvent embarquer des SDK différents.

Rendre également la commande de build explicite

Une fois la prévalidation réussie, lancez le build depuis un point d’entrée clairement défini :

export DEVELOPER_DIR="/Applications/Xcode_16.2.app/Contents/Developer"

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -derivedDataPath "$PWD/.derived-data" \
  CODE_SIGNING_ALLOWED=NO \
  build

L’espace de travail, le scheme, la configuration et le chemin de Derived Data sont ici indiqués explicitement. CODE_SIGNING_ALLOWED=NO ne convient qu’aux validations de compilation qui ne nécessitent aucune signature. Pour une archive ou un build destiné à un appareil, utilisez le processus de signature sécurisé déjà prévu par le projet au lieu de reprendre ce paramètre tel quel.

Valider la nouvelle version en parallèle avant de basculer

Après l’installation d’une nouvelle version de Xcode, conservez d’abord l’ancienne et exécutez la validation de mise à niveau dans une tâche indépendante. La préparation initiale peut être effectuée par un administrateur :

sudo DEVELOPER_DIR="/Applications/Xcode_16.3.app/Contents/Developer" \
  xcodebuild -runFirstLaunch

Exécutez ensuite le même commit avec l’ancienne puis la nouvelle version. Comparez la réussite de la compilation, le nombre de tests, l’évolution des avertissements, les architectures des artefacts et les métadonnées des archives. Ne supprimez pas immédiatement l’ancien répertoire : en cas d’incompatibilité avec une dépendance ou une option de compilation, vous perdriez toute possibilité de retour rapide.

La validation de la mise à niveau doit aussi vérifier si le format du projet a été réécrit automatiquement. Si la nouvelle version de Xcode modifie des fichiers de projet, examinez ces changements dans un commit distinct au lieu de les mélanger au code fonctionnel. Il devient ainsi possible de distinguer une migration automatique des outils, une mise à jour des dépendances et une véritable évolution des fonctionnalités.

Pièges courants et liste de contrôle avant livraison

Le premier piège consiste à modifier uniquement xcode-select tout en oubliant qu’un DEVELOPER_DIR reste défini dans l’environnement de la tâche. La variable d’environnement est prioritaire : le chemin global affiché dans les journaux n’est donc pas nécessairement celui qu’utilise réellement la tâche. Le deuxième piège consiste à nommer directement l’application Xcode.app. Si une mise à niveau l’écrase, il devient impossible de savoir quelle version les anciennes tâches utilisaient. Le troisième consiste à vérifier uniquement le compilateur sans contrôler le SDK ni le chemin résolu par xcrun, au risque d’intégrer des outils inattendus.

Avant la livraison, effectuez les vérifications suivantes dans cet ordre :

  1. Le répertoire de l’application Xcode contient un numéro de version explicite et l’ancienne version reste utilisable.
  2. La version attendue est consignée dans le dépôt au lieu de dépendre de la mémoire d’une personne.
  3. Chaque tâche de CI définit son propre DEVELOPER_DIR.
  4. La prévalidation contrôle à la fois Xcode, Swift, le SDK et les chemins des outils.
  5. La commande de build précise explicitement le workspace, le scheme et la configuration.
  6. La nouvelle version ne devient la version par défaut qu’après validation de la compilation, des tests et des artefacts.
  7. Les tâches parallèles ne modifient pas le xcode-select global.

Une fois ces étapes appliquées, l’état de la chaîne d’outils du Mac cloud devient traçable dans les journaux. En cas de différence entre deux builds, vous pouvez commencer par écarter une dérive de version, puis examiner les dépendances, le code et la configuration des tâches, au lieu de procéder à l’aveugle entre plusieurs variables.

Questions fréquentes

Des tâches CI simultanées peuvent-elles utiliser des versions différentes de Xcode ?

Oui. Définissez DEVELOPER_DIR dans chaque tâche et évitez de modifier le chemin global de xcode-select pendant les exécutions parallèles.

La commande xcodebuild -version suffit-elle pour valider l’environnement ?

Non. Contrôlez aussi DEVELOPER_DIR, les versions du SDK et de Swift, ainsi que les chemins d’outils résolus par xcrun.

Comment introduire une nouvelle version de Xcode sans interrompre les builds ?

Installez-la sous un nom versionné, effectuez sa préparation initiale, puis lancez les validations, builds et tests avant de changer la version par défaut.

Besoin d’un nœud physique Mac mini dédié

Consultez les configurations disponibles, les nœuds et les cycles de facturation fixes pour appliquer les étapes de l’article à un environnement Mac dans le cloud utilisable durablement.

Louer un Mac mini maintenant