Wenn derselbe Cloud-Mac mehrere CI-Jobs nacheinander ausführt, sind die am schwersten reproduzierbaren Fehler häufig keine Codefehler, sondern Rückstände des vorherigen Jobs. Veraltete Sockets, nicht freigegebene Sperren, nur teilweise geschriebene Caches oder gleichnamige Exportverzeichnisse können dazu führen, dass derselbe Commit beim nächsten Build ein anderes Ergebnis liefert. Ein dedizierter physischer Knoten verhindert Störungen durch andere Konten, isoliert jedoch nicht automatisch parallel laufende Jobs desselben Kontos. Deshalb muss die Pipeline die Grenzen im Dateisystem ausdrücklich festlegen.
Verzeichnisse mit jobübergreifenden Rückständen identifizieren
Ein geleerter Workspace bedeutet nicht, dass die gesamte Umgebung zurückgesetzt wurde. macOS-Werkzeuge verteilen ihren Zustand auch außerhalb des Workspace. Bei der Fehlersuche sollten mindestens die folgenden vier Bereiche geprüft werden:
- temporäre Dateien, Sockets und Downloadfragmente unter
TMPDIR; - DerivedData, Indizes und Build-Datenbanken von Xcode;
- der Clang-Modulcache sowie jobspezifische SwiftPM-Caches;
- von Skripten angelegte Verzeichnisse für Exporte, Protokolle und Testanhänge.
Führe vor und nach einem regulären Build jeweils env | sort aus und protokolliere die tatsächlich verwendeten Pfade für HOME, TMPDIR und den Workspace. Vergleiche anschließend mit du -sh die Größe der relevanten Verzeichnisse, statt sofort alles rekursiv zu löschen. Wenn ein Fehler nur bei paralleler Ausführung auftritt, sollte zuerst nach fest vorgegebenen Dateinamen, Ports und Ausgabepfaden gesucht werden.
Ziel der Isolation ist nicht, bei jedem Lauf sämtliche Caches zu löschen. Jeder Job soll vielmehr eindeutig eigene beschreibbare Verzeichnisse besitzen und ausschließlich die von ihm selbst erzeugten Inhalte bereinigen.
Für jeden Job ein eindeutiges Stammverzeichnis erstellen
Die Job-ID sollte aus einer vom CI-System bereitgestellten eindeutigen Nummer stammen. Ist keine verlässliche ID verfügbar, kann sie mit mktemp erzeugt werden. Verwende nicht nur den Branch-Namen, da derselbe Branch mehrere Builds gleichzeitig auslösen kann.
#!/bin/bash
set -euo pipefail
job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"
export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"
mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"
cleanup() {
local status=$?
rm -rf "$job_root"
exit "$status"
}
trap cleanup EXIT INT TERM
Das von mktemp erstellte Verzeichnis kollidiert standardmäßig nicht mit dem eines anderen Jobs. Die Bereinigungsfunktion speichert zuerst den Exit-Code, löscht anschließend das Verzeichnis und beendet den Prozess mit demselben Status. So wird ein fehlgeschlagener Test nicht durch den Bereinigungsbefehl verdeckt. Pfadvariablen müssen in Anführungszeichen stehen, damit Workspace-Namen mit Leerzeichen nicht in mehrere Argumente aufgeteilt werden.
HOME nicht ohne Prüfung überschreiben
HOME auf das Jobverzeichnis umzuleiten wirkt zunächst wie eine vollständige Isolation, kann aber den Zugriff auf den Schlüsselbund des aktuellen Benutzers, Werkzeugeinstellungen und bereits abgeschlossene Systeminitialisierungen unterbrechen. Für gewöhnliche Builds lassen sich einzelnen Paketmanagern separate Caches zuweisen. Bei Signiervorgängen sollte HOME in der Regel unverändert bleiben; isoliert werden nur Build-Artefakte und Caches, die nachweislich verschoben werden können.
Schreibpfade von Xcode explizit anbinden
Allein durch das Setzen von TMPDIR wird DerivedData nicht automatisch verschoben. Der Build-Befehl sollte die entscheidenden Pfade als Parameter übergeben, damit aus den Protokollen unmittelbar hervorgeht, welche Verzeichnisse der aktuelle Job verwendet.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
COMPILER_INDEX_STORE_ENABLE=NO \
CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
test \
-resultBundlePath "$RESULT_BUNDLE"
Wenn die CI keine Codeindizierung benötigt, kann der Index Store deaktiviert werden, um unnötige Schreibvorgänge zu reduzieren. Testergebnisse sollten im Stammverzeichnis des Jobs liegen. Sollen nach einem Fehlschlag Anhänge hochgeladen werden, muss die Archivierung jedoch vor der Bereinigung abgeschlossen sein. Robuster ist folgender Ablauf: Tests ausführen, Diagnosedateien in den Artefaktbereich der Pipeline kopieren, das Kopierergebnis prüfen und anschließend den Prozess beenden.
Gemeinsame Caches schreibgeschützt, Job-Caches beschreibbar
Müssen Abhängigkeiten wiederverwendet werden, kann ein verifizierter gemeinsamer Cache als Ausgangsbasis dienen: Zu Beginn des Jobs wird er in dessen eigenes Verzeichnis kopiert oder dort wiederhergestellt. Während des Builds wird ausschließlich diese jobeigene Kopie beschrieben. Nach einem erfolgreichen Lauf kann ein separater Schritt die gemeinsame Version aktualisieren. Zwei xcodebuild-Prozesse dürfen niemals gleichzeitig denselben Modulcache oder dieselbe Build-Datenbank beschreiben.
Auch bei abnormalem Abbruch Belege sichern
Ein direkt eingesetztes trap 'rm -rf ...' EXIT löscht bei einem Fehler sofort alle Spuren. In der Praxis sollte zunächst eine kompakte Diagnoseliste mit Exit-Code, freiem Speicherplatz, Verzeichnisgrößen und noch laufenden relevanten Prozessen erstellt werden. Erst danach wird entschieden, welche Dateien erhalten bleiben. Gib nicht die vollständige Liste der Umgebungsvariablen im Protokoll aus, da sie Token oder für die Signierung relevante Werte enthalten kann.
Vor der Bereinigung kann Folgendes ausgeführt werden:
{
echo "exit_status=$status"
df -h "$job_root"
du -sh "$job_root"/* 2>/dev/null || true
find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"
ARTIFACT_DIR muss außerhalb des Stammverzeichnisses des Jobs liegen. Die Pipeline ist für den Upload und die regelmäßige Bereinigung verantwortlich. Soll der Fehlerzustand erhalten bleiben, kann das Diagnoseverzeichnis vor dem Löschen des ursprünglichen Verzeichnisses komprimiert werden. Protokolle und Pfade müssen dabei von vertraulichen Informationen bereinigt werden.
Isolation durch Prüfung auf Rückstände verifizieren
Führe nach der Umstellung denselben Commit zweimal nacheinander aus und prüfe vor dem zweiten Lauf, dass kein Verzeichnis mit der Kennung des vorherigen Jobs mehr vorhanden ist. Bei paralleler Ausführung ist zusätzlich sicherzustellen, dass sich TMPDIR, DerivedData und die Ergebnispaketpfade beider Jobs vollständig unterscheiden.
Die Abnahme kann anhand von fünf festen Punkten erfolgen: Jeder Job besitzt ein eindeutiges Stammverzeichnis; sämtliche Xcode-Pfade sind explizit angegeben; gemeinsame Caches werden nicht gleichzeitig beschrieben; der Exit-Hook wird sowohl bei Erfolg als auch bei Fehlschlägen ausgeführt; Diagnoseartefakte werden vor der Bereinigung kopiert. Nimmt die Speicherbelegung dennoch kontinuierlich zu, sollte anhand der Job-ID nach Werkzeugverzeichnissen gesucht werden, die noch außerhalb der definierten Grenzen liegen, statt den Löschbereich pauschal zu erweitern.
Auf den dedizierten physischen Macs von SureVM eignet sich dieses Verfahren besonders für dauerhaft betriebene Build-Knoten: Die systemweite Toolchain bleibt erhalten, während der veränderliche Zustand jedes Jobs auf nachvollziehbare und löschbare Verzeichnisse beschränkt wird. Das entscheidende Kriterium ist einfach: Unabhängig davon, ob der vorherige Job erfolgreich war, fehlgeschlagen ist oder abgebrochen wurde, darf er die Eingabebedingungen des nächsten Jobs nicht verändern.
Häufig gestellte Fragen
Warum sollten CI-Jobs kein festes temporäres Verzeichnis gemeinsam nutzen?
Parallele Jobs können gleichnamige Dateien überschreiben. Nach einem Abbruch bleiben zudem Sperrdateien, Sockets oder unvollständige Ergebnisse zurück, die den nächsten Build verfälschen.
Isoliert TMPDIR automatisch auch Xcode DerivedData?
Nein. DerivedData und Modul-Caches können an anderen Orten liegen und müssen deshalb mit eigenen Build-Argumenten oder Umgebungsvariablen auf den Job-Pfad gesetzt werden.
Sollte ein Signierungsjob ein eigenes HOME erhalten?
In der Regel nicht. Die Signierung kann von der Keychain des aktuellen Benutzers abhängen. Zuerst sollten nur temporäre Dateien, Build-Ausgaben und eindeutig portable Caches isoliert werden.
Wählen Sie exklusive physische Mac-Knoten für Builds, Entwicklung und Experimente.
Vergleichen Sie zwei Mac mini M4-Konfigurationen, vier Mietlaufzeiten und fünf buchbare Knoten, und treffen Sie Ihre Wahl passend zu Ihrem Workflow.