Lorsqu’un même Mac cloud exécute plusieurs tâches CI à la suite, les pannes les plus difficiles à reproduire ne viennent souvent pas du code, mais des fichiers temporaires laissés par la tâche précédente. D’anciens sockets, des verrous non libérés, des caches partiellement écrits ou des répertoires d’export portant le même nom peuvent produire des résultats différents sur un même commit lors de la compilation suivante. Un nœud physique dédié élimine les interférences provenant d’autres comptes, mais n’isole pas automatiquement les tâches simultanées d’un même compte. La pipeline doit donc définir explicitement les limites du système de fichiers.
Identifier les répertoires qui laissent des résidus entre les tâches
Un espace de travail vidé ne signifie pas que l’environnement a été entièrement réinitialisé. Les outils macOS répartissent leur état au-delà de cet espace. Le diagnostic doit au minimum porter sur les quatre catégories d’emplacements suivantes :
- les fichiers temporaires, sockets et fragments de téléchargement sous
TMPDIR; - DerivedData, les index et les bases de données de compilation de Xcode ;
- le cache des modules Clang et les caches SwiftPM propres à chaque tâche ;
- les répertoires d’export, de journaux et de pièces jointes de test créés par les scripts.
Exécutez env | sort avant et après une compilation normale afin de consigner les chemins réellement utilisés pour HOME, TMPDIR et l’espace de travail. Comparez ensuite la taille des répertoires concernés avec du -sh, au lieu de commencer par une suppression récursive. Si l’échec ne se produit qu’en cas d’exécution simultanée, recherchez en priorité les noms de fichiers fixes, les ports fixes et les chemins de sortie fixes.
L’objectif de l’isolation n’est pas de supprimer tous les caches à chaque exécution, mais d’attribuer explicitement à chaque tâche ses propres répertoires accessibles en écriture et de ne nettoyer que le contenu qu’elle a créé.
Créer un répertoire racine unique pour chaque tâche
L’identifiant de la tâche doit provenir d’un numéro unique fourni par le système CI. En l’absence d’identifiant fiable, mktemp peut en générer un. N’utilisez pas uniquement le nom de la branche, car une même branche peut déclencher plusieurs compilations simultanées.
#!/bin/bash
set -euo pipefail
job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"
export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"
mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"
cleanup() {
local status=$?
rm -rf "$job_root"
exit "$status"
}
trap cleanup EXIT INT TERM
Par défaut, le répertoire créé par mktemp ne portera pas le même nom que celui d’une autre tâche. La fonction de nettoyage enregistre d’abord le code de sortie, supprime le répertoire, puis quitte avec le même état. Elle évite ainsi qu’une commande de nettoyage masque l’échec d’un test. Les variables de chemin doivent être placées entre guillemets afin qu’un nom d’espace de travail contenant des espaces ne soit pas découpé en plusieurs arguments.
Ne pas remplacer HOME sans discernement
Faire pointer HOME vers le répertoire de la tâche semble assurer une isolation complète, mais peut couper l’accès au trousseau du compte courant, aux préférences des outils et aux initialisations système déjà effectuées. Pour une compilation ordinaire, il est possible d’attribuer un cache séparé à chaque gestionnaire de paquets concerné. Lorsqu’une signature est nécessaire, il vaut généralement mieux conserver HOME et isoler uniquement les produits de compilation ainsi que les caches dont la migration est explicitement prise en charge.
Raccorder explicitement les chemins d’écriture de Xcode
Définir uniquement TMPDIR ne déplace pas automatiquement DerivedData. La commande de compilation doit recevoir les chemins essentiels sous forme de paramètres afin que les journaux indiquent directement les répertoires utilisés par la tâche en cours.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
COMPILER_INDEX_STORE_ENABLE=NO \
CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
test \
-resultBundlePath "$RESULT_BUNDLE"
Si l’indexation du code n’est pas nécessaire dans la CI, le stockage de l’index peut être désactivé afin de réduire les écritures inutiles. Les résultats des tests doivent être placés dans le répertoire racine de la tâche. Toutefois, s’il faut téléverser des pièces jointes après un échec, leur archivage doit être terminé avant le déclenchement du nettoyage. Un enchaînement plus robuste consiste à exécuter les tests, copier les fichiers de diagnostic vers la zone d’artefacts de la pipeline, vérifier le résultat de la copie, puis quitter.
Caches partagés en lecture seule, caches de tâche accessibles en écriture
Lorsqu’il est nécessaire de réutiliser des dépendances, un cache partagé préalablement vérifié peut servir de base. Au démarrage, chaque tâche le copie ou le restaure dans son propre répertoire. La compilation écrit uniquement dans cette copie, puis une étape distincte peut mettre à jour la version partagée après une exécution réussie. Ne laissez jamais deux processus xcodebuild écrire simultanément dans le même cache de modules ou la même base de données de compilation.
Conserver des éléments de diagnostic même en cas d’arrêt anormal
L’utilisation directe de trap 'rm -rf ...' EXIT supprime immédiatement les traces au moment de l’échec. En pratique, il faut d’abord produire un bref relevé de diagnostic comprenant le code de sortie, l’espace disque disponible, la taille des répertoires et les processus concernés encore actifs, puis décider des fichiers à conserver. N’affichez pas l’ensemble des variables d’environnement dans les journaux, car elles peuvent contenir des jetons ou des valeurs liées à la signature.
La commande suivante peut être exécutée avant le nettoyage :
{
echo "exit_status=$status"
df -h "$job_root"
du -sh "$job_root"/* 2>/dev/null || true
find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"
ARTIFACT_DIR doit se trouver en dehors du répertoire racine de la tâche. La pipeline doit assurer son téléversement et son nettoyage périodique. S’il faut conserver l’état ayant provoqué l’échec, le répertoire de diagnostic peut être compressé avant la suppression du répertoire d’origine. Les journaux et les chemins doivent alors être expurgés de toute information sensible.
Vérifier l’isolation par un audit des résidus
Une fois les modifications terminées, exécutez deux fois de suite le même commit. Avant la seconde exécution, vérifiez qu’il ne reste aucun répertoire associé à l’identifiant de la tâche précédente. En cas d’exécution simultanée, assurez-vous également que les chemins de TMPDIR, de DerivedData et des paquets de résultats sont entièrement différents entre les deux tâches.
La validation peut reposer sur cinq points fixes : chaque tâche dispose d’un répertoire racine unique ; tous les chemins Xcode sont explicitement définis ; aucun cache partagé ne fait l’objet d’écritures simultanées ; le gestionnaire de sortie s’exécute en cas de réussite comme d’échec ; les artefacts de diagnostic sont copiés avant le nettoyage. Si l’espace disque utilisé continue malgré tout d’augmenter, recherchez à partir de l’identifiant de tâche les répertoires d’outils qui échappent encore aux limites définies, au lieu d’élargir la portée des suppressions.
Sur les Mac physiques dédiés de SureVM, cette méthode convient particulièrement aux nœuds de compilation utilisés sur la durée : la chaîne d’outils installée à l’échelle de la machine est conservée, tandis que l’état variable de chaque tâche reste limité à des répertoires traçables et supprimables. Le critère final est simple : que la tâche précédente ait réussi, échoué ou été interrompue, elle ne doit jamais modifier les conditions d’entrée de la tâche suivante.
Questions fréquentes
Pourquoi éviter un répertoire temporaire fixe partagé par tous les jobs CI ?
Des jobs parallèles peuvent écraser des fichiers portant le même nom. Un job interrompu peut aussi laisser des verrous, des sockets ou des résultats partiels qui perturbent l’exécution suivante.
La définition de TMPDIR suffit-elle à isoler DerivedData ?
Non. Xcode peut placer DerivedData et le cache des modules dans des emplacements distincts. Il faut donc fournir explicitement un chemin propre à chaque job.
Faut-il remplacer HOME pendant une tâche de signature ?
Généralement non. La signature peut dépendre du Keychain de l’utilisateur courant. Il vaut mieux isoler d’abord les fichiers temporaires, les sorties de compilation et les caches clairement portables.
Choisissez des nœuds Mac physiques dédiés pour compiler, développer et expérimenter
Comparez deux configurations de Mac mini M4, quatre durées de location et cinq nœuds disponibles, puis choisissez selon votre flux de travail.