Engineering-Notizen

Beschädigte Xcode-Build-Datenbank auf einem Cloud Mac reparieren

Beschädigte Xcode-Build-Datenbank auf einem Cloud Mac reparieren

Nachdem ein Remote-Build-Knoten mehrere Tage ununterbrochen gelaufen ist, kann Xcode plötzlich fehlschlagen, obwohl sich der Projektcode nicht geändert hat: Im Protokoll steht, dass build.db nicht geöffnet werden kann, das Disk-Image-Format fehlerhaft ist oder die Datenbank beschädigt wurde. Auch ein erneuter Build bleibt dann an derselben Stelle hängen. Das gesamte DerivedData-Verzeichnis zu löschen ist zwar bequem, entfernt jedoch zugleich Indizes, Modul-Caches und wiederverwendbare Artefakte. Dadurch werden nachfolgende Builds langsamer und die wertvollsten Spuren des Fehlers gehen verloren.

Auf einem Cloud Mac von NUMACS empfiehlt sich bei solchen Problemen diese Reihenfolge: Projekteinstieg eindeutig festlegen, Protokolle sichern, sicherstellen, dass kein Build-Prozess mehr auf die Dateien zugreift, die Integrität der Datenbank prüfen und schließlich nur XCBuildData des betroffenen Projekts neu erstellen.

Zuerst bestätigen, dass die Build-Datenbank die Ursache ist

Verlassen Sie sich nicht nur auf die letzte Zeile in der Xcode-Oberfläche. Starten Sie zunächst einen Build über die Befehlszeile – aus demselben Verzeichnis sowie mit demselben Scheme und derselben Konfiguration – und speichern Sie die vollständige Ausgabe:

set -o pipefail
mkdir -p "$HOME/build-evidence"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  build 2>&1 | tee "$HOME/build-evidence/xcodebuild.log"

status=${PIPESTATUS[0]}
echo "$status" > "$HOME/build-evidence/exit-code.txt"
exit "$status"

Suchen Sie insbesondere nach build.db, database, malformed, disk image und XCBuildData. Wenn das Protokoll eindeutig auf einen Compilerfehler im Quellcode, eine fehlgeschlagene Abhängigkeitsauflösung oder unzureichenden Speicherplatz hinweist, muss zunächst dieses Problem behoben werden. Nicht jeder Build-Fehler ist auf die Datenbank zurückzuführen.

Sichern Sie vor der Reparatur mindestens das vollständige Protokoll, den Exit-Code und den Zeitpunkt des Fehlers. Nach einer Bereinigung lässt sich der ursprüngliche Zustand der Datenbank in der Regel nicht mehr reproduzieren.

Das tatsächlich verwendete DerivedData-Verzeichnis ermitteln

Auf demselben Cloud Mac können mehrere DerivedData-Verzeichnisse für gleichnamige Projekte vorhanden sein. Wer nur anhand der Änderungszeit eines Verzeichnisses entscheidet, riskiert, die falschen Daten zu löschen. Lassen Sie stattdessen xcodebuild die aktuellen Build-Einstellungen ausgeben und leiten Sie das Projektstammverzeichnis aus BUILD_DIR ab.

BUILD_DIR=$(
  xcodebuild \
    -workspace App.xcworkspace \
    -scheme App \
    -configuration Debug \
    -showBuildSettings |
  awk -F ' = ' '$1 ~ /^[[:space:]]*BUILD_DIR$/ {print $2; exit}'
)

DERIVED_ROOT=$(dirname "$(dirname "$BUILD_DIR")")
XCBD="$DERIVED_ROOT/Build/Intermediates.noindex/XCBuildData"

printf 'DerivedData: %s
XCBuildData: %s
' \
  "$DERIVED_ROOT" "$XCBD"
test -d "$XCBD"

Wenn die Pipeline -derivedDataPath verwendet, sollte dieser explizite Pfad direkt übernommen werden, statt erneut das Benutzerverzeichnis zu durchsuchen. So liefert das Skript in interaktiven Sitzungen und unbeaufsichtigten Jobs dasselbe Ergebnis.

Zu bearbeitende Dateien protokollieren

Erfassen Sie vor der Bereinigung Größe und Änderungszeit der Datenbank und der zugehörigen Dateien:

find "$XCBD" -maxdepth 1 -type f \
  -exec stat -f '%Sm %z %N' -t '%Y-%m-%dT%H:%M:%S%z' {} \; \
  > "$HOME/build-evidence/xcbuilddata-files.txt"

Anhand dieser Liste lässt sich erkennen, ob die Datenbank unmittelbar vor dem Fehler neu geschrieben wurde. Außerdem können Sie bei wiederkehrenden Fehlern feststellen, ob stets derselbe Pfad betroffen ist.

Prozesszugriffe ausschließen und build.db schreibgeschützt prüfen

Eine Datenbank darf nicht einfach gelöscht werden, solange ein aktiver Build darauf zugreift. Prüfen Sie zunächst die relevanten Prozesse und Dateihandles:

pgrep -alf 'Xcode|xcodebuild|XCBBuildService' || true
lsof "$XCBD/build.db" || true

Wenn noch ein Build des aktuellen Jobs läuft, beenden Sie ihn zuerst über den regulären Abbruchmechanismus der Pipeline und warten Sie, bis auch die untergeordneten Prozesse beendet wurden. Beenden Sie nicht wahllos alle gleichnamigen Prozesse auf dem Rechner, da in anderen Arbeitsverzeichnissen weiterhin gültige Jobs laufen können.

Sobald keine Zugriffe mehr bestehen, prüfen Sie die SQLite-Datenbank im schreibgeschützten Modus:

sqlite3 "file:$XCBD/build.db?mode=ro" \
  'PRAGMA quick_check;'

Lautet das Ergebnis ok, wurden keine offensichtlichen Schäden an der Datenbankstruktur gefunden. Prüfen Sie dann anhand des Protokolls weiter, ob Berechtigungen, Speicherplatz oder die Projektkonfiguration die Ursache sind. Fahren Sie nur dann mit der Neuerstellung fort, wenn der Befehl einen von null verschiedenen Exit-Code zurückgibt, die Datei nicht gelesen werden kann oder Integritätsfehler ausgegeben werden.

Prüfergebnis Nächster Schritt
lsof zeigt einen aktiven Build Job regulär beenden und erneut prüfen
quick_check gibt ok zurück Datenbank beibehalten und andere Fehler untersuchen
Kein Zugriff und Integritätsprüfung fehlgeschlagen XCBuildData des betroffenen Projekts neu erstellen
Pfad ist nicht vorhanden Workspace, Scheme und DerivedData-Parameter prüfen

XCBuildData mit minimalem Eingriff neu erstellen

Verschieben Sie das beschädigte Verzeichnis zunächst in das Beweisverzeichnis, anstatt es sofort endgültig zu löschen. Der Verschiebevorgang sollte innerhalb desselben Dateisystems erfolgen, um die Kopierzeit zu minimieren:

STAMP=$(date '+%Y%m%d-%H%M%S')
QUARANTINE="$HOME/build-evidence/XCBuildData-$STAMP"

mv "$XCBD" "$QUARANTINE"
mkdir -p "$(dirname "$XCBD")"

Führen Sie anschließend den Build mit denselben Parametern erneut aus. Xcode erstellt ein neues XCBuildData-Verzeichnis einschließlich build.db, während die übrigen Verzeichnisse in DerivedData erhalten bleiben.

set -o pipefail

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  build 2>&1 | tee "$HOME/build-evidence/recovery-build.log"

Falls der Build auch nach dieser minimalen Neuerstellung fehlschlägt, sollten Sie nicht sofort weitere Verzeichnisse löschen. Vergleichen Sie zuerst die alten und neuen Protokolle und prüfen Sie, ob der Fehler weiterhin auf die Datenbank verweist. Hat sich der Fehler auf Abhängigkeiten, Berechtigungen oder den Quellcode verlagert, ist das Datenbankproblem behoben und der neue Fehler muss separat behandelt werden.

Wiederherstellung mit zwei Builds prüfen und erneuten Schäden vorbeugen

Ein einzelner erfolgreicher Build reicht nicht aus, um einen stabilen Zustand zu bestätigen. Beim ersten Build wird die neue Datenbank erzeugt; ein zweiter inkrementeller Build stellt sicher, dass sie erneut gelesen und aktualisiert werden kann. Für beide Builds müssen Workspace, Scheme, Konfiguration, Ziel und DerivedData-Pfad unverändert bleiben. Speichern Sie die Protokolle und Exit-Codes jeweils separat.

Prüfen Sie bei der Abnahme folgende Punkte:

  1. Eine neue build.db wurde erstellt und der schreibgeschützte quick_check gibt ok zurück.
  2. Sowohl der saubere als auch der inkrementelle Build werden erfolgreich abgeschlossen.
  3. In den Protokollen erscheinen keine Fehler zum Datenbankformat oder zum Lesen von XCBuildData mehr.
  4. Die Build-Artefakte stammen aus der erwarteten Konfiguration und dem vorgesehenen Zielverzeichnis.
  5. Die gesicherten Fehlerdaten wurden vor der bestätigten Wiederherstellung nicht gelöscht.

Zur Vorbeugung sollten Sie nicht regelmäßig sämtliche Caches leeren. Wichtiger ist, jedem Arbeitsverzeichnis einen eindeutigen DerivedData-Pfad zuzuweisen und beim Abbruch eines Jobs zu warten, bis alle untergeordneten Build-Prozesse beendet wurden. Außerdem sollte der freie Speicherplatz auf den Ausführungsknoten kontinuierlich überwacht werden, damit während eines Datenbankschreibvorgangs keine unvollständigen Dateien durch Platzmangel entstehen.

Wenn derselbe Pfad wiederholt beschädigt wird, sollten Sie unerwartete Prozessabbrüche, den Datenträgerzustand und die Xcode-Version vor dem jeweiligen Fehler erfassen. Machen Sie den Bereinigungsbefehl nicht zu einem festen Vorbereitungsschritt für jeden Build. Das Zurücksetzen des Caches bei jedem Durchlauf verschleiert die eigentliche Ursache und beseitigt zugleich die Vorteile inkrementeller Builds.

Der Ablauf lässt sich schließlich auf vier Schritte reduzieren: Beweise sichern, Pfad bestätigen, schreibgeschützt prüfen und minimal neu erstellen. Erst wenn diese Maßnahmen keine Wiederherstellung ermöglichen, sollten Sie einen größeren Bereich von DerivedData bereinigen. So verkürzen Sie die Wiederherstellungszeit und bewahren zugleich genügend Informationen, um die Ursache wiederkehrender Fehler zu ermitteln.

Häufig gestellte Fragen

Sollte bei einem build.db-Fehler sofort das gesamte DerivedData gelöscht werden?

Nein. Zuerst Builds beenden und Protokolle sichern. Danach nur XCBuildData des betroffenen Projekts entfernen; dadurch bleiben andere wiederverwendbare Daten zunächst erhalten.

Wie lässt sich eine Beschädigung von einem noch laufenden Prozess unterscheiden?

Mit lsof wird geprüft, ob build.db noch geöffnet ist. Ist kein relevanter Prozess aktiv und meldet SQLite quick_check einen Fehler, spricht das für eine beschädigte Datenbank.

Wie wird die Reparatur zuverlässig bestätigt?

Führen Sie mit festen Workspace-, Scheme-, Konfigurations- und DerivedData-Werten einen sauberen und anschließend einen inkrementellen Build aus und speichern Sie beide Ergebnisse.

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