Wenn derselbe Commit auf dem Entwicklungsrechner erfolgreich gebaut wird, auf einem anderen Cloud-Mac jedoch andere Abhängigkeiten aufgelöst werden, liegt das meist nicht an einem sporadisch versagenden Compiler. In der Regel wurden die Auflösungsdatei, die Xcode-Version und die Cache-Grenzen nicht gemeinsam fixiert. Besonders bei kurzfristig gemieteten Knoten tritt dieses Problem schnell zutage: Bei der Bereitstellung sind die Verzeichnisse zwar neu, doch Pipeline-Skripte verwenden möglicherweise standardmäßig einen alten Cache weiter oder ändern während des Builds unbemerkt die Abhängigkeitsversionen.
Zuerst die Eingaben eines identischen Builds definieren
Reproduzierbarkeit bedeutet nicht, dass jedes Mal das gesamte Arbeitsverzeichnis erhalten bleiben muss. Entscheidend ist, dass identische Eingaben immer dieselbe Menge an Abhängigkeiten ergeben. Mindestens vier Angaben sollten protokolliert werden: der Code-Commit, der Inhalt von Package.resolved, die Xcode-Build-Version und die Prozessorarchitektur. Nur die Xcode-Hauptversion zu erfassen, reicht nicht aus, da sich auch die Patchstände der Toolchain innerhalb derselben Hauptversion unterscheiden können.
Erstellen Sie zunächst auf dem Knoten einen Snapshot der Umgebung:
set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD
Bei Anwendungsprojekten sollte Package.resolved in der Regel eingecheckt werden. In einem Workspace liegt die Datei häufig unter App.xcworkspace/xcshareddata/swiftpm/Package.resolved. Besteht nur eine Projektdatei, kann sie sich auch im Workspace-Verzeichnis innerhalb des Projekts befinden. Pflegen Sie nicht zwei Auflösungsdateien parallel, sondern prüfen Sie zuerst, welche Datei die Pipeline tatsächlich einliest.
Das Prüfkriterium ist eindeutig: Zeigt
git status --porcelainnach Ausführung des Build-Skripts eine Änderung der Auflösungsdatei an, kann dieser Build nicht als Prüfung des ursprünglichen Commits gelten.
Das Auflösungsergebnis statt nur des Versionsbereichs fixieren
Die Versionsbereiche in Package.swift beschreiben, welche Versionen ausgewählt werden dürfen. Erst Package.resolved hält fest, welche Versionen tatsächlich ausgewählt wurden. Bei der erstmaligen Einrichtung eines Knotens kann die Auflösung explizit ausgeführt werden. Der eigentliche Build sollte dagegen ausschließlich die bereits dokumentierten Versionen akzeptieren.
set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$CACHE"
git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=macOS' \
-clonedSourcePackagesDirPath "$CACHE" \
-onlyUsePackageVersionsFromResolvedFile \
build
Für ein iOS-Ziel sollte destination auf das in der Pipeline festgelegte generische Geräte- oder Simulatorziel geändert werden. Das Skript sollte nicht automatisch das aktuell gestartete Gerät auswählen. Für reine Swift-Pakete können swift package resolve und swift build verwendet werden. Projekte und Workspaces sollten dagegen einheitlich über xcodebuild laufen, damit zwei unterschiedliche Auflösungswege nicht zu abweichenden Verzeichniszuständen führen.
Cache-Grenzen anhand der Eingaben festlegen
Ein Cache soll lediglich die für Downloads und Checkouts benötigte Zeit verkürzen. Er darf nicht zur Quelle der Abhängigkeitsversionen werden. Ein belastbarer Cache-Schlüssel sollte die Architektur, die Xcode-Build-Informationen und den Hash der Auflösungsdatei enthalten:
set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
| Änderung der Eingaben | Arbeits-Cache wiederverwenden | Begründung |
|---|---|---|
| Nur der Anwendungscode ändert sich | Ja | Die Abhängigkeitsmenge bleibt unverändert |
Package.resolved ändert sich |
Nein | Ausgecheckte Versionen oder Revisionen haben sich geändert |
| Die Xcode-Build-Version ändert sich | Nein | Toolchain und Artefaktformate können sich unterscheiden |
Wechsel zwischen arm64 und einer anderen Architektur |
Nein | Binärartefakte dürfen nicht vermischt werden |
Bei der Ausführung auf mehreren Knoten dürfen nicht mehrere Rechner gleichzeitig in dasselbe Verzeichnis schreiben. Stattdessen kann ein nach Cache-Schlüssel archivierter, schreibgeschützter Cache bereitgestellt werden, den jeder physische Knoten in ein eigenes lokales Arbeitsverzeichnis kopiert. Auch bei einem erneuten Versuch nach einem Fehler sollte zunächst die aktuelle Arbeitskopie bereinigt werden, anstatt sämtliche historischen Archive zu löschen.
Den Auftrag bei abweichenden Abhängigkeiten sofort abbrechen
Eine Prüfung des Repository-Status vor und nach dem Build deckt sowohl Änderungen an der Auflösungsdatei als auch versehentlich im Quellverzeichnis abgelegte Skriptartefakte auf. Produktive Aufträge sollten mit einem sauberen Checkout beginnen:
set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
-clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
-onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"
Heben Sie nach einem Fehler nicht sofort die Versionsbeschränkungen auf, um die Abhängigkeiten neu aufzulösen. Dadurch würde ein Konfigurationsfehler lediglich als erfolgreicher Build erscheinen. Bewahren Sie stattdessen das Auflösungsprotokoll, den Umgebungs-Snapshot und den Cache-Schlüssel auf. Prüfen Sie anschließend, ob dem Repository ein fixiertes Auflösungsergebnis fehlt, eine Revision der Abhängigkeit nicht mehr gültig ist oder auf dem Knoten die falsche Xcode-Version ausgewählt wurde.
Häufige Fehler in der richtigen Reihenfolge untersuchen
Zeitüberschreitung bei der Auflösung
Prüfen Sie zuerst, ob der Knoten die Quellen der Abhängigkeiten erreichen kann. Kontrollieren Sie anschließend den verfügbaren Speicherplatz und die Berechtigungen des Cache-Verzeichnisses. Sobald die Netzwerkverbindung wieder funktioniert, sollte der Vorgang mit derselben Auflösungsdatei wiederholt werden, ohne zuvor Abhängigkeiten zu aktualisieren. Schlägt die Auflösung jedes Mal beim selben Paket fehl, sollten dessen Deklaration und Revision separat auf ihre weiterhin bestehende Übereinstimmung geprüft werden.
Auflösung erfolgreich, Kompilierung fehlgeschlagen
Vergleichen Sie zunächst die Ausgaben von xcode-select -p, xcodebuild -version und uname -m auf beiden Knoten. Bei binären Abhängigkeiten muss außerdem geprüft werden, ob deren Slices die aktuelle Architektur enthalten und die im Projekt konfigurierte minimale Systemversion unterstützen. Löschen Sie die lokale Kopie für den betreffenden Cache-Schlüssel und bauen Sie das Projekt erneut. So lässt sich ein beschädigter Cache von einer Inkompatibilität im Quellcode unterscheiden.
Checkliste vor dem Commit
- Im Repository gibt es nur eine tatsächlich verwendete Datei
Package.resolved. - Der Build-Befehl verwendet
-onlyUsePackageVersionsFromResolvedFile. - Der Cache-Schlüssel enthält die Architektur, die Xcode-Build-Informationen und den Hash der Auflösungsdatei.
- Jeder Knoten verwendet ein eigenes beschreibbares Arbeitsverzeichnis.
- Der Hash der Auflösungsdatei ist vor und nach dem Build identisch.
- Die Protokolle enthalten Commit-ID, Cache-Schlüssel und Toolchain-Version, aber keine Zugangsdaten.
Häufig gestellte Fragen
Sollte Package.resolved in das Repository eingecheckt werden?
Für Anwendungen und deploybare Dienste in der Regel ja. Der Build sollte ausschließlich die dort fixierten Versionen verwenden. Bibliotheken können anders verfahren, müssen ihren unterstützten Versionsbereich aber in einer sauberen Umgebung prüfen.
Dürfen mehrere Cloud-Macs denselben SwiftPM-Cache beschreiben?
Nein, parallele Schreibzugriffe auf dasselbe Arbeitsverzeichnis sind zu vermeiden. Cache-Schlüssel sollten Architektur, Xcode-Build und Prüfsumme von Package.resolved enthalten; jeder Knoten erhält eine eigene Arbeitskopie.
Was ist bei erfolgreicher Auflösung, aber fehlgeschlagener Kompilierung zu prüfen?
Zuerst Xcode-Pfad und Build-Version prüfen, danach die Arbeitskopie des betroffenen Cache-Schlüssels neu erstellen. Anschließend Architektur, Mindestversion des Systems und Änderungen an Package.resolved kontrollieren.
Eine reproduzierbare Umgebung auf demselben Cloud-Mac behalten
Wählen Sie Modell, Region und Mietdauer fest aus und erledigen Sie Entwicklungs- oder Automatisierungsaufgaben mit vollständiger grafischer macOS-Oberfläche und Kommandozeile.