Lorsqu’un build Xcode sur un Mac distant dans le cloud affiche soudainement Cycle inside ... ou Multiple commands produce ..., le premier réflexe — souvent contre-productif — consiste à supprimer immédiatement DerivedData. Cette opération peut modifier temporairement l’ordre des erreurs, mais elle efface aussi de précieux indices de diagnostic. Une méthode plus fiable consiste à figer le code, le Scheme, la configuration et le chemin de Xcode, à conserver le journal complet, puis à déterminer s’il s’agit d’un cycle de dépendances entre targets ou de plusieurs Build Phases qui revendiquent le même chemin de sortie.
Stabiliser la reproduction du problème
Avant de commencer le diagnostic, suspendez la récupération automatique du code, les mises à niveau des dépendances et les builds parallèles afin d’éviter toute modification du workspace entre deux essais. Notez le commit actuel, la version de Xcode et le répertoire de développement réellement sélectionné :
set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p
mkdir -p .diagnostics
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-showBuildTimingSummary \
build 2>&1 | tee .diagnostics/build.log
Si le projet utilise un Workspace, remplacez -project App.xcodeproj par -workspace App.xcworkspace. Ne fournissez pas les deux options simultanément. Extrayez ensuite les passages importants, tout en conservant le journal d’origine pour pouvoir vérifier leur contexte :
grep -nE \
'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
.diagnostics/build.log
La dernière commande indiquée dans le journal correspond généralement à l’endroit où le conflit a été détecté, pas nécessairement au point de départ du cycle. Remontez la chaîne de dépendances affichée par Xcode afin de trouver la première target, le premier script ou le premier chemin de produit qui apparaît plusieurs fois.
Distinguer deux catégories d’erreurs similaires
Un cycle de dépendances signifie que A doit attendre B, tandis que B doit, directement ou indirectement, attendre A. Une sortie dupliquée signifie que deux commandes déclarent toutes deux être responsables du même fichier. Ces problèmes peuvent se produire simultanément, mais ils ne se corrigent pas de la même manière.
| Indice dans le journal | À vérifier en priorité | Cause fréquente |
|---|---|---|
Cycle inside |
Target Dependencies, dépendances implicites | Dépendance réciproque entre l’App et le Framework |
Multiple commands produce |
Copy Files, Compile Sources, Run Script | Même fichier copié ou généré deux fois |
| Le script s’exécute à chaque build | Entrées et sorties du Run Script | Dépendances non déclarées ou analyse des dépendances désactivée |
| Échec uniquement pendant l’Archive | Phase Embed, scripts d’archivage | Configuration incohérente des phases entre Debug et Release |
Commencez par utiliser les commandes suivantes pour confirmer la target et la configuration réellement utilisées par ce build, au lieu de vous fier aux options actuellement visibles dans l’interface de Xcode :
xcodebuild -list -json -project App.xcodeproj \
> .diagnostics/project-list.json
xcodebuild -showBuildSettings \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
> .diagnostics/build-settings.txt
Remonter la chaîne de dépendances des targets et Build Phases
Dans la section Target Dependencies de Xcode, commencez par examiner les targets situées aux deux extrémités de la chaîne signalée. L’App peut dépendre d’un Framework, mais le Framework ne doit pas dépendre à son tour de l’App pour accéder aux types qu’elle contient. Le code partagé doit être déplacé dans un module indépendant, ou la référence inverse doit être supprimée au moyen de protocoles et de l’injection de dépendances.
Examinez ensuite chaque Build Phase et vérifiez en priorité les relations suivantes :
Rechercher les responsabilités dupliquées
Un même fichier source ne doit pas figurer dans deux sections Compile Sources qui produisent le même fichier objet. De même, un Framework ne doit pas être traité à la fois par la phase d’intégration générée par le système et recopié par une phase Copy Files personnalisée. Pour les générateurs de code, définissez explicitement le répertoire de génération afin d’éviter que les sorties soient écrites dans un répertoire source déjà compilé par une autre target.
Vérifier les dépendances implicites
Si un script lit le produit de build d’une autre target à partir d’un chemin fixe sans déclarer explicitement cette dépendance, l’ordre du build peut varier selon le niveau de parallélisme. Ne masquez pas le problème en désactivant les builds parallèles : cela rend seulement l’erreur plus difficile à reproduire, sans corriger la structure du graphe.
Déclarer clairement les entrées et sorties des Run Scripts
Les Run Scripts sont une source fréquente de cycles. Lorsqu’un script lit un fichier de configuration, génère un fichier Swift ou copie des ressources, renseignez ses Input Files et Output Files dans la phase, ou utilisez des listes de fichiers. Le script lui-même doit également garantir qu’une seule phase est responsable de chaque sortie.
Par exemple, un script qui génère un fichier de version peut commencer par vérifier les variables requises, puis utiliser un fichier temporaire et un remplacement atomique :
set -euo pipefail
input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"
test -f "$input"
mkdir -p "$(dirname "$output")"
version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
"$version" > "$tmp"
if test -f "$output" && cmp -s "$tmp" "$output"; then
rm "$tmp"
else
mv "$tmp" "$output"
fi
La phase correspondante doit déclarer $(SRCROOT)/Config/version.txt comme entrée et $(DERIVED_FILE_DIR)/GeneratedVersion.swift comme sortie. Placer les fichiers générés dans le répertoire dérivé facilite l’identification de leur propriétaire par rapport à une modification directe des fichiers du dépôt, tout en réduisant le risque que des tâches parallèles écrasent leurs sorties respectives.
Valider la correction avec des modifications minimales
Ne modifiez qu’une dépendance, une phase ou un chemin de sortie à la fois, puis enregistrez un nouveau journal. Supprimer plusieurs références en une seule opération peut permettre au build de réussir, mais rend difficile l’identification de la véritable cause et risque de laisser subsister un problème similaire dans la configuration d’archivage.
Après la correction, procédez à la validation dans l’ordre suivant :
- Exécutez un build propre pour confirmer que le graphe complet peut être reconstruit à partir de zéro.
- Sans effectuer de nettoyage, lancez deux builds consécutifs afin de vérifier que les scripts ignorent correctement les entrées inchangées.
- Contrôlez séparément Debug et Release ; pour un projet destiné à être distribué, exécutez également une Archive.
- Comparez les deux journaux et vérifiez qu’aucune nouvelle sortie dupliquée ni aucun avertissement relatif à un script exécuté sans condition n’est apparu.
- Documentez dans la revue de code les changements apportés aux dépendances des targets, les entrées et sorties des phases, ainsi que leur justification.
Le nettoyage de DerivedData peut servir de dernier test d’isolation, mais ne constitue pas une correction. Un projet Xcode réellement stable doit produire le même ordre de dépendances avec différents niveaux de parallélisme, aussi bien lors d’un build propre que d’un build incrémental. Les développements interactifs et les tâches automatisées sur un Mac dans le cloud ne risquent alors plus d’échouer aléatoirement à cause de variations d’ordonnancement.
Questions fréquentes
Supprimer DerivedData corrige-t-il un cycle de build Xcode ?
Généralement non. Cette action élimine les anciens fichiers intermédiaires, mais le cycle réapparaît si les dépendances ou les chemins de sortie restent inchangés.
Pourquoi une phase Run Script peut-elle créer un cycle ?
Sans entrées et sorties déclarées, Xcode ne sait pas ordonner le script par rapport aux autres phases, surtout lorsque plusieurs étapes écrivent au même endroit.
Comment valider la correction ?
Exécutez un build propre puis deux builds consécutifs sans nettoyage, et vérifiez l'absence de cycles, de sorties dupliquées et de scripts exécutés sans condition.
Exécutez votre prochaine tâche Mac à distance sur un nœud dédié
Choisissez parmi deux configurations disponibles selon l’ampleur de la tâche, cinq nœuds et des cycles à la journée, à la semaine, au mois ou au trimestre. Vérifiez l’intégralité de la configuration et le montant en USD avant de commander.