- DerivedData-Cache auf einem Mac existiert auf einem zweiten Runner standardmäßig nicht — die versteckten Kosten von Multi-Node-iOS-CI sind, dass jede Maschine eigenständig kalt startet
- Teilen lässt sich die Build-Artefakt-Schicht (DerivedData, SPM-Auflösung, Pods/) — nicht „mehrere Maschinen schreiben gleichzeitig in dasselbe Verzeichnis“; pull → build → push oder ein regionaler Cache-Hub ist 2026 der Mainstream
- Auf einer Cloud-Mac-Flotte: Lockfile-Hash + Xcode-Version als Cache-Keys mit rsync/Object Storage — warme Builds sparen oft weitere 5–15 Minuten (zusätzlich zur Einzelmaschinen-Optimierung)
Sie haben GitHub Actions actions/cache konfiguriert und warme Builds sind von 18 auf 9 Minuten gesunken — dann skalieren Sie auf drei selbst gehostete M4-Runner und P95 kriecht wieder auf 16 Minuten. Jede Maschine „sieht diesen Commit zum ersten Mal“.
Das ist kein Rückschritt auf null, sondern Phase zwei von Multi-Node-Builds: Xcode-Cache-Sharing. Dieser Artikel behandelt nicht „warum Cache“ (siehe den CocoaPods-/SPM-/DerivedData-Einzelmaschinen-Leitfaden), sondern wie Build-Daten zwischen mehreren Macs synchronisiert werden, damit jeder Runner Module wiederverwenden kann, die ein Teamkollege bereits kompiliert hat.
1. Warum Multi-Node-CI langsamer ist als Einzelmaschine
Wenn Load Balancing Jobs zufällig Runner A/B/C zuweist, ist der Festplattenzustand jeder Maschine unabhängig. MyAppKit.swiftmodule, das Runner A gerade kompiliert hat, nützt Runner B nichts — es sei denn, Sie übertragen die Artefakte.
GitHub-gehostete Runner nutzen actions/cache, um Tarballs in der Cloud zu speichern und beim Jobstart wiederherzustellen. Selbst gehostete oder Cloud-Mac-Flotten haben oft keine vergleichbare verwaltete Schicht, daher wählen Teams entweder:
- Pro-Maschinen-Aufwärmung zu akzeptieren (verschwendete Rechenzeit)
- Cache-Verzeichnisse per NFS mit gleichzeitigem Schreiben zu mounten (Korruptionsrisiko)
- Einen Cache-Hub aufzubauen: pull vor dem Build, push nach Erfolg
Die dritte Option ist bei iOS-Teams am verbreitetsten und passt zu Multi-Region-Node-Deployments: drei M4 in US East teilen einen Cache-Bucket, zwei in Asien-Pazifik einen anderen, und nur die SPM-/Pods-Schicht wird regionsübergreifend synchronisiert.
2. Schichten der Xcode-Build-Daten
Vor dem Sync trennen Sie, was sich zu übertragen lohnt, von dem, was nicht gemischt werden darf.
2.1 DerivedData
Das Verzeichnis, auf das xcodebuild -derivedDataPath zeigt, mit kompilierten .o, .swiftmodule, Link-Zwischenprodukten und Teilindizes. Das Volumen erreicht oft mehrere GB — es ist der größte Hebel für Warm-Build-Geschwindigkeit und die Schicht, die am empfindlichsten auf die Xcode-Version reagiert.
Festen Pfad in CI setzen, z. B. /Volumes/CI/DerivedData/<scheme>, passend zur Variable REMOTE im Sync-Skript — der Einzelmaschinen-Cache-Artikel betonte, dass abweichende Pfade keinen Cache bedeuten.
2.2 SPM und CocoaPods
SPM: ~/Library/Caches/org.swift.swiftpm + im Repo .build (falls Sie Auflösungsoutput committen). CocoaPods: Pods/ und ~/Library/Caches/CocoaPods.
Diese Schichten ändern sich seltener als DerivedData (sie folgen dem Lockfile) und eignen sich für maschinen- und sogar regionsübergreifenden Sync. Viele Teams pushen nur SPM/Pods nach S3 und halten DerivedData regional per rsync.
2.3 ModuleCache und globale Xcode-Caches
~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex und diverse SDKStatCaches. Werden meist zusammen mit DerivedData behandelt; bei separatem Sync gilt dieselbe Xcode-Minor-Version.
3. Vier Multi-Node-Sync-Architekturen
| Muster | Ansatz | Vorteile | Risiken |
|---|---|---|---|
| Nur lokal | Jeder Runner hat unabhängige Platte, kein Sharing | Null Ops | Horizontale Skalierung spart keine Zeit |
| NFS-Shared-Mount | DerivedData auf Netzwerkvolume, read-only oder read-write | Einfaches Setup | Gleichzeitiges Schreiben korrumpiert leicht; Netzwerklatenz bremst Linking |
| rsync-Hub | Zentrales Verzeichnis oder Leader-Maschine; pull vor Build, push nach Erfolg | Kontrollierbar, gute Xcode-Kompatibilität | Skripte und Locking nötig |
| Object-Storage-Tarball | Nach Cache-Key packen und nach S3/R2 hochladen; beim Jobstart entpacken | Regionsübergreifend, ähnlich GHA-Cache-Modell | Große DerivedData-Uploads sind langsam; Kompression nötig |
In der Praxis 2026 bevorzugen Cloud-Mac-Flotten im selben Rechenzentrum / derselben Region rsync-Hubs; globale Teams nutzen Object Storage für SPM/Pods und halten DerivedData regional. Behandeln Sie NFS nicht als Allheilmittel für Multi-Writer-Shared-DerivedData.
Niemals xcodebuild auf zwei Runnern gleichzeitig gegen denselben DerivedData-Baum laufen lassen. Wenn Sie ein Volume teilen müssen: Dateisperren oder Single-Writer-Multi-Reader (ein designierter Warmer baut, dann verteilt rsync).
4. Cache-Keys und Invalidierung
Multi-Node-Shared-Cache-Keys sollten konservativer sein als auf Einzelmaschinen — ein falscher Treffer ist schlimmer als ein Kaltstart (rätselhafte Link-Fehler, Signing-Ausfälle).
Felder im Key:
xcodebuild -versionoder UmgebungsvariableXCODE_VERSIONhashFiles('**/Podfile.lock')oderPackage.resolved- Scheme-Name oder Target-Set (getrennte Caches für Multi-Scheme-Repos)
arm64(nur Apple Silicon — vermeidet Legacy-x86-Verschmutzung)
Nicht nur den vollen github.sha als Key — sonst verfehlt jede Maschine immer. Geschichtete restore-keys: dd-arm64-main-<lockhash> → dd-arm64-main- → dd-arm64-.
Invalidieren bei: Xcode-Upgrade, Lockfile-Änderung, SWIFT_VERSION oder größere Build-Settings-Änderung, oder nach manuellem clean build — Key-Suffix erhöhen oder Remote-Verzeichnis löschen.
5. Praxis: rsync pull/push-Skript
Unten ein minimales Skript, validiert auf einer M4-Cloud-Mac-Flotte. Der Cache-Hub liegt unter cache-leader.internal:/cache/ios/ (Maschine mit großer Platte in der Flotte oder 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
CI-Ablauf:
- Jobstart →
cache-sync.sh pull xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build- Nur bei erfolgreichem Build →
cache-sync.sh push
Bei parallelen Maschinen kann push kollidieren — flock nutzen oder last-write-wins akzeptieren; DerivedData unter gleichem Lockfile + Xcode sollte kompatibel sein. Bei sehr häufigem Push auf asynchronen Upload wechseln (rsync im Hintergrund nach dem Build, ohne Pipeline zu blockieren).
6. Regionale Cloud-Mac-Cache-Hubs
Wenn Runner in US East, US West und Asien-Pazifik stehen, lohnt sich oceansübergreifendes rsync voller DerivedData oft nicht (Latenz + Egress). Empfohlener Ansatz:
- Ein Cache-Bucket pro Region (S3 / R2 / Provider-Object-Storage)
- Sync-Inhalt:
Pods/-Tarball nachPodfile.lock-Hash + SPM-Cache nachPackage.resolved - DerivedData nur innerhalb der Region per rsync; erster Job kalt, folgende warm
- Image-Versionen mit Golden-Image-Tags abgleichen, um Xcode-Drift zu vermeiden, die die ganze Bibliothek invalidiert
SPM-Cache vor Upload mit tar czf komprimieren — oft 40 %–60 % kleiner; DerivedData besteht bereits aus vielen kleinen Dateien, daher ist tar + paralleler Upload stabiler als rohes regionsübergreifendes rsync.
7. Anbindung an GitHub Actions / Self-hosted Runner
Auf Self-hosted Runnern den Workflow so wrappen:
- 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
Nutzen Sie weiterhin GitHub-gehostetes actions/cache, können Self-hosted-Flotten dual track fahren: GHA-Cache als Fallback, rsync-Hub als latenzarme Same-Region-Schicht. Beide Key-Namespaces getrennt halten, um Überschreiben zu vermeiden.
Monitoring: pull/push-Dauer, übertragene Bytes und ob der Build inkrementell war (CompileSwift-Zeilen in xcodebuild-Logs zählen). Wenn P95 nachlässt, zuerst Cache-Miss prüfen, dann Maschinenlast — dasselbe Playbook wie beim Troubleshooting schwankender Build-Zeiten.
8. Fallstrick-Checkliste
- Gleichzeitiges DerivedData-Schreiben: zufällige
stat cache file corrupted→ auf pull/push wechseln - Abweichende Xcode-Minor-Versionen: Modul-ABI-Mismatch → Key enthält Version, Images teilen einheitlichen Tag
- Pfad-Mismatch: Cache unter Pfad A, Kompilierung unter Pfad B → Voll-Rebuild
- Push nach clean: leere Verzeichnisse überschreiben guten Cache → push nur bei Erfolg und nicht nach clean
- Signierte Produkte im Cache: veraltetes Signing nach Provisioning-Änderungen →
Build/Productsvom Cache ausschließen oder Keys nach Konfiguration trennen - Platte voll: DerivedData-Aufblähung → Remote-
${KEY}-Verzeichnisse periodisch per LRU bereinigen, letzte N Lockfile-Hashes behalten
9. FAQ
Können mehrere Macs dasselbe DerivedData-Verzeichnis direkt teilen?
Nicht mit gleichzeitigem Schreiben. Read-only-Mounts oder Single-Writer/Multi-Reader können funktionieren, aber Linking kann trotzdem sperren. Produktions-Setups bevorzugen lokales DerivedData plus rsync.
Braucht Xcode-Cache-Sharing dieselbe Xcode-Version?
Ja. Nach Xcode-Upgrade neuen Cache-Key nutzen; alte Verzeichnisse asynchron löschen.
Vergleich mit Bazel / Tuist Remote Cache?
Bazel/Tuist Remote Cache ist feiner (Action-Ebene) und passt zu riesigen Monorepos. Standard-Xcode-Projekte haben geringere Migrationskosten durch DerivedData-Sync und bleiben mit bestehenden xcodebuild-Flows kompatibel.
Wie viel Speicher brauchen mehrere Cloud Macs?
Lokal mindestens 50–100 GB pro Maschine für DerivedData + SPM; Hub oder Object Storage nach parallelen Schemes × Cache-Größe pro Scheme × behaltenen Versionen dimensionieren. M4 + 2 TB Erweiterung reicht meist für mittlere Teams.
Fazit
Xcode-Cache-Sharing adressiert den „zweiten Kaltstart“ der Multi-Runner-Ära: Nach Einzelmaschinen-Cache-Optimierung sollte horizontale Skalierung nicht wieder die volle Compile-Steuer zahlen. Drei getrennte Schichten (Pods / SPM / DerivedData), eine Sync-Disziplin (pull-build-push) und ein eisernes Gesetz (niemals gleichzeitig in DerivedData schreiben).
Cache ist der günstigste Compute-Multiplikator in verteilten Builds — schnellerer ROI als zwei weitere M4 zu kaufen.
Starten Sie mit einem Cache-Leader und cache-sync.sh; benennen Sie Remote-Verzeichnisse nach Lockfile-Hash; pushen Sie nur bei grünem Build. Die P95-Kurve nächste Woche zeigt, ob es sich gelohnt hat.
Cloud Mac mit Speicher für Multi-Node-CI
Vuncloud Mac mini M4: US East, US West und Asien-Pazifik per SSH — ideal für große DerivedData-Volumes, rsync-Cache-Hubs und Self-hosted-Runner-Flotten.
Weiterführend
- CocoaPods / SPM / DerivedData: Einzelmaschinen-Cache-Leitfaden
- Golden Images und Cloud-Mac-Automatisierung
- Einheitliche Build-Umgebungen für grenzüberschreitende iOS-Teams
- Warum iOS CI/CD auf Mac mini M4 läuft
Das Verhalten der Apple-Toolchain folgt offiziellen Releases; prüfen Sie SSH- und Object-Storage-Credentials gemäß der Sicherheitsrichtlinie Ihres Teams. Zuletzt aktualisiert: 27. Juli 2026.