Wenn auf demselben Cloud-Mac nacheinander Abhängigkeitsupdates, Projektmigrationen und destruktive Skripte ausgeführt werden, ist meist nicht der Build selbst das größte Problem. Schwieriger ist es, zuverlässig zu einem sauberen Arbeitsbereich zurückzukehren. Beim erneuten Auschecken eines großen Repositorys müssen zahlreiche kleine Dateien nochmals gelesen werden. Wird dagegen ein altes Verzeichnis wiederverwendet, können nicht verfolgte Dateien, Build-Artefakte und falsche Berechtigungen erhalten bleiben. Liegt das Arbeitsverzeichnis auf einem APFS-Volume, lässt sich zunächst eine geprüfte Basisversion pflegen und anschließend per Copy-on-Write-Klon für jeden Auftrag eine veränderbare Kopie erstellen.
Welches Problem APFS-Klone lösen
Bei einem APFS-Klon verwenden zwei Dateien anfangs dieselben zugrunde liegenden Datenblöcke. Erst wenn eine der Dateien geändert wird, weist das Dateisystem den betroffenen Bereichen neue Blöcke zu. Eine Basisversion mit Quellcode, Abhängigkeiten und statischen Werkzeugen lässt sich dadurch vergleichsweise schnell in ein eigenständiges Verzeichnis kopieren, ohne dass spätere Änderungen in die Basisversion zurückgeschrieben werden.
Das unterscheidet Klone von Hardlinks: Geklonte Dateien besitzen eigene Verzeichniseinträge, und Änderungen an der Kopie wirken sich nicht auf die Originaldatei aus. Ein Klon ist jedoch auch kein Backup, da sich Basisversion und Kopie weiterhin auf demselben Volume befinden. Bei einem beschädigten Volume, einer versehentlich gelöschten Basisversion oder einem vollständigen Datenverlust des Rechners ermöglichen Klone keine Wiederherstellung von einem anderen Speicherort.
APFS-Klone sollten als Mechanismus zum kostengünstigen Erstellen von Arbeitskopien verstanden werden, nicht als Garantie für Cache-Treffer, Snapshot-System oder Datenschutzlösung.
du -sh zeigt die logische Größe eines Verzeichnisses an, sagt aber nicht unmittelbar aus, wie viel zusätzlicher physischer Speicher durch einen Klon belegt wird. Um die Speicherentwicklung zu überwachen, sollte zugleich der freie Speicherplatz des APFS-Volumes geprüft und für länger schreibende Aufträge ein Schwellenwert zur Bereinigung festgelegt werden.
Zunächst eine überprüfbare Basisversion erstellen
Die Basisversion sollte nicht einfach aus einem Verzeichnis bestehen, das nach irgendeinem Build übrig geblieben ist. Checken Sie zuerst einen festen Commit aus, stellen Sie die Abhängigkeiten wieder her und entfernen Sie anschließend sämtlichen Zustand, der nur zu einem einzelnen Auftrag gehört. Prüfen Sie mindestens, ob der Arbeitsbaum sauber ist und die Submodule auf den richtigen Revisionen stehen, und protokollieren Sie den Commit der Basisversion.
set -euo pipefail
ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
mkdir -p "$ROOT"
git -C "$BASE" status --porcelain
git -C "$BASE" submodule status --recursive
git -C "$BASE" rev-parse HEAD > "$BASE/.baseline-revision"
test -z "$(git -C "$BASE" status --porcelain)"
Da .baseline-revision selbst zu einer nicht verfolgten Datei wird, sollte sie außerhalb des Repositorys abgelegt oder vorab in eine vom Team freigegebene Ignorierregel aufgenommen werden. Robuster ist eine Verzeichnisstruktur, bei der $ROOT/metadata die Commit-ID speichert und $BASE die Prüfung auf einen sauberen Arbeitsbaum jederzeit besteht.
Welche Inhalte nicht in die Basisversion gehören
DerivedData, Testergebnispakete, Archive, temporäre Schlüsselbunde, Laufzeitprotokolle und aktive Sockets sollten nicht Teil der Basisversion sein. Solche Inhalte enthalten häufig absolute Pfade, Prozesszustände oder auftragsspezifische Zugangsdaten. Ob Abhängigkeitsverzeichnisse aufgenommen werden können, hängt davon ab, ob sich ihr Inhalt eindeutig aus Lockdateien ableiten lässt und ob Installationsskripte in systemweite Pfade schreiben.
Außerdem sollte ausschließlich der Aktualisierungsauftrag Schreibzugriff auf die Basisversion haben. Reguläre Build-Konten dürfen sie lesen, sollten darin jedoch keine Build-Befehle ausführen. Andernfalls kann ein einziger Bedienfehler sämtliche späteren Klone verunreinigen.
Für jeden Auftrag einen eigenen Klon erstellen
Prüfen Sie zunächst, ob sich das Stammverzeichnis tatsächlich auf APFS befindet, und erzeugen Sie anschließend anhand der Auftrags-ID ein eindeutiges Verzeichnis. Der Zielpfad darf noch nicht existieren. Außerdem muss die Auftrags-ID auf sichere Zeichen beschränkt werden, damit externe Eingaben nicht ungeprüft in einen Löschpfad gelangen.
set -euo pipefail
ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
JOB_ID="${BUILD_ID:?BUILD_ID is required}"
case "$JOB_ID" in
*[!A-Za-z0-9._-]*) exit 64 ;;
esac
test "$(stat -f %T "$ROOT")" = "apfs"
WORK="$ROOT/jobs/$JOB_ID"
DERIVED="$ROOT/derived/$JOB_ID"
test ! -e "$WORK"
mkdir -p "$ROOT/jobs" "$DERIVED"
cp -cR "$BASE" "$WORK"
xcodebuild \
-workspace "$WORK/App.xcworkspace" \
-scheme App \
-derivedDataPath "$DERIVED" \
build
cp -cR fordert eine Klonkopie an, das tatsächliche Verhalten hängt jedoch weiterhin von Quellpfad, Zielpfad und Dateisystem ab. Quell- und Zielverzeichnis müssen sich auf demselben APFS-Volume befinden. Bei einer volumenübergreifenden Ausführung darf der Vorgang nicht als Copy-on-Write-Ablauf betrachtet werden. Vor der Bereitstellung lässt sich mit einem Testverzeichnis, das eine große Datei enthält, die Kopierdauer und die Veränderung des belegten Volume-Speichers beobachten.
Parallele Aufträge und veränderlichen Zustand isolieren
Ein eigener Arbeitsbereich bedeutet nicht, dass damit bereits sämtlicher Zustand isoliert ist. Build-Werkzeuge können weiterhin Caches, temporäre Verzeichnisse und Konfigurationsdateien im Benutzerverzeichnis lesen und verändern. Mindestens DerivedData und das Ergebnisverzeichnis sollten für jeden Auftrag separat angelegt werden. Mehrere Aufträge sollten außerdem weder denselben Satz von Simulatorgeräten noch denselben Ausgabepfad verwenden.
Ein Speicherbudget für parallele Aufträge festlegen
Direkt nach dem Erstellen belegt ein Klon nur wenig zusätzlichen Speicher. Kompilierte Objekte, neu geschriebene Abhängigkeiten und Archive erzeugen jedoch schnell neue Blöcke. Die Obergrenze für parallele Aufträge sollte daher anhand des größten zu erwartenden beschreibbaren Zuwachses festgelegt werden, nicht anhand der logischen Größe der Basisversion. Vor dem Start eines Auftrags kann der freie Volume-Speicher geprüft und nach Abschluss des Builds die Veränderung protokolliert werden:
df -h "$ROOT"
du -sh "$DERIVED" "$WORK"
Wenn ein Auftrag große Teile der Abhängigkeitsdateien neu schreibt, nimmt der Speichervorteil des Klonens schrittweise ab. In diesem Fall sollten unveränderliche Abhängigkeiten in der Basisversion verbleiben, während häufig veränderte generierte Verzeichnisse in auftragsspezifische Pfade ausgelagert werden. Beschreibbare Caches sollten nicht nur deshalb gemeinsam genutzt werden, um einen hohen Klonanteil aufrechtzuerhalten.
Sicher bereinigen und die Basisversion regelmäßig neu erstellen
Das wichtigste Ziel eines Bereinigungsskripts ist nicht, möglichst schnell zu löschen, sondern unter keinen Umständen das Stammverzeichnis der Aufträge zu verlassen. Vor dem Löschen müssen Präfix, Existenz des Verzeichnisses und Auftrags-ID gemeinsam geprüft werden. Eine möglicherweise leere Variable darf niemals ungeprüft an einen rekursiven Löschbefehl übergeben werden.
set -euo pipefail
ROOT="$HOME/ci-workspaces"
WORK="$ROOT/jobs/${BUILD_ID:?BUILD_ID is required}"
DERIVED="$ROOT/derived/${BUILD_ID:?BUILD_ID is required}"
case "$WORK" in
"$ROOT"/jobs/*) rm -rf -- "$WORK" ;;
*) exit 64 ;;
esac
case "$DERIVED" in
"$ROOT"/derived/*) rm -rf -- "$DERIVED" ;;
*) exit 64 ;;
esac
Aktualisierungen der Basisversion sollten in einem neuen Verzeichnis erfolgen: Checken Sie den gewünschten Commit aus, installieren Sie deterministisch festgelegte Abhängigkeiten, führen Sie die Abnahmeprüfung durch und ersetzen Sie erst danach die aktuelle Basisversion. Eine vorhandene Basisversion sollte nicht direkt aktualisiert und anschließend weiterverwendet werden, da ein unterbrochenes Update einen nicht eindeutig beurteilbaren Mischzustand aus alten und neuen Dateien hinterlassen kann.
Die abschließende Prüfung umfasst vier Punkte: Der Commit der Basisversion ist nachvollziehbar und der Arbeitsbaum bleibt sauber; jeder Auftrag besitzt einen eindeutigen Arbeitsbereich und ein eigenes Verzeichnis für generierte Daten; der freie Volume-Speicher reicht für die maximalen parallelen Schreibvorgänge aus; und die Bereinigungslogik akzeptiert ausschließlich kontrollierte Pfade. Erst wenn diese Bedingungen erfüllt sind, werden APFS-Klone zu einem reproduzierbaren technischen Prozess statt zu einem Kopiertrick, der nur auf den ersten Blick schnell wirkt.
Häufig gestellte Fragen
Ersetzt ein APFS-Klon den Build-Cache oder eine Sicherung?
Nein. Er erstellt auf demselben APFS-Volume schnell eine beschreibbare Kopie, doch geänderte Blöcke belegen zusätzlichen Platz. Wichtige Daten benötigen weiterhin eine unabhängige Sicherung.
Warum sollte DerivedData nicht Teil der Basis sein?
DerivedData enthält absolute Pfade, Indizes und auftragsspezifischen Zustand. Ein separates Verzeichnis pro Build verbessert Isolation, Reproduzierbarkeit und Fehlersuche.
Diesen Workflow auf einem Cloud-Mac weiter validieren
Wählen Sie aus drei Apple-Silicon-Konfigurationen die passende Kombination aus Arbeitsspeicher, Speicher, Knoten und Mietdauer.