Wenn derselbe Befehl xcodebuild test mehrere Wochen lang auf einem Cloud Mac ausgeführt wird, kann er plötzlich melden, dass die Destination nicht verfügbar ist, oder einen gleichnamigen Simulator aus einer neu installierten Runtime auswählen. Das Projekt selbst hat sich nicht geändert. Verschoben hat sich vielmehr das Ergebnis der Zielgeräteauflösung durch Xcode. Eine stabile Lösung besteht nicht darin, eine bestimmte UDID dauerhaft in der CI-Konfiguration zu hinterlegen. Stattdessen müssen Archivierung, Kompilierungsprüfung und Simulatortests klar getrennt und das jeweilige Ziel zu Beginn jedes Auftrags neu aufgelöst und protokolliert werden.
Zunächst drei Arten von Build-Zielen unterscheiden
Die Xcode Destination legt fest, für welche Plattform und welches Gerät eine Aktion ausgeführt wird. Sie ist nicht mit dem Scheme gleichzusetzen. Ein gemeinsam verwendetes Scheme kann für ein generisches iOS-Archiv, Tests auf einem bestimmten Simulator und reine Kompilierungsprüfungen dienen. Diese drei Aufgaben sollten jedoch nicht denselben ungenauen Befehl verwenden.
| Aufgabe | Empfohlenes Ziel | Konkrete UDID erforderlich? |
|---|---|---|
| Archive erstellen | generic/platform=iOS | Nein |
| Simulatortests | Bestimmter iOS Simulator | Ja |
| Kompilierungsprüfung | Je nach Artefaktplattform generisches oder konkretes Ziel | Abhängig vom Testumfang |
Wird für einen Archivierungsauftrag name=iPhone 16 angegeben, entsteht ohne Not eine Abhängigkeit von einer Simulator-Runtime. Wird für einen Testauftrag umgekehrt nur platform=iOS Simulator angegeben, kann Xcode mehrere Kandidaten finden. Die endgültige Auswahl hängt dann von den installierten Runtimes und der Reihenfolge ab, in der die Geräte erstellt wurden.
Ein generisches Ziel weist nach, dass das Projekt ein Artefakt für die betreffende Plattform erzeugen kann. Ein konkretes Ziel weist nach, dass die Tests in einer genau festgelegten Geräteumgebung ausgeführt werden können. Eine Destination sollte nicht beide Bedeutungen gleichzeitig abdecken müssen.
Verfügbare Ziele vor Auftragsbeginn prüfen
Legen Sie zunächst den Xcode-Pfad fest und lassen Sie anschließend das Projekt selbst seine verfügbaren Ziele melden. So lässt sich unterscheiden, ob das Scheme die Plattform nicht unterstützt oder ob auf dem Host kein passendes Gerät vorhanden ist. Dadurch vermeiden Sie, vorschnell Caches zu löschen.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
WORKSPACE="App.xcworkspace"
SCHEME="App"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-showdestinations
Die Ausgabe sollte als Auftragsartefakt aufbewahrt werden. Wenn die Liste überhaupt keinen iOS Simulator enthält, prüfen Sie zuerst die Supported Destinations des Schemes, die Zielplattform und das Test-Target, statt wiederholt simctl erase all auszuführen. Ist das Gerät vorhanden, aber als unavailable markiert, kontrollieren Sie, ob die zugehörige Runtime von der aktuellen Xcode-Version noch unterstützt wird.
Identität der Toolchain ebenfalls protokollieren
Destination-Probleme treten häufig zusammen mit einem Wechsel der standardmäßig verwendeten Xcode-Version auf. Pro Lauf sollten mindestens die folgenden Informationen protokolliert werden:
xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices available
Diese Ausgaben enthalten keine Projektschlüssel und eignen sich daher zur Archivierung als Build-Diagnosedaten. Wenn mehrere Cloud Macs parallel arbeiten, lässt sich damit außerdem feststellen, ob der Fehler nur bei einer bestimmten Toolchain auftritt.
Simulator dynamisch auflösen statt UDID festzuschreiben
Eine UDID gehört zu einer Geräteinstanz auf dem aktuellen Host. Sie kann sich ändern, wenn ein Simulator gelöscht und neu erstellt, eine Runtime ersetzt oder ein Worker zurückgesetzt wird. Die CI sollte anhand des Gerätenamens und des Verfügbarkeitsstatus suchen und ein eindeutiges Ergebnis verlangen.
Das folgende Skript wählt aus den JSON-Daten von simctl einen verfügbaren iPhone 16 aus. In einer realen Pipeline sollte zusätzlich die Kennung der gewünschten Runtime als Parameter übergeben werden, damit gleichnamige Geräte unter zwei Betriebssystemversionen nicht zu Mehrdeutigkeiten führen.
SIM_NAME="${SIM_NAME:-iPhone 16}"
SIM_UDID="$(
xcrun simctl list devices available -j |
python3 -c '
import json, sys
name = sys.argv[1]
data = json.load(sys.stdin)
matches = []
for runtime, devices in data["devices"].items():
for device in devices:
if device.get("isAvailable") and device.get("name") == name:
matches.append((runtime, device["udid"]))
if len(matches) != 1:
for runtime, udid in matches:
print(f"{runtime} {udid}", file=sys.stderr)
raise SystemExit(f"expected one simulator named {name}, found {len(matches)}")
print(matches[0][1])
' "$SIM_NAME"
)"
printf 'Resolved simulator: %s (%s)\n' "$SIM_NAME" "$SIM_UDID"
Entscheidend ist, bei mehr als einem Kandidaten sofort abzubrechen. Wird stillschweigend das erste Array-Element verwendet, kann der Auftrag zwar fortgesetzt werden, doch die Mehrdeutigkeit der Umgebung führt zu Testergebnissen, die sich nur schwer reproduzieren lassen.
Test- und Archivierungsbefehle trennen
Nachdem die UDID ermittelt wurde, starten Sie zunächst das Gerät und warten Sie, bis dessen Initialisierung abgeschlossen ist. bootstatus -b verhindert, dass die Tests beginnen, bevor die Systemdienste einsatzbereit sind.
xcrun simctl boot "$SIM_UDID" 2>/dev/null || true
xcrun simctl bootstatus "$SIM_UDID" -b
xcodebuild test \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-resultBundlePath "Artifacts/TestResults.xcresult"
Bei der Archivierung wird dagegen überhaupt kein Simulator referenziert:
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "Artifacts/App.xcarchive"
Diese Trennung schafft zudem klarere Zuständigkeitsgrenzen: Die Testphase verwaltet den Simulatorzustand, während sich die Archivierungsphase ausschließlich mit dem Projekt, der Signierungskonfiguration und dem Ausgabepfad befasst. Selbst wenn beide Schritte auf demselben SetMini Cloud Mac ausgeführt werden, sollten sie in zwei getrennten Protokollabschnitten festgehalten werden.
Auflösungsergebnis als prüfbaren Nachweis festhalten
Ein erfolgreicher Befehl allein reicht nicht aus. Die Pipeline sollte Scheme, DEVELOPER_DIR, Gerätename, UDID, Runtime, Aktionstyp und Pfad des Ergebnis-Bundles in einer einfachen Text- oder JSON-Datei speichern. Bei der Fehleranalyse sollten zunächst diese Fakten verglichen werden, bevor mögliche Codeänderungen untersucht werden.
In der Beendigungsphase sollten nur Geräte heruntergefahren werden, die vom aktuellen Auftrag gestartet wurden:
cleanup() {
if [[ -n "${SIM_UDID:-}" ]]; then
xcrun simctl shutdown "$SIM_UDID" 2>/dev/null || true
fi
}
trap cleanup EXIT
Fahren Sie auf gemeinsam genutzten Workern nicht bedingungslos alle Simulatoren herunter und verwenden Sie erase all nicht als reguläre Bereinigungsmaßnahme. Ersteres unterbricht parallel laufende Aufträge. Letzteres verschleiert die Ursache verunreinigter Zustände und verlängert die anschließende Initialisierung.
Die abschließende Prüfung lässt sich auf fünf Punkte reduzieren: Archive verwenden ein generisches Ziel; Tests verwenden die für den aktuellen Auftrag dynamisch aufgelöste UDID; es darf nur einen Kandidaten geben; Xcode und Runtime werden vor der Ausführung protokolliert; beim Beenden werden ausschließlich Geräte des aktuellen Auftrags behandelt. Damit wird die Destination von einem impliziten Umgebungszustand zu einer überprüfbaren und reproduzierbaren Build-Eingabe.
Häufig gestellte Fragen
Braucht ein iOS-Archiv einen bestimmten Simulator?
Nein. Verwenden Sie generic/platform=iOS, damit der Archivauftrag nicht von einem Simulator abhängt, der sich nach einem Runtime-Update ändern kann.
Sollte eine Simulator-UDID dauerhaft in der CI-Konfiguration stehen?
Nein. Ermitteln Sie die UDID zu Beginn jedes Auftrags anhand von Gerätemodell und Runtime und protokollieren Sie anschließend den tatsächlich verwendeten Wert.
Wie behandelt man mehrere Simulatoren mit demselben Namen?
Filtern Sie zuerst verfügbare Geräte und die gewünschte Runtime. Bleiben mehrere Kandidaten übrig, muss der Auftrag mit einer Kandidatenliste abbrechen.
Entwicklungsschritte in eine jederzeit erreichbare Cloud-Mac-Umgebung verlagern
Wählen Sie zwischen zwei Apple-Silicon-Konfigurationen und vier Rechenzentren. Die Ressourcen werden nicht mit anderen Mietern geteilt; maßgeblich ist der aktuelle Status in der Konsole.