Journal d’ingénierie

Éviter les verrous DerivedData dans les builds Xcode parallèles

Éviter les verrous DerivedData dans les builds Xcode parallèles

Lorsque deux pipelines Xcode s’exécutent simultanément sur le même Mac dans le cloud, les erreurs intermittentes comme database is locked, la corruption de la base d’indexation ou les erreurs d’édition de liens « impossibles à reproduire en local » ne sont souvent pas dues à une modification du code. Commencez par vérifier si les deux tâches utilisent le répertoire par défaut ~/Library/Developer/Xcode/DerivedData. Ce répertoire ne contient pas seulement des caches : il héberge également les bases de données de build, les index, les caches de modules et les fichiers intermédiaires continuellement réécrits par les tâches. Le traiter comme un cache partagé autorisant des écritures concurrentes conduit inévitablement à des échecs.

Identifier d’abord l’origine de la contention

Ne supprimez pas immédiatement tous les caches dès qu’un build échoue. Commencez par conserver la commande xcodebuild complète de la tâche en échec, son heure de démarrage, son répertoire de travail et la liste des processus. Recherchez ensuite dans les journaux les occurrences de locked, database, unable to attach, malformed ainsi que les messages signalant des artefacts produits plusieurs fois.

pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log

Si les deux tâches utilisent la même valeur pour -derivedDataPath, ou si aucune ne définit explicitement ce paramètre, vous avez déjà identifié le principal facteur de risque. Vérifiez également qu’après l’expiration d’un pipeline, seul le script parent n’a pas été arrêté tandis que xcodebuild ou ses sous-processus de compilation continuent à écrire dans le répertoire.

La réussite d’une nouvelle tentative ne prouve pas que le problème est résolu. Si la fenêtre de concurrence se réduit, le deuxième build peut réussir par hasard alors que les écritures partagées existent toujours.

Distinguer un conflit de verrouillage d’un échec de compilation ordinaire

Les erreurs de compilation du code source apparaissent généralement de manière reproductible dans le même fichier et à la même ligne. Les conflits de verrouillage dépendent davantage de l’ordonnancement des tâches concurrentes et peuvent survenir de façon aléatoire pendant la résolution des dépendances, la génération des modules ou l’édition de liens. Si les builds réussissent systématiquement une fois les tâches parallèles désactivées, puis échouent à nouveau dès que la concurrence est rétablie, examinez en priorité la propriété des répertoires plutôt que de modifier le code applicatif.

Attribuer un espace de travail distinct à chaque tâche

La méthode fiable consiste à attribuer un identifiant unique à la tâche dans le chemin du dépôt extrait, de DerivedData, du paquet de résultats et du répertoire temporaire. Cet identifiant peut provenir du numéro de tâche du pipeline ; pour une validation locale, l’identifiant du processus peut servir de valeur de remplacement.

set -euo pipefail

JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"

mkdir -p "$DERIVED_DATA"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -resultBundlePath "$RESULT_BUNDLE" \
  build-for-testing

test -d "$RESULT_BUNDLE"

Le chemin doit être créé par la tâche et ne doit pouvoir être supprimé que par elle. N’utilisez pas le nom du projet comme nom de répertoire unique, car deux branches d’un même projet entreraient encore en collision. Les scripts de nettoyage ne doivent pas non plus employer de correspondances approximatives, par exemple en supprimant tous les répertoires DerivedData dont le nom contient App.

Séparer la résolution des dépendances de la compilation

Après avoir isolé DerivedData, si plusieurs tâches continuent à modifier simultanément le même répertoire d’extraction des dépendances, le problème se déplace vers l’étape de résolution des paquets. Résolvez d’abord les dépendances, puis interdisez toute modification automatique de leurs versions pendant le build principal. Le fichier de verrouillage validé doit être versionné dans le dépôt, et le pipeline ne doit jamais le mettre à jour silencieusement.

PACKAGES="$ROOT/SourcePackages"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$PACKAGES"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "$PACKAGES" \
  -disableAutomaticPackageResolution \
  build

Si un cache est nécessaire, mettez en cache les téléchargements ou un instantané immuable préalablement vérifié, au lieu de laisser plusieurs tâches écrire dans le même répertoire actif. La clé de cache doit au minimum inclure l’empreinte du fichier de verrouillage, la version de Xcode et l’architecture cible. Même en cas de cache trouvé, vérifiez que le fichier de verrouillage n’a pas changé.

Définir une grille de validation objective

Une seule exécution réussie ne suffit pas à valider la correction. Lancez simultanément plusieurs séries de tâches sur le même commit, puis contrôlez les limites entre les répertoires, les processus et les artefacts.

| Élément contrôlé | Condition de réussite | Action en cas d’échec | |---|---|---| | Chemin DerivedData | Unique pour chaque tâche parallèle | Corriger l’identifiant de tâche et la construction du chemin | | Fichier de verrouillage des dépendances | Empreinte identique avant et après le build | Interdire la résolution automatique et vérifier les scripts | | Processus résiduels | Aucun processus de compilation associé après la fin de la tâche | Corriger la gestion des délais d’expiration et des signaux d’arrêt | | Paquet de résultats | Généré séparément par chaque tâche et lisible | Séparer les valeurs de `resultBundlePath` | | Périmètre du nettoyage | Seul le répertoire racine de la tâche courante est supprimé | Retirer les caractères génériques et la suppression de répertoires partagés |

Il est recommandé d’exécuter simultanément au moins deux builds strictement identiques, puis une série de builds provenant de branches différentes. Le premier test valide l’isolation des écritures ; le second permet de détecter les répertoires de travail, caches de modules ou chemins d’artefacts qui resteraient partagés sur la seule base du nom du projet.

Conserver les preuves et nettoyer en toute sécurité

L’environnement d’une tâche en échec ne doit pas être détruit immédiatement. Archivez d’abord les journaux de build, le paquet de résultats, la version de Xcode, l’empreinte du fichier de verrouillage, l’espace disque disponible et le chemin de la tâche. Le code source, les variables d’environnement et les jetons présents dans les journaux doivent être expurgés au préalable. Une tâche réussie peut être nettoyée après confirmation que ses artefacts ont été copiés dans un répertoire de publication indépendant.

Le nettoyage doit de préférence être limité au répertoire racine de la tâche et inclure une vérification du préfixe du chemin. Si une tâche est arrêtée de force, le mécanisme de récupération ultérieur doit traiter les répertoires résiduels à partir de l’identifiant de la tâche, plutôt que de vider tous les répertoires de développement de la machine. Lors de l’exécution de plusieurs pipelines sur NUMACS, vérifiez également dans la console les configurations actuellement disponibles, puis définissez la limite de tâches en fonction du niveau de concurrence. Une concurrence plus élevée ne remplace pas l’isolation des écritures.

L’objectif final n’est pas de rendre l’erreur « moins fréquente », mais d’établir des invariants clairs : chaque tâche parallèle dispose d’un seul processus autorisé à écrire, les versions des dépendances restent inchangées pendant le build, le contexte d’un échec demeure traçable et le nettoyage n’affecte aucune autre tâche. Une fois ces quatre conditions remplies, DerivedData cesse d’être une source de pannes aléatoires et redevient un ensemble de données de build maîtrisable.

Questions fréquentes

Chaque tâche parallèle doit-elle avoir son propre DerivedData ?

Oui. Toute tâche susceptible d’écrire en même temps doit recevoir un chemin unique avec -derivedDataPath. Un chemin commun exige une exécution strictement séquentielle.

L’isolation supprime-t-elle tous les bénéfices du cache ?

Non. Les dépendances téléchargées et les artefacts immuables validés peuvent être réutilisés séparément, tandis que les données modifiables restent propres à chaque tâche.

Une simple relance suffit-elle après une erreur database is locked ?

Non. La relance masque la concurrence sans la corriger. Il faut supprimer les chemins d’écriture partagés et rechercher les processus xcodebuild résiduels.

Numacs Mac dans le cloud

Externalisez vos tâches de build sur une station de travail physique dédiée

Deux configurations Apple Silicon sont proposées sur des machines physiques dédiées, jamais virtualisées. Louez-les à la journée, à la semaine, au mois ou au trimestre, avec des nœuds à Singapour, Tokyo, Séoul et Hong Kong.

Choisir un appareil et commander