Vuncloud Blog
← Retour aux Dev Notes

Partage de cache Xcode en pratique : synchroniser les données de build sur plusieurs nœuds

DerivedData · SPM · CocoaPods · sync multi-runners~13 min de lecture

Tableau de bord analytique — symbolise le monitoring et l'efficacité de sync du cache de build Xcode multi-nœuds
TL;DR · Trois phrases
  • 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.

3
couches de cache : dépendances · artefacts · index
5–15 min
économie typique supplémentaire des builds à chaud multi-nœuds
1
règle d’or : jamais d’écriture concurrente sur le même DerivedData

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.

Éditeur de code sur un MacBook, symbole de plusieurs Runners partageant le même cache de compilation Xcode
Vous partagez des modules compilés — pas un projet ouvert sur chaque machine. Chaque Runner build toujours depuis une copie locale de DerivedData.

3. Quatre architectures de sync multi-nœuds

ModèleApprocheAvantagesRisques
Local uniquementChaque Runner a son disque, pas de partageZéro opsLa montée en charge horizontale ne gagne pas de temps
Montage NFS partagéDerivedData sur volume réseau, lecture seule ou lecture-écritureSetup simpleÉcritures concurrentes corrompent facilement ; latence réseau ralentit le linking
Hub rsyncRépertoire central ou machine leader ; pull avant build, push après succèsContrôlable, bonne compatibilité XcodeScripts et verrous requis
Tarball stockage objetEmpaqueter et envoyer vers S3/R2 par clé de cache ; télécharger et extraire au démarrageInter-régions, proche du modèle cache GHAGros 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.

Règle d’or

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 -version ou variable d’environnement XCODE_VERSION
  • hashFiles('**/Podfile.lock') ou Package.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 :

  1. Début du job → cache-sync.sh pull
  2. xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build
  3. 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 hash Podfile.lock + cache SPM clé par Package.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 corrupted alé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/Products du 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.

Voir les offres Cloud Mac · Modèle de coût Runner CI 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.

Dev Notes · Accélération build

Sync DerivedData · multi-runners · Cloud Mac

hub rsync · clé de cache · buckets régionaux

Voir les offres Cloud Mac
Offre limitée Voir les forfaits