- Le cache DerivedData sur un Mac n’existe pas par défaut sur un second Runner — le coût caché du CI iOS multi-nœuds, c’est que chaque machine redémarre à froid de son côté
- Ce que vous pouvez partager, c’est la couche d’artefacts de build (DerivedData, résolution SPM, Pods/) — pas « plusieurs machines écrivent en même temps dans le même répertoire » ; pull → build → push ou un hub de cache régional est l’approche dominante en 2026
- Sur une flotte de Cloud Mac, utilisez le hash du lockfile + la version Xcode comme clés de cache avec rsync/stockage objet — les builds à chaud économisent souvent 5 à 15 minutes de plus (en plus de l’optimisation cache mono-machine)
Vous avez configuré actions/cache sur GitHub Actions et les builds à chaud sont passés de 18 à 9 minutes — puis vous passez à trois Runners M4 auto-hébergés et le P95 remonte à 16 minutes. Chaque machine « voit ce commit pour la première fois ».
Ce n’est pas un retour à la case départ, mais la phase deux des builds multi-nœuds : le partage de cache Xcode. Cet article ne traite pas du « pourquoi mettre en cache » (voir le guide mono-machine CocoaPods / SPM / DerivedData), mais de comment synchroniser les données de build entre plusieurs Mac pour que n’importe quel Runner réutilise les modules qu’un collègue a déjà compilés.
1. Pourquoi le CI multi-nœuds est plus lent qu’en mono-machine
Quand l’équilibrage de charge assigne aléatoirement les jobs aux Runners A/B/C, l’état disque de chaque machine est indépendant. Le MyAppKit.swiftmodule que le Runner A vient de compiler ne sert à rien au Runner B — sauf si vous transférez les artefacts.
Les runners hébergés par GitHub utilisent actions/cache pour stocker des tarballs dans le cloud et les restaurer au démarrage du job. Les flottes auto-hébergées ou de Cloud Mac n’ont souvent pas d’équivalent géré, donc les équipes :
- Acceptent un réchauffement par machine (calcul gaspillé)
- Montent des répertoires de cache sur NFS avec écritures concurrentes (risque de corruption)
- Construisent un hub de cache : pull avant le build, push après succès
La troisième option est la plus courante chez les équipes iOS et convient aux déploiements multi-régions : trois M4 à US East partagent un bucket de cache, deux en Asie-Pacifique un autre, et seule la couche SPM/Pods se synchronise entre régions.
2. Couches des données de build Xcode
Avant de synchroniser, séparez ce qui vaut le transfert de ce qui ne doit pas être mélangé.
2.1 DerivedData
Le répertoire visé par xcodebuild -derivedDataPath, contenant les .o compilés, .swiftmodule, intermédiaires de link et index partiels. Le volume atteint souvent plusieurs Go — c’est le plus gros levier pour la vitesse des builds à chaud et la couche la plus sensible à la version Xcode.
Fixez un chemin en CI, par ex. /Volumes/CI/DerivedData/<scheme>, aligné sur la variable REMOTE de votre script de sync — l’article cache mono-machine insistait : chemins différents, pas de cache du tout.
2.2 SPM et CocoaPods
SPM : ~/Library/Caches/org.swift.swiftpm + .build dans le dépôt (si vous commitez la résolution). CocoaPods : Pods/ et ~/Library/Caches/CocoaPods.
Ces couches changent moins souvent que DerivedData (elles suivent le lockfile), donc elles conviennent au sync inter-machines et même inter-régions. Beaucoup d’équipes ne poussent que SPM/Pods vers S3 et gardent DerivedData en rsync régional.
2.3 ModuleCache et caches globaux Xcode
~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex et divers SDKStatCaches. En général gérés avec DerivedData ; si sync séparé, imposez la même version mineure Xcode.
3. Quatre architectures de sync multi-nœuds
| Modèle | Approche | Avantages | Risques |
|---|---|---|---|
| Local uniquement | Chaque Runner a son disque, pas de partage | Zéro ops | La montée en charge horizontale ne gagne pas de temps |
| Montage NFS partagé | DerivedData sur volume réseau, lecture seule ou lecture-écriture | Setup simple | Écritures concurrentes corrompent facilement ; latence réseau ralentit le linking |
| Hub rsync | Répertoire central ou machine leader ; pull avant build, push après succès | Contrôlable, bonne compatibilité Xcode | Scripts et verrous requis |
| Tarball stockage objet | Empaqueter et envoyer vers S3/R2 par clé de cache ; télécharger et extraire au démarrage | Inter-régions, proche du modèle cache GHA | Gros uploads DerivedData lents ; compression nécessaire |
En pratique 2026, les flottes Cloud Mac même datacenter / même région préfèrent les hubs rsync ; les équipes globales utilisent le stockage objet pour SPM/Pods et gardent DerivedData régional. Ne considérez pas NFS comme une solution miracle pour DerivedData partagé multi-writer.
Ne lancez jamais xcodebuild sur deux Runners en même temps contre le même arbre DerivedData. Si vous devez partager un volume, utilisez des verrous fichier ou du single-writer multi-reader (un warmer désigné build, puis rsync distribue).
4. Clés de cache et invalidation
Les clés de cache partagé multi-nœuds doivent être plus conservatrices qu’en mono-machine — un faux hit est pire qu’un démarrage à froid (erreurs de link mystérieuses, échecs de signature).
Champs à inclure dans la clé :
xcodebuild -versionou variable d’environnementXCODE_VERSIONhashFiles('**/Podfile.lock')ouPackage.resolved- Nom de scheme ou ensemble de targets (caches séparés pour repos multi-scheme)
arm64(préfixe Apple Silicon uniquement, évite la pollution x86 héritée)
Ne pas utiliser le github.sha complet comme seule clé — sinon chaque machine rate toujours. Utilisez des restore-keys en couches : dd-arm64-main-<lockhash> → dd-arm64-main- → dd-arm64-.
Invalider quand : mise à niveau Xcode, changement de lockfile, SWIFT_VERSION ou changement majeur des Build Settings, ou après un clean build manuel — incrémentez le suffixe de clé ou supprimez le répertoire distant.
5. Pratique : script rsync pull/push
Voici un script minimal validé sur une flotte M4 Cloud Mac. Le hub de cache est à cache-leader.internal:/cache/ios/ (machine à grand disque dans la flotte ou NAS).
#!/usr/bin/env bash
# cache-sync.sh — 在 xcodebuild 前后调用
set -euo pipefail
ACTION="${1:?pull|push}"
CACHE_ROOT="${CACHE_ROOT:-cache-leader.internal:/cache/ios}"
SCHEME="${SCHEME:-MyApp}"
KEY="${CACHE_KEY:-arm64-$(md5 -q Podfile.lock 2>/dev/null || echo nolock)-$(xcodebuild -version | head -1 | tr ' ' '-')}"
LOCAL_DD="${DERIVED_DATA_PATH:-/Volumes/CI/DerivedData/${SCHEME}}"
REMOTE="${CACHE_ROOT}/${KEY}/${SCHEME}/DerivedData"
LOCAL_SPM="${HOME}/Library/Caches/org.swift.swiftpm"
REMOTE_SPM="${CACHE_ROOT}/${KEY}/swiftpm"
rsync_opts=(-az --delete-delay --contimeout=10)
case "$ACTION" in
pull)
rsync "${rsync_opts[@]}" "${REMOTE}/" "${LOCAL_DD}/" || true
rsync "${rsync_opts[@]}" "${REMOTE_SPM}/" "${LOCAL_SPM}/" || true
;;
push)
# 仅成功构建后调用
rsync "${rsync_opts[@]}" "${LOCAL_DD}/" "${REMOTE}/"
rsync "${rsync_opts[@]}" "${LOCAL_SPM}/" "${REMOTE_SPM}/"
;;
*) echo "usage: $0 pull|push" >&2; exit 1 ;;
esac
Flux CI :
- Début du job →
cache-sync.sh pull xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build- Uniquement si build réussi →
cache-sync.sh push
Avec des machines en parallèle, push peut entrer en course — utilisez flock ou acceptez last-write-wins ; DerivedData sous le même lockfile + Xcode devrait être compatible. Si push très fréquent, passez à un upload asynchrone (rsync en arrière-plan après le build, sans bloquer le pipeline).
6. Hubs de cache Cloud Mac inter-régions
Quand les Runners sont en US East, US West et Asie-Pacifique, le rsync transocéanique du DerivedData complet vaut souvent peu (latence + egress). Approche recommandée :
- Un bucket de cache par région (S3 / R2 / stockage objet du fournisseur)
- Contenu sync : tarball
Pods/clé par hashPodfile.lock+ cache SPM clé parPackage.resolved - DerivedData uniquement dans la région via rsync ; premier job à froid, suivants à chaud
- Aligner les versions d’image avec les tags golden image pour éviter la dérive Xcode qui invalide toute la bibliothèque
Compressez le cache SPM avec tar czf avant upload — souvent 40 % à 60 % plus petit ; DerivedData est déjà composé de nombreux petits fichiers, donc tar + upload parallèle est plus stable que le rsync inter-régions brut.
7. Intégration GitHub Actions / Runners auto-hébergés
Sur les runners auto-hébergés, enveloppez le workflow ainsi :
- name: Restore shared Xcode cache
run: ./scripts/cache-sync.sh pull
env:
CACHE_ROOT: ${{ secrets.IOS_CACHE_SSH }}
SCHEME: MyApp
DERIVED_DATA_PATH: /Volumes/CI/DerivedData/MyApp
- name: Build
run: xcodebuild -scheme MyApp -derivedDataPath /Volumes/CI/DerivedData/MyApp build
- name: Save shared Xcode cache
if: success()
run: ./scripts/cache-sync.sh push
Si vous utilisez encore actions/cache hébergé par GitHub, les flottes auto-hébergées peuvent faire du double canal : cache GHA en secours, hub rsync comme couche basse latence même région. Gardez les deux espaces de noms de clés séparés pour éviter les écrasements.
Monitoring : durée pull/push, octets transférés, et si le build était incrémental (compter les lignes CompileSwift dans les logs xcodebuild). Quand le P95 ralentit, vérifiez d’abord le cache miss, puis la charge machine — le même playbook que pour diagnostiquer des temps de build erratiques.
8. Liste des pièges
- Écritures concurrentes DerivedData :
stat cache file corruptedaléatoire → passer au pull/push - Versions mineures Xcode différentes : incompatibilité ABI des modules → clé inclut la version, images partagent un tag unifié
- Décalage de chemins : cache au chemin A, compilation au chemin B → rebuild complet
- Push après clean : répertoires vides écrasent un bon cache → push uniquement en succès et pas après clean
- Produits signés dans le cache : signature obsolète après changement de provisioning → exclure
Build/Productsdu cache ou séparer les clés par configuration - Disque plein : gonflement DerivedData → purger périodiquement les répertoires distants
${KEY}en LRU, garder les N derniers hash de lockfile
9. FAQ
Plusieurs Mac peuvent-ils partager le même répertoire DerivedData ?
Pas en écriture concurrente. Montage lecture seule ou single-writer/multi-reader possible, mais le linking peut bloquer. En production : DerivedData local + rsync.
Le partage de cache Xcode exige-t-il la même version Xcode ?
Oui. Après mise à niveau, nouvelle clé de cache ; supprimez les anciens répertoires de façon asynchrone.
Comparaison avec le cache distant Bazel / Tuist ?
Bazel/Tuist est plus fin (niveau action) pour les très gros monorepos. Les projets Xcode standard migrent plus facilement via sync DerivedData, compatible xcodebuild.
Quel espace disque pour plusieurs Cloud Mac ?
Réservez 50–100 Go par machine pour DerivedData + SPM en local ; dimensionnez le hub par schemes parallèles × taille cache × versions conservées. M4 + 2 To suffit souvent aux équipes moyennes.
Conclusion
Le partage de cache Xcode répond au « second démarrage à froid » de l’ère multi-Runner : après l’optimisation cache mono-machine, monter en charge ne devrait pas repayer la taxe de compilation complète. Trois couches séparées (Pods / SPM / DerivedData), une discipline de sync (pull-build-push), une règle d’or (jamais d’écriture concurrente sur DerivedData).
Le cache est le multiplicateur de calcul le moins cher des builds distribués — ROI plus rapide que d’acheter deux M4 de plus.
Commencez avec un leader de cache et cache-sync.sh ; nommez les répertoires distants par hash de lockfile ; poussez uniquement quand le build est vert. La courbe P95 de la semaine prochaine dira si ça valait le coup.
Cloud Mac avec marge disque pour CI multi-nœuds
Vuncloud Mac mini M4 : US East, US West et Asie-Pacifique prêts SSH — idéal pour grands volumes DerivedData, hubs rsync et flottes de Runners auto-hébergés.
Pour aller plus loin
- CocoaPods / SPM / DerivedData : guide cache mono-machine
- Golden images et automatisation Cloud Mac
- Environnements de build unifiés pour équipes iOS transfrontalières
- Pourquoi le CI/CD iOS tourne sur Mac mini M4
Le comportement de la toolchain Apple suit les versions officielles ; auditez les identifiants SSH et stockage objet selon la politique de sécurité de votre équipe. Dernière mise à jour : 27 juillet 2026.