Ein iOS-Projekt kann nur wenig Swift-Code enthalten und dennoch Dutzende Objective-C-, C- oder C++-Übersetzungseinheiten mitbringen. Sobald ein Cloud-Mac-Job in einem neuen Arbeitsverzeichnis startet, werden diese nativen Abhängigkeiten erneut mit Clang kompiliert und die Vorteile inkrementeller Builds gehen verloren. ccache setzt genau bei dieser wiederkehrenden Arbeit an: Aus dem Ergebnis des Präprozessors, den Compileroptionen und der Compileridentität wird ein Schlüssel erzeugt. Bei einem Treffer wird die vorhandene Objektdatei direkt wiederverwendet.
ccache ist jedoch kein universeller Schalter zur Beschleunigung von Xcode. Vor der Integration muss feststehen, dass tatsächlich Clang die meiste Zeit beansprucht und nicht das Swift-Frontend, der Linker, die Ressourcenkompilierung oder die Testphase.
Zuerst die Cache-Kandidaten bestimmen
Bewahren Sie das vollständige Build-Protokoll auf und suchen Sie nach CompileC, CompileC normal sowie nach Dateien mit den Endungen .m, .mm, .c, .cc und .cpp. Nur wenn diese Schritte einen nennenswerten Anteil der Build-Zeit ausmachen, lohnt sich der Einsatz von ccache. Bei einem reinen Swift-Projekt kann der Nutzen selbst bei korrekter Konfiguration nahezu null sein.
Erfassen Sie zunächst drei Ausgangswerte:
- Dauer eines vollständigen Builds nach einer Bereinigung;
- Dauer eines weiteren Builds ohne Codeänderungen;
- Anzahl der nativen Übersetzungseinheiten und die langsamsten Targets.
Speichern Sie nicht von Anfang an das gesamte DerivedData-Verzeichnis im Cache. Es enthält Indizes, Modul-Caches, Build-Datenbanken und Zwischenprodukte, die eng an die Xcode-Version, das SDK, absolute Pfade und den Projektgraphen gekoppelt sind. Das ccache-Verzeichnis eignet sich besser als eigenständige und beobachtbare Cache-Einheit.
Weisen Sie zuerst nach, dass der Job Clang wiederholt aufruft, und befassen Sie sich erst danach mit der Trefferquote. Andernfalls werden ein langsamerer Link-Vorgang, die Swift-Typprüfung oder zeitaufwendige Skriptphasen schnell fälschlich als Cache-Fehler eingeordnet.
Clang über Wrapper aufrufen
Ersetzen Sie nach der Installation von ccache in der Build-Umgebung nicht die Dateien des Systemcompilers. Legen Sie stattdessen zwei schlanke Wrapper im Projektarbeitsbereich an und übergeben Sie diese über die Xcode-Build-Einstellungen. Für ein Rollback müssen dann lediglich die Parameter entfernt werden, und andere Jobs auf demselben Knoten bleiben unbeeinflusst.
set -eu
export CCACHE_BIN="$(command -v ccache)"
export CCACHE_DIR="$HOME/.cache/pingmini-ccache"
export CCACHE_BASEDIR="$PWD"
mkdir -p .ci/bin "$CCACHE_DIR"
cat > .ci/bin/clang <<'SH'
#!/bin/sh
exec "${CCACHE_BIN:?CCACHE_BIN is required}" /usr/bin/clang "$@"
SH
cat > .ci/bin/clang++ <<'SH'
#!/bin/sh
exec "${CCACHE_BIN:?CCACHE_BIN is required}" /usr/bin/clang++ "$@"
SH
chmod +x .ci/bin/clang .ci/bin/clang++
ccache --set-config="cache_dir=$CCACHE_DIR"
ccache --set-config="base_dir=$PWD"
ccache --set-config="compiler_check=content"
ccache --set-config="compression=true"
ccache --set-config="max_size=20G"
ccache --zero-stats
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-derivedDataPath "$DERIVED_DATA" \
build \
CC="$PWD/.ci/bin/clang" \
CPLUSPLUS="$PWD/.ci/bin/clang++"
ccache --show-stats
CC ist für C und Objective-C zuständig, CPLUSPLUS für C++ und Objective-C++. Der Linker darf nicht über einen Wrapper aufgerufen werden, und der Wrapper darf sich nicht als Swift-Compiler ausgeben. Prüfen Sie nach der Ausführung im ausführlichen Protokoll, ob tatsächlich .ci/bin/clang aufgerufen wurde. Die bloße Installation von ccache belegt noch nicht, dass Xcode es verwendet.
Cache-Schlüssel über Jobs hinweg stabil halten
CI-Systeme checken denselben Commit häufig in Verzeichnisse aus, deren Pfad eine Jobnummer enthält. Obwohl der Quellcode unverändert ist, unterscheiden sich dadurch die absoluten Pfade. Das kann Debug-Informationen, Headerpfade und Compileroptionen verändern. CCACHE_BASEDIR und base_dir schreiben Pfade innerhalb des Arbeitsbereichs in relative Pfade um und bilden damit die Grundlage für eine Wiederverwendung über mehrere Jobs hinweg.
Die folgenden Eingaben müssen dennoch stabil gehalten werden:
Toolchain und Build-Parameter festlegen
Der Cache-Schlüssel sollte die Xcode-Hauptversion, das SDK, die Zielarchitektur, den Konfigurationsnamen und eine Prüfsumme der Lockdatei enthalten. Bei einem Toolchain-Upgrade sollte bewusst ein neuer Cache-Namensraum verwendet werden. Das lässt sich leichter überprüfen, als alte und neue Objekte im selben Verzeichnis zu vermischen.
Derselbe Cache darf nicht gleichzeitig für Debug und Release verwendet werden. Ebenso sollten unterschiedliche Architekturen keinen künstlich vereinfachten Schlüssel teilen. ccache erkennt zwar Unterschiede in den Parametern, doch ein zu breit gefasster Persistenzschlüssel der äußeren Cache-Ebene führt zu vielen unnötigen Downloads und Verdrängungen.
Generierte Dateien kontrollieren
Ein generierter Header, in den bei jedem Lauf die aktuelle Zeit, eine zufällige Kennung oder ein temporärer absoluter Pfad geschrieben wird, invalidiert sämtliche davon abhängigen Übersetzungseinheiten. Generierungsskripte sollten Dateien nur überschreiben, wenn sich deren Inhalt tatsächlich geändert hat. Dazu wird zunächst eine temporäre Datei verglichen und anschließend atomar ersetzt:
generate_config > "$TMPDIR/build-config.new"
if ! cmp -s "$TMPDIR/build-config.new" Generated/build-config.h; then
mv "$TMPDIR/build-config.new" Generated/build-config.h
else
rm "$TMPDIR/build-config.new"
fi
Vermeiden Sie im Quellcode Zeitmakros, deren Wert sich bei jeder Kompilierung ändert. Wenn die Anwendung tatsächlich die Build-Zeit benötigt, legen Sie sie in einer separaten Datei ab, damit möglichst wenige Übersetzungseinheiten betroffen sind.
Cache-Treffer mit zwei Builds prüfen
Bei der ersten Integration sollte die Abnahme in zwei voneinander isolierten Durchläufen erfolgen. Setzen Sie im ersten Durchlauf die Statistiken zurück, lassen Sie den leeren Cache jedoch bestehen und führen Sie einen vollständigen Build aus. Löschen Sie vor dem zweiten Durchlauf DerivedData, behalten Sie ccache bei und bauen Sie mit exakt derselben Toolchain und denselben Parametern erneut. Erst direkte Treffer im zweiten Durchlauf belegen, dass die Wiederverwendung nicht vom alten Build-Verzeichnis abhängt.
Prüfen Sie cacheable calls, hits, misses und uncacheable calls, anstatt nur die gesamte Trefferquote zu betrachten. Beim ersten Build sind überwiegend Misses zu erwarten. Falls auch der zweite Durchlauf ausschließlich Misses liefert, gehen Sie in dieser Reihenfolge vor:
- Prüfen Sie im Build-Protokoll, ob der Wrapper aufgerufen wurde;
- vergleichen Sie Arbeitsbereich, SDK, Architektur und Compileroptionen beider Durchläufe;
- prüfen Sie, ob sich generierte Header bei jedem Lauf ändern;
- suchen Sie nach Zeitmakros, Zufallswerten oder absoluten Pfaden;
- vergewissern Sie sich, dass das Cache-Verzeichnis nach Abschluss des Jobs tatsächlich gespeichert wird.
Wenn viele uncacheable calls auftreten, reproduzieren Sie mithilfe des ccache-Debug-Protokolls nur eine einzelne Übersetzungseinheit. So vermeiden Sie enorme Mengen an Diagnosedateien für den gesamten Build. Deaktivieren Sie das Debugging nach der Ursachenanalyse wieder, damit nicht die Protokolle selbst den Datenträger füllen.
Grenzen für Bereinigung und Rollback festlegen
Die Cache-Obergrenze sollte sich nach dem Umfang der nativen Targets richten und nicht unbegrenzt wachsen. Führen Sie regelmäßig ccache --cleanup aus und überwachen Sie die Verzeichnisgröße, die Anzahl der Verdrängungen sowie fehlgeschlagene Schreibvorgänge. Mehrere parallele Jobs können dasselbe Verzeichnis gemeinsam nutzen, es muss jedoch auf einem zuverlässigen lokalen Dateisystem liegen. Bei instabilen Remote-Mounts kann der Zugriff auf Metadaten die eingesparte Kompilierzeit vollständig aufzehren.
Wenn eine fehlerhafte Wiederverwendung vermutet wird, bewahren Sie zunächst die Protokolle und Cache-Statistiken auf. Führen Sie anschließend denselben Befehl mit CCACHE_DISABLE=1 erneut aus. Verschwindet das Problem, grenzen Sie den Cache nach Toolchain, Target und Konfiguration enger ein. Deaktivierte Prüfungen, großzügigere Zeitstempelregeln oder das Ignorieren von Headeränderungen sind keine dauerhafte Lösung.
Entscheidend für die abschließende Abnahme ist keine möglichst eindrucksvolle Trefferquote, sondern das gleichzeitige Erfüllen von drei Bedingungen: Die Artefakte sind mit und ohne Cache identisch, im zweiten Durchlauf werden deutlich weniger native Dateien kompiliert, und Cache-Invalidierungen lassen sich durch Änderungen an der Toolchain oder den Eingaben erklären. Erst dann ist ccache eine wartbare Schicht der Build-Infrastruktur.
Häufig gestellte Fragen
Beschleunigt ccache auch die Swift-Kompilierung?
Nein. ccache wirkt hier auf Clang-Aufrufe für Objective-C, Objective-C++, C und C++. Für Swift bleiben ein stabiles DerivedData-Verzeichnis, passende Parallelität und die Xcode-eigene inkrementelle Kompilierung entscheidend.
Warum zeigt der zweite Build weiterhin keine Treffer?
Zuerst prüfen, ob Xcode wirklich den Wrapper aufruft. Danach Arbeitsverzeichnisse, Compiler-Argumente, generierte Header und wechselnde Makros vergleichen; jede dieser Abweichungen kann den Cache-Schlüssel verändern.
Soll die CI DerivedData oder nur den ccache-Ordner sichern?
Der ccache-Ordner ist meist die robustere eigenständige Cache-Einheit. DerivedData ist enger an Xcode-Version, absoluten Pfad und Projektzustand gebunden und benötigt deshalb strengere Schlüssel.
Exklusiven physischen Mac für den nächsten Build vorbereiten
Wählen Sie aus drei festen Konfigurationen den passenden Cloud Mac für Entwicklung, Continuous Integration oder lokale Modelltests und richten Sie ihn passend zur Dauer Ihres Projekts ein.