Lorsqu’un projet qui se compile correctement sur un Mac local est transféré vers des tâches cloud non supervisées, le point le plus souvent négligé n’est pas la configuration du compilateur, mais les phases Run Script des Build Phases. Un script peut lire une configuration située hors du dépôt, écrire dans le répertoire des sources ou s’exécuter à chaque build faute de sorties déclarées. Une exécution isolée ne révèle pas forcément le problème, tandis que des tâches concurrentes peuvent écraser mutuellement leurs fichiers et rendre progressivement les builds incrémentaux inutiles. Pour corriger ce type de défaillance, il faut d’abord définir clairement les limites d’accès aux fichiers de chaque script, puis les valider avec le sandbox, plutôt que d’ajouter immédiatement des tentatives de relance.
Établir une base d’audit reproductible
Commencez par figer le projet, le Scheme, la configuration de build et le répertoire Derived Data. Pendant l’audit, ne réutilisez pas le répertoire habituel : d’anciens artefacts pourraient masquer l’absence d’une étape de génération.
set -euo pipefail
ROOT="$PWD"
DERIVED="$ROOT/.audit-derived"
rm -rf "$DERIVED"
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED" \
clean build | tee "$ROOT/audit-clean.log"
Si le projet utilise un Workspace, remplacez -project par -workspace. Le premier build sert à vérifier la chaîne complète. Sans modifier les sources, lancez ensuite un second build sans clean. Relevez le nombre d’exécutions des Run Script, leur durée totale et la date de modification de leurs fichiers de sortie. Ces deux journaux constituent respectivement les références du build propre et du build incrémental.
L’objectif de l’audit n’est pas d’ignorer tous les scripts, mais de faire en sorte que chacun ne s’exécute que lorsqu’une entrée change, qu’une sortie manque ou qu’une condition d’exécution explicite l’exige.
Inventorier toutes les phases Run Script
Repérez d’abord les scripts dans le fichier du projet, puis revenez dans Xcode pour déterminer leur Target, leur position avant ou après la compilation, ainsi que l’activation éventuelle de l’analyse des dépendances. Une recherche textuelle convient à ce premier tri :
grep -nE "PBXShellScriptBuildPhase|shellScript =|inputPaths =|outputPaths =" \
App.xcodeproj/project.pbxproj
Créez un tableau pour chaque phase sans vous limiter au nom du script. Beaucoup s’appellent simplement « Run Script », ce qui ne permet pas de diagnostiquer un problème.
| Point à contrôler | Question à résoudre |
|---|---|
| Condition d’exécution | S’exécute-t-il à chaque fois ou uniquement lorsque les dépendances changent ? |
| Entrées | Quels fichiers sources, configurations, outils et listes de fichiers sont lus ? |
| Sorties | Où sont écrits les fichiers générés, les rapports ou les marqueurs d’achèvement ? |
| Effets de bord | Modifie-t-il les sources, une configuration globale ou un cache partagé ? |
| Concurrence | Deux tâches simultanées écrivent-elles au même emplacement ? |
| Stratégie d’échec | Un échec d’une sous-commande entraîne-t-il immédiatement un code de sortie non nul ? |
Il est recommandé de commencer chaque script par set -euo pipefail. Vérifiez également les commandes reliées par des pipelines : sans pipefail, l’échec d’une commande en amont peut être masqué par un tee final qui réussit.
Déclarer les limites d’accès avec xcfilelist
Un petit nombre de chemins peut être saisi directement dans Input Files et Output Files. Pour des listes plus longues, les fichiers .xcfilelist sont plus faciles à auditer. Dans la mesure du possible, les chemins doivent s’appuyer sur des variables de build telles que $(SRCROOT) et $(DERIVED_FILE_DIR), plutôt que sur des répertoires utilisateur codés en dur.
Par exemple, une phase qui génère une empreinte à partir d’une configuration YAML peut utiliser la liste d’entrées suivante :
$(SRCROOT)/Config/app.yml
$(SRCROOT)/Scripts/generate-config.sh
La liste des sorties ne doit contenir que les fichiers réellement produits par le script :
$(DERIVED_FILE_DIR)/Generated/config.sha256
Le script correspondant doit d’abord écrire le fichier temporaire dans le répertoire cible, puis remplacer atomiquement le fichier final afin d’éviter qu’une tâche concurrente ne lise un résultat incomplet :
set -euo pipefail
SOURCE="$SRCROOT/Config/app.yml"
OUTPUT="$DERIVED_FILE_DIR/Generated/config.sha256"
TEMP="$OUTPUT.tmp.$$"
mkdir -p "$(dirname "$OUTPUT")"
shasum -a 256 "$SOURCE" > "$TEMP"
mv "$TEMP" "$OUTPUT"
Ne déclarez pas approximativement l’ensemble du dépôt comme entrée, ni la racine des sources comme sortie. Une portée trop large peut supprimer les erreurs, mais elle déclenche aussi le script au moindre changement et masque les dépendances réelles. Écrivez de préférence les artefacts générés dans Derived Data. Lorsqu’une étape de génération de code doit impérativement modifier le dépôt, exécutez-la séparément et faites vérifier les différences par le gestionnaire de versions.
Activer le sandbox et analyser les refus
Après cette première passe de déclaration, validez le résultat en surchargeant le réglage de build depuis la ligne de commande. Il n’est pas nécessaire de modifier immédiatement toutes les configurations :
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-derivedDataPath "$PWD/.audit-derived" \
ENABLE_USER_SCRIPT_SANDBOXING=YES \
build | tee "$PWD/audit-sandbox.log"
En cas de sandbox deny, identifiez d’abord le chemin refusé, le type d’opération et la phase concernée. Un fichier de configuration lu sans être déclaré doit être ajouté aux entrées ; un fichier créé ou modifié sans déclaration doit être ajouté aux sorties. Les outils qui chargent leurs bibliothèques système n’exigent généralement pas d’ajouter tous les répertoires système aux listes. Concentrez-vous sur les fichiers du projet, les configurations et les outils internes auxquels le script accède explicitement.
Une erreur fréquente consiste à désactiver directement le sandbox ou à ajouter tout le répertoire personnel de l’utilisateur aux entrées. La première solution laisse subsister les dépendances implicites ; la seconde provoque des reconstructions incontrôlables. Si un script dépend d’un fichier extérieur au dépôt, copiez ce fichier dans le répertoire de travail de la tâche, vérifiez-le, puis déclarez-le comme entrée explicite.
Intégrer les validations incrémentales et concurrentes à la CI
Après correction, exécutez au minimum quatre séries de tests : un build propre avec un répertoire Derived Data vide, un second build sans modification des sources, un build après modification d’une seule entrée déclarée, puis deux builds parallèles utilisant des répertoires Derived Data indépendants. Les tâches parallèles peuvent partager une copie en lecture seule des sources, mais jamais leurs répertoires de sortie.
Contrôlez les points suivants dans cet ordre :
- Le build propre peut générer depuis zéro tous les artefacts nécessaires.
- Le second build n’exécute pas systématiquement les scripts dont les sorties existent déjà et sont stables.
- La phase concernée est relancée après la modification d’une entrée déclarée.
- La modification d’un fichier sans rapport ne déclenche pas cette phase.
- Deux tâches parallèles n’écrasent pas le même fichier temporaire ou le même rapport.
- L’échec d’un script entraîne un code de sortie non nul pour
xcodebuild. - Les journaux n’affichent ni jetons, ni contenu de clés privées, ni variables d’environnement complètes.
Enfin, enregistrez le réglage du sandbox dans la configuration de build réellement utilisée par l’équipe et conservez une tâche de build propre exécutée périodiquement. Les builds incrémentaux garantissent la rapidité, tandis que les builds propres révèlent les dépendances manquantes. Ce n’est qu’en réussissant les deux qu’un Run Script peut être considéré comme reproductible et correctement délimité.
Questions fréquentes
Pourquoi une phase Run Script s’exécute-t-elle à chaque build ?
Elle ne déclare généralement aucune sortie, ou l’exécution fondée sur l’analyse des dépendances est désactivée. Des entrées et sorties stables permettent à Xcode de déterminer si la phase est à jour.
Comment corriger un message sandbox deny ?
Repérez le chemin refusé et le type d’accès dans le journal, puis déclarez ce chemin comme entrée ou sortie, directement ou avec un xcfilelist. Désactiver le sandbox ne corrige pas la dépendance.
Comment tester le build incrémental après la modification ?
Lancez deux fois le même build, puis modifiez une entrée déclarée. Le second build inchangé doit ignorer la phase, tandis que la modification doit relancer le script et actualiser sa sortie.
Placez votre prochaine compilation iOS sur MiniDebug M4.
M4, 16 Go de RAM et SSD de 256 Go inclus. Location à la journée, à la semaine, au mois ou au trimestre, avec cinq nœuds au choix : Singapour, Tokyo, Séoul, Hong Kong et côte Est des États-Unis. La disponibilité réelle est indiquée en temps réel dans la console.