Technisches Runbook

Xcode-Ersteinrichtung für Cloud-Mac-CI

Xcode-Ersteinrichtung für Cloud-Mac-CI

Auf einen Cloud-Mac lässt sich bereits per SSH zugreifen und auch xcodebuild -version gibt eine Version zurück. Trotzdem kann der erste unbeaufsichtigte Job weiterhin an der Lizenzannahme, der Installation zusätzlicher Komponenten oder der Auswahl des Tool-Pfads hängen bleiben. Besonders problematisch ist, dass solche Fehler häufig erst bei neuen Nodes, nach einem Xcode-Upgrade oder nach dem Neuaufbau eines Runners auftreten. Wird Xcode einmal manuell über die grafische Oberfläche geöffnet, scheint das Problem vorübergehend verschwunden zu sein. Zuverlässiger ist es, die Ersteinrichtung als eigenständigen Schritt der Node-Bereitstellung zu behandeln, statt die Umgebung während eines regulären Build-Jobs reparieren zu lassen.

Node-Initialisierung und Build-Job getrennt behandeln

Die Node-Initialisierung darf den Systemzustand verändern, benötigt Administratorrechte und muss für jede Xcode-Version nur einmal ausgeführt werden. Build-Jobs sollten dagegen wiederholbar und nicht interaktiv sein und möglichst ohne Befehle mit erhöhten Rechten auskommen. Werden beide Abläufe in demselben CI-Skript vermischt, entstehen drei Probleme: Mehrere Jobs installieren gleichzeitig Komponenten, unbeaufsichtigte sudo-Aufrufe warten auf Eingaben und bei einem Fehler lässt sich nicht erkennen, ob der Code oder eine unvollständig vorbereitete Umgebung die Ursache ist.

Es empfiehlt sich, den Lebenszyklus eines Runners in drei Phasen zu unterteilen:

  1. Xcode installieren oder auf die gewünschte Version umstellen sowie Lizenzannahme und erstmalige Komponenteninitialisierung abschließen.
  2. Einen schreibgeschützten Preflight-Check ausführen und das tatsächlich verwendete Developer-Verzeichnis sowie die Tool-Versionen protokollieren.
  3. Den Runner erst nach erfolgreichem Preflight-Check für Jobs freigeben.

„Xcode lässt sich öffnen“ ist kein Abnahmekriterium. Entscheidend ist, dass der Runner-Benutzer in einer nicht interaktiven Shell die Build-Werkzeuge findet, SDKs abfragen und ein minimales Projekt erfolgreich einlesen kann.

Den tatsächlich verwendeten Xcode-Pfad festlegen

Sind auf einem System mehrere Xcode-Versionen installiert, sollte man sich nicht ausschließlich auf das globale xcode-select verlassen. Es beeinflusst andere Jobs auf demselben physischen Node. Nach einem Versionswechsel kann zudem der irreführende Eindruck entstehen, dass im Protokoll eine Version ausgewiesen wird, tatsächlich aber eine andere zum Einsatz kommt. Für CI ist es besser, DEVELOPER_DIR auf Job-Ebene festzulegen.

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

printf 'developer_dir=%s
' "$DEVELOPER_DIR"
xcodebuild -version
xcrun --find clang
xcrun --find simctl
xcodebuild -showsdks

Wird ein Anwendungsverzeichnis mit Versionsnummer verwendet, sollte der Pfad in der Runner-Konfiguration oder in einer versionierten Umgebungsdatei hinterlegt werden, statt ihn in mehrere Skripte zu kopieren. Bei einem Xcode-Update muss dann nur eine zentrale Stelle geändert werden; der Preflight-Check sollte den Pfad ausgeben. So lässt sich bei der Protokollanalyse zuerst die Identität der Toolchain bestätigen, bevor das eigentliche Projekt untersucht wird.

GUI-Zustand ersetzt keine Prüfung in der Shell

Umgebungsvariablen, Standard-Shell und Berechtigungskontext einer Remote-Desktop-Sitzung können von denen des Runner-Dienstes abweichen. Sämtliche Prüfungen müssen daher unter demselben Benutzerkonto ausgeführt werden, das auch den Build startet. Insbesondere ist zu kontrollieren, ob das Ergebnis von xcrun --find unterhalb des erwarteten DEVELOPER_DIR liegt. Die bloße Existenz des Anwendungsverzeichnisses reicht nicht aus.

Ersteinrichtung einmalig abschließen

Während der Node-Vorbereitung wird zunächst der Status der Ersteinrichtung geprüft. Anschließend übernimmt ein Initialisierungsprozess mit Administratorrechten die Lizenzannahme und die Vorbereitung der Komponenten:

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

if ! xcodebuild -checkFirstLaunchStatus; then
  sudo xcodebuild -license accept
  sudo xcodebuild -runFirstLaunch
fi

xcodebuild -checkFirstLaunchStatus

Entscheidend sind hier nicht die Befehle selbst, sondern der Zeitpunkt ihrer Ausführung. Sie gehören nicht in ein Build-Skript, das bei jedem Commit gestartet wird. runFirstLaunch kann gemeinsam genutzte Komponenten verändern. Wenn zwei Jobs den Befehl gleichzeitig ausführen, kostet das nicht nur unnötig Zeit, sondern kann auch dazu führen, dass einer der Jobs einen noch unvollständigen Zustand liest.

Auch die Lizenzannahme darf nicht durch das Anklicken eines Dialogfensters erledigt werden. In einer automatisierten Umgebung muss sie ausdrücklich ausgeführt, der Exit-Code geprüft und die Annahme weiterer Jobs bei einem Fehler gestoppt werden. Erfordern Administratorbefehle interaktive Eingaben, sind diese während der Node-Bereitstellung zu behandeln. Ein Hintergrund-Runner darf nicht auf eine solche Eingabe warten.

Gleichzeitige Initialisierung mit einer atomaren Sperre verhindern

Bei automatischer Skalierung oder dem gleichzeitigen Start mehrerer Dienste kann ein Initialisierungsskript parallel aufgerufen werden, selbst wenn es nur für eine einmalige Ausführung vorgesehen ist. Mit den macOS-Standardwerkzeugen lässt sich über das Anlegen eines Verzeichnisses eine atomare Sperre umsetzen:

set -euo pipefail

lock_dir="/tmp/xcode-bootstrap.lock"

if ! mkdir "$lock_dir" 2>/dev/null; then
  echo "Xcode bootstrap is already running" >&2
  exit 75
fi

cleanup() {
  rmdir "$lock_dir" 2>/dev/null || true
}
trap cleanup EXIT

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
xcodebuild -checkFirstLaunchStatus

mkdir ist innerhalb desselben Dateisystems atomar, sodass nur ein Prozess erfolgreich sein kann. Der Exit-Code 75 kann von der Orchestrierungsebene als vorübergehender Fehler interpretiert werden, der einen späteren Wiederholungsversuch auslöst, statt den Node dauerhaft als fehlerhaft zu markieren.

Auch veraltete Sperren nach einem unerwarteten Abbruch müssen berücksichtigt werden. Sie können während des Node-Starts entfernt werden, nachdem sichergestellt wurde, dass kein Initialisierungsprozess mehr läuft. Alternativ lässt sich das Sperrverzeichnis an einem Ort anlegen, dessen Inhalt bei einem Neustart gelöscht wird. Eine Sperre darf nicht ohne Prüfung entfernt werden, da sonst eine noch laufende Initialisierung gestört werden könnte.

Preflight-Prüfung vor der Job-Freigabe einrichten

Nach erfolgreicher Prüfung des Ersteinrichtungsstatus folgt eine Abnahme, die den Systemzustand nicht verändert. Mindestens abgedeckt werden sollten die Xcode-Identität, die SDK-Abfrage, die Simulatorwerkzeuge und der Projekteinstiegspunkt:

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

xcodebuild -checkFirstLaunchStatus
xcodebuild -version
xcodebuild -showsdks >/tmp/xcode-sdks.txt
xcrun simctl list runtimes
xcodebuild -list -workspace "Example.xcworkspace"

In der letzten Zeile muss der tatsächliche Workspace oder das tatsächliche Projekt des Repositorys eingetragen werden. Diese Prüfung ist schneller als ein vollständiger Archive-Build, erkennt aber frühzeitig einen fehlenden Projekteinstiegspunkt, unerfüllte Voraussetzungen für die Abhängigkeitsauflösung oder ein falsches Arbeitsverzeichnis des Runners.

Welche Diagnosedaten bei Fehlern aufbewahrt werden sollten

Die Diagnoseprotokolle sollten mindestens DEVELOPER_DIR, xcodebuild -version, xcrun --find clang, xcrun simctl list runtimes und den Exit-Code des fehlgeschlagenen Befehls enthalten. Es sollte nicht der vollständige Satz aller Umgebungsvariablen protokolliert werden, da darin Tokens, Signaturpasswörter oder Repository-Zugangsdaten enthalten sein können.

Die übliche Prüfungsreihenfolge lautet: zuerst den Pfad bestätigen, dann den Ersteinrichtungsstatus prüfen, anschließend die benötigten SDKs und Runtimes kontrollieren und erst danach die Projektkonfiguration untersuchen. Nach einem Wechsel der Xcode-Version muss dieser Ablauf für das neue Developer-Verzeichnis erneut ausgeführt werden, selbst wenn die vorherige Version bereits initialisiert war. Abschließend sollten die Version des Initialisierungsskripts und die Xcode-Version gemeinsam dokumentiert werden. So lässt sich der Runner bei einem Neuaufbau in derselben Reihenfolge wiederherstellen, ohne davon abhängig zu sein, dass jemand zuvor manuell eine grafische Oberfläche geöffnet hat.

Häufig gestellte Fragen

Warum startet Xcode interaktiv, während der CI-Job noch einen unvollständigen Erststart meldet?

Interaktive Sitzung und Runner können unterschiedliche Xcode-Installationen auflösen. Die Prüfung muss mit demselben DEVELOPER_DIR wie der Build erfolgen.

Soll jeder CI-Job sudo xcodebuild -runFirstLaunch ausführen?

Nein. Der Befehl gehört einmalig in die Bereitstellung des Knotens oder in den Versionswechsel. Build-Jobs sollten nur lesende Prüfungen ausführen.

Ist nach einem Wechsel der Xcode-Version eine neue Initialisierung nötig?

Ja. Lizenzstatus, Werkzeugkomponenten und Simulator-Runtimes müssen für das neue Developer-Verzeichnis erneut geprüft werden.

MacVPSGo Cloud-Mac

Benötigen Sie eine dedizierte Apple-Silicon-Build-Maschine?

Mieten Sie dedizierte physische Knoten tage-, wochen-, monats- oder quartalsweise für Xcode-Builds, iOS-CI, Remote-Entwicklung und Automatisierungsaufgaben.

Konfiguration auswählen und bestellen