Wenn ein Xcode-Build auf einem entfernten Cloud-Mac plötzlich Cycle inside ... oder Multiple commands produce ... meldet, besteht der häufigste erste Fehler darin, sofort DerivedData zu löschen. Dadurch kann sich die Reihenfolge der Fehlermeldungen vorübergehend ändern, zugleich gehen jedoch wichtige Diagnosehinweise verloren. Zuverlässiger ist es, Code, Scheme, Konfiguration und Xcode-Pfad unverändert zu lassen, das vollständige Protokoll zu sichern und anschließend zu ermitteln, ob eine zyklische Target-Abhängigkeit vorliegt oder mehrere Build-Phasen denselben Ausgabepfad beanspruchen.
Den Fehler zunächst reproduzierbar machen
Pausieren Sie vor der Diagnose automatische Code-Aktualisierungen, Abhängigkeits-Upgrades und parallele Builds, damit sich der Workspace nicht zwischen zwei Tests verändert. Erfassen Sie den aktuellen Commit, die Xcode-Version und das tatsächlich ausgewählte Entwicklerverzeichnis:
set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p
mkdir -p .diagnostics
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-showBuildTimingSummary \
build 2>&1 | tee .diagnostics/build.log
Wenn das Projekt einen Workspace verwendet, ersetzen Sie -project App.xcodeproj durch -workspace App.xcworkspace. Übergeben Sie nicht beide Optionen gleichzeitig. Extrahieren Sie anschließend die relevanten Stellen, bewahren Sie aber das ursprüngliche Protokoll für die Prüfung des Kontexts auf:
grep -nE \
'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
.diagnostics/build.log
Der letzte Befehl im Protokoll bezeichnet meist nur die Stelle, an der der Konflikt erkannt wurde. Er ist nicht zwangsläufig der Ausgangspunkt des Zyklus. Folgen Sie der von Xcode ausgegebenen Abhängigkeitskette nach oben, bis Sie das erste wiederholte Target, Skript oder den ersten wiederholten Ausgabepfad finden.
Zwei ähnlich wirkende Fehlerarten unterscheiden
Bei einem Abhängigkeitszyklus muss A auf B warten, während B direkt oder indirekt wiederum auf A wartet. Eine doppelte Ausgabe bedeutet dagegen, dass zwei Befehle denselben Dateipfad für sich beanspruchen. Beide Fehler können gleichzeitig auftreten, erfordern aber unterschiedliche Korrekturen.
| Protokollmerkmal | Zuerst prüfen | Häufige Ursache |
|---|---|---|
Cycle inside |
Target Dependencies, implizite Abhängigkeiten | App und Framework hängen voneinander ab |
Multiple commands produce |
Copy Files, Compile Sources, Run Script | Dieselbe Datei wird zweimal kopiert oder erzeugt |
| Skript wird bei jedem Build ausgeführt | Ein- und Ausgaben von Run Script | Abhängigkeiten fehlen oder die Abhängigkeitsanalyse ist deaktiviert |
| Fehler tritt nur beim Archive auf | Embed-Phase, Archivierungsskripte | Phasen sind für Debug und Release unterschiedlich konfiguriert |
Prüfen Sie zunächst per Befehl, welche Targets und welche Konfiguration bei diesem Build tatsächlich verwendet werden, statt sich auf die aktuell in der Xcode-Oberfläche angezeigten Optionen zu verlassen:
xcodebuild -list -json -project App.xcodeproj \
> .diagnostics/project-list.json
xcodebuild -showBuildSettings \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
> .diagnostics/build-settings.txt
Abhängigkeitsketten durch Targets und Build-Phasen verfolgen
Beginnen Sie in Xcode unter Target Dependencies mit den Targets am Anfang und Ende der gemeldeten Kette. Eine App darf von einem Framework abhängen. Das Framework sollte jedoch nicht wiederum von der App abhängen, nur um auf dort definierte Typen zuzugreifen. Gemeinsamer Code sollte in ein eigenständiges Modul ausgelagert werden. Alternativ lässt sich der Rückverweis durch Protokolle und Dependency Injection auflösen.
Prüfen Sie anschließend nacheinander alle Build Phases und achten Sie besonders auf die folgenden Beziehungen:
Doppelte Zuständigkeiten prüfen
Dieselbe Quelldatei sollte nicht gleichzeitig in zwei Compile-Sources-Einträgen enthalten sein, die identische Objektdateien erzeugen. Ebenso sollte ein Framework nicht sowohl von der automatisch erzeugten Embed-Phase verarbeitet als auch durch eine benutzerdefinierte Copy-Files-Phase erneut kopiert werden. Legen Sie für Codegeneratoren ein eindeutiges Ausgabeverzeichnis fest, damit deren Ausgaben nicht in ein Quellverzeichnis geschrieben werden, das bereits von einem anderen Target kompiliert wird.
Implizite Abhängigkeiten prüfen
Liest ein Skript das Build-Artefakt eines anderen Targets über einen fest codierten Pfad, ohne eine explizite Abhängigkeit zu deklarieren, kann sich die Build-Reihenfolge je nach Parallelisierungsgrad ändern. Kaschieren Sie das Problem nicht, indem Sie parallele Builds deaktivieren. Dadurch wird der Fehler lediglich seltener ausgelöst, die fehlerhafte Graphstruktur aber nicht behoben.
Ein- und Ausgaben von Run Script eindeutig deklarieren
Run Script ist eine häufige Quelle für Zyklen. Wenn ein Skript Konfigurationsdateien liest, Swift-Dateien erzeugt oder Ressourcen kopiert, sollten in der Phase Input Files und Output Files oder entsprechende Dateilisten hinterlegt werden. Außerdem muss sichergestellt sein, dass jede Ausgabe genau einer Phase zugeordnet ist.
Ein Skript zum Erzeugen einer Versionsdatei kann beispielsweise zunächst die erforderlichen Variablen prüfen und anschließend eine temporäre Datei atomar ersetzen:
set -euo pipefail
input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"
test -f "$input"
mkdir -p "$(dirname "$output")"
version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
"$version" > "$tmp"
if test -f "$output" && cmp -s "$tmp" "$output"; then
rm "$tmp"
else
mv "$tmp" "$output"
fi
In der zugehörigen Phase sollte $(SRCROOT)/Config/version.txt als Eingabe und $(DERIVED_FILE_DIR)/GeneratedVersion.swift als Ausgabe deklariert werden. Generierte Dateien im abgeleiteten Verzeichnis abzulegen, schafft klarere Zuständigkeiten als eine direkte Änderung von Dateien im Repository. Außerdem sinkt das Risiko, dass parallele Tasks ihre Ausgaben gegenseitig überschreiben.
Die Korrektur mit minimalen Änderungen validieren
Ändern Sie pro Durchlauf nur eine Abhängigkeit, eine Phase oder einen Ausgabepfad und speichern Sie danach erneut das Protokoll. Werden mehrere Referenzen auf einmal entfernt, kann der Build zwar erfolgreich sein, die eigentliche Ursache lässt sich dann jedoch nur schwer bestätigen. Zudem können gleichartige Probleme in der Archivierungskonfiguration unentdeckt bleiben.
Validieren Sie die Korrektur in dieser Reihenfolge:
- Führen Sie einen sauberen Build aus und prüfen Sie, ob sich der vollständige Graph von Grund auf aufbauen lässt.
- Führen Sie ohne vorherige Bereinigung zwei weitere Builds direkt hintereinander aus und prüfen Sie, ob Skripte unveränderte Eingaben korrekt überspringen.
- Testen Sie Debug und Release separat. Bei Projekten für die Veröffentlichung sollte außerdem einmal Archive ausgeführt werden.
- Vergleichen Sie die beiden Protokolle und vergewissern Sie sich, dass keine neuen doppelten Ausgaben oder Warnungen zu bedingungslos ausgeführten Skripten auftreten.
- Dokumentieren Sie Änderungen an Target-Abhängigkeiten, Ein- und Ausgaben von Phasen sowie deren Begründung in der Code-Review-Beschreibung.
Das Löschen von DerivedData kann als abschließender Isolationstest dienen, ist aber keine Fehlerbehebung. Ein wirklich stabiles Xcode-Projekt muss bei unterschiedlichem Parallelisierungsgrad sowie bei sauberen und inkrementellen Builds dieselbe Abhängigkeitsreihenfolge ergeben. Nur dann bleiben sowohl die interaktive Entwicklung auf dem Cloud-Mac als auch unbeaufsichtigte Aufgaben von zufälligen Fehlern durch Änderungen der Ausführungsplanung verschont.
Häufig gestellte Fragen
Behebt das Löschen von DerivedData einen Xcode-Buildzyklus?
In der Regel nicht. Alte Zwischenprodukte verschwinden, aber fehlerhafte Target-Abhängigkeiten oder doppelte Ausgabepfade erzeugen den Zyklus beim nächsten Build erneut.
Warum verursachen Run Scripts häufig Probleme im Buildgraph?
Fehlen deklarierte Ein- und Ausgaben oder schreibt das Skript in dasselbe Ziel wie eine andere Phase, kann Xcode keine eindeutige Reihenfolge bestimmen.
Wie wird eine Korrektur zuverlässig geprüft?
Führen Sie einen sauberen Build und danach zwei Builds ohne Bereinigung aus. Zyklen, doppelte Ausgaben und Warnungen zu ständig laufenden Skripten müssen verschwunden sein.
Führe deine nächste Remote-Mac-Aufgabe auf einem dedizierten Knoten aus
Wähle je nach Projektumfang aus zwei verfügbaren Konfigurationen, fünf Standorten sowie Tages-, Wochen-, Monats- oder Quartalslaufzeiten. Vor der Bestellung kannst du Konfiguration und USD-Betrag vollständig prüfen.