Engineering-Notizen

DerivedData-Sperren bei parallelen Xcode-Builds vermeiden

DerivedData-Sperren bei parallelen Xcode-Builds vermeiden

DerivedData-Sperren bei parallelen Xcode-Builds vermeiden

Wenn auf demselben Mac in der Cloud zwei Xcode-Pipelines gleichzeitig laufen, sind sporadische Fehler wie database is locked, beschädigte Indexdatenbanken oder lokal nicht reproduzierbare Linkerfehler häufig nicht auf Codeänderungen zurückzuführen. Prüfen Sie zuerst, ob beide Jobs das standardmäßige Verzeichnis ~/Library/Developer/Xcode/DerivedData verwenden. Dieses Verzeichnis enthält nicht nur Cache-Daten, sondern auch Build-Datenbanken, Indizes, Modul-Caches und Zwischenergebnisse, die während eines Jobs fortlaufend überschrieben werden. Wird es wie ein gemeinsam beschreibbarer Cache behandelt, sind Fehler nur eine Frage der Zeit.

Zuerst die Ursache des Konflikts ermitteln

Löschen Sie bei einem Fehler nicht sofort alle Caches. Sichern Sie zunächst den vollständigen xcodebuild-Befehl des fehlgeschlagenen Jobs, seine Startzeit, das Arbeitsverzeichnis und die Prozessliste. Durchsuchen Sie anschließend die Protokolle nach locked, database, unable to attach, malformed und Hinweisen auf mehrfach erzeugte Artefakte.

pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log

Wenn beide Jobs denselben Wert für -derivedDataPath verwenden oder diesen Parameter gar nicht explizit setzen, ist das wichtigste Risiko bereits identifiziert. Prüfen Sie außerdem, ob bei einem Pipeline-Timeout nur das übergeordnete Skript beendet wurde, während xcodebuild oder untergeordnete Compilerprozesse weiter in das Verzeichnis schreiben.

Ein erfolgreicher Wiederholungsversuch beweist nicht, dass das Problem behoben ist. Wenn sich das Zeitfenster für die Konkurrenz verkürzt, kann der zweite Build zufällig erfolgreich sein, obwohl weiterhin mehrere Prozesse in dasselbe Verzeichnis schreiben.

Sperrkonflikte von gewöhnlichen Kompilierungsfehlern unterscheiden

Fehler beim Kompilieren des Quellcodes treten normalerweise reproduzierbar in derselben Datei und Zeile auf. Sperrkonflikte hängen dagegen stärker vom zeitlichen Ablauf paralleler Prozesse ab und erscheinen scheinbar zufällig beim Auflösen von Abhängigkeiten, Erzeugen von Modulen oder Linken. Wenn mehrere Builds ohne Parallelisierung zuverlässig erfolgreich sind und bei erneut aktivierter Parallelisierung wieder fehlschlagen, sollte zuerst die Verzeichniszuordnung geprüft werden, nicht der Anwendungscode.

Jedem Job einen eigenen Arbeitsbereich zuweisen

Eine zuverlässige Lösung besteht darin, das ausgecheckte Repository, DerivedData, Ergebnis-Bundles und temporäre Verzeichnisse mit einer eindeutigen Job-Kennung zu versehen. Diese Kennung kann aus der Pipeline-Jobnummer stammen und bei lokalen Tests ersatzweise aus der Prozess-ID gebildet werden.

set -euo pipefail

JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"

mkdir -p "$DERIVED_DATA"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -resultBundlePath "$RESULT_BUNDLE" \
  build-for-testing

test -d "$RESULT_BUNDLE"

Ein Pfad darf nur von dem Job erstellt und gelöscht werden, dem er zugeordnet ist. Verwenden Sie nicht den Projektnamen als einzigen Verzeichnisnamen, da sonst zwei Branches desselben Projekts miteinander kollidieren. Auch Bereinigungsskripte dürfen keine unscharfen Suchmuster verwenden und beispielsweise sämtliche DerivedData-Verzeichnisse löschen, deren Name App enthält.

Abhängigkeitsauflösung und Kompilierung trennen

Wenn nach der Trennung von DerivedData weiterhin mehrere Jobs dasselbe Verzeichnis mit ausgecheckten Abhängigkeiten verändern, verlagert sich der Konflikt auf die Paketauflösung. Lösen Sie die Abhängigkeiten zuerst auf und verhindern Sie anschließend, dass ihre Versionen während des eigentlichen Builds automatisch geändert werden. Eine geprüfte Sperrdatei sollte in das Repository eingecheckt sein und darf von der Pipeline nicht unbemerkt aktualisiert werden.

PACKAGES="$ROOT/SourcePackages"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$PACKAGES"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "$PACKAGES" \
  -disableAutomaticPackageResolution \
  build

Falls Caching erforderlich ist, sollten heruntergeladene Daten oder geprüfte, unveränderliche Snapshots zwischengespeichert werden. Mehrere Jobs dürfen nicht gleichzeitig in dasselbe aktive Verzeichnis schreiben. Der Cache-Schlüssel muss mindestens den Hash der Sperrdatei, die Xcode-Version und die Zielarchitektur enthalten. Auch nach einem Cache-Treffer muss geprüft werden, dass sich die Sperrdatei nicht geändert hat.

Eine eindeutig auswertbare Abnahmetabelle erstellen

Eine Korrektur darf nicht anhand eines einzigen erfolgreichen Durchlaufs bewertet werden. Starten Sie für denselben Commit mehrere Jobs gleichzeitig und überprüfen Sie dabei die Grenzen zwischen Verzeichnissen, Prozessen und Artefakten.

| Prüfkriterium | Erfolgsbedingung | Maßnahme bei einem Fehler | |---|---|---| | DerivedData-Pfad | Für jeden parallelen Job eindeutig | Job-Kennung und Zusammensetzung des Pfads korrigieren | | Abhängigkeits-Sperrdatei | Hash vor und nach dem Build identisch | Automatische Auflösung deaktivieren und Skripte prüfen | | Verbliebene Prozesse | Nach Jobende laufen keine zugehörigen Compilerprozesse mehr | Behandlung von Timeouts und Beendigungssignalen korrigieren | | Ergebnis-Bundle | Von jedem Job separat erzeugt und lesbar | `resultBundlePath` trennen | | Bereinigungsbereich | Nur das Stammverzeichnis des aktuellen Jobs wird gelöscht | Platzhalter und das Löschen gemeinsam genutzter Verzeichnisse entfernen |

Führen Sie mindestens zwei vollständig identische Builds gleichzeitig aus und anschließend eine Gruppe von Builds aus unterschiedlichen Branches. Der erste Test prüft die Isolation von Schreibzugriffen. Der zweite deckt Fälle auf, in denen Arbeitsverzeichnisse, Modul-Caches oder Artefaktpfade weiterhin ausschließlich nach dem Projektnamen gemeinsam genutzt werden.

Beweise sichern und Daten gefahrlos bereinigen

Nach einem fehlgeschlagenen Job sollte der Zustand nicht sofort gelöscht werden. Archivieren Sie zunächst das Build-Protokoll, das Ergebnis-Bundle, die Xcode-Version, den Hash der Sperrdatei, den verbleibenden Speicherplatz und den Job-Pfad. Quellcode, Umgebungsvariablen und in Protokollen enthaltene Token müssen zuvor anonymisiert werden. Erfolgreiche Jobs können bereinigt werden, nachdem ihre Artefakte nachweislich in ein separates Veröffentlichungsverzeichnis kopiert wurden.

Die Bereinigung sollte an das Stammverzeichnis des Jobs gebunden sein und den erwarteten Pfadpräfix zusätzlich überprüfen. Wird ein Job zwangsweise beendet, muss ein nachgelagerter Bereinigungsprozess die verbliebenen Verzeichnisse anhand der Job-Kennung entfernen, statt das Entwicklerverzeichnis des gesamten Systems zu leeren. Auch wenn mehrere Pipelines auf NUMACS ausgeführt werden, sollten Sie zunächst in der Konsole die aktuell verfügbaren Konfigurationen prüfen und das Job-Limit anschließend an die gewünschte Parallelität anpassen. Mehr Parallelität ersetzt keine Isolation der Schreibzugriffe.

Das Ziel besteht nicht darin, dass Fehler nur „seltener auftreten“, sondern darin, klare Invarianten zu etablieren: Jeder parallele Job hat genau einen schreibenden Prozess, die Abhängigkeitsversionen bleiben während des Builds unverändert, fehlgeschlagene Jobs lassen sich nachvollziehen und die Bereinigung beeinträchtigt keine anderen Jobs. Erst wenn diese vier Bedingungen erfüllt sind, wird DerivedData von einer Quelle sporadischer Fehler wieder zu kontrollierbaren Build-Daten.

Häufig gestellte Fragen

Benötigt jeder parallele Job ein eigenes DerivedData?

Ja. Jeder gleichzeitig schreibende Job sollte mit -derivedDataPath ein eigenes Verzeichnis erhalten. Nur strikt serielle Jobs können denselben Pfad sicher wiederverwenden.

Geht durch die Trennung der gesamte Cache-Vorteil verloren?

Nein. Paketdownloads und geprüfte, unveränderliche Artefakte lassen sich separat vorhalten, während beschreibbare Build-Daten pro Job isoliert bleiben.

Reicht ein erneuter Build nach einer Datenbanksperre aus?

Nein. Ein Retry verdeckt nur die Konkurrenzsituation. Zuerst müssen gemeinsame Schreibpfade und verbliebene xcodebuild-Prozesse beseitigt werden.

NUMACS Cloud-Mac

Build-Aufgaben auf eine dedizierte physische Workstation auslagern

Beide Apple-Silicon-Konfigurationen laufen auf dedizierten physischen Maschinen statt in virtuellen Umgebungen. Sie können tage-, wochen-, monats- oder quartalsweise gemietet werden und sind an den Standorten Singapur, Tokio, Seoul und Hongkong verfügbar.

Gerät auswählen und bestellen