Engineering-Notizen von MiniDebug

Ein- und Ausgaben von Xcode Run Scripts im Cloud-Mac prüfen

Ein- und Ausgaben von Xcode Run Scripts im Cloud-Mac prüfen

Wenn ein Projekt auf dem lokalen Mac problemlos gebaut wird, nach der Verlagerung auf unbeaufsichtigte Cloud-Mac-Jobs jedoch Fehler zeigt, liegt die häufigste übersehene Ursache nicht bei den Compileroptionen, sondern in den Run Scripts unter Build Phases. Ein Skript liest möglicherweise Konfigurationen außerhalb des Repositorys, schreibt in das Quellverzeichnis oder wird bei jedem Build ausgeführt, weil keine Ausgaben deklariert sind. Bei einem einzelnen Durchlauf bleibt das Problem oft unbemerkt, während parallele Jobs Dateien gegenseitig überschreiben und inkrementelle Builds zunehmend wirkungslos werden. Solche Fehler sollten behoben werden, indem zuerst die Dateigrenzen jedes Skripts eindeutig definiert und anschließend mit aktivierter Sandbox überprüft werden – nicht durch zusätzliche Wiederholungsversuche.

Zunächst eine reproduzierbare Audit-Basis schaffen

Legen Sie zuerst Projekt, Scheme, Build-Konfiguration und Derived-Data-Verzeichnis fest. Verwenden Sie während des Audits nicht das reguläre Arbeitsverzeichnis wieder, da alte Artefakte fehlende Generierungsschritte fälschlicherweise als funktionsfähig erscheinen lassen können.

set -euo pipefail

ROOT="$PWD"
DERIVED="$ROOT/.audit-derived"

rm -rf "$DERIVED"

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED" \
  clean build | tee "$ROOT/audit-clean.log"

Wenn das Projekt einen Workspace verwendet, ersetzen Sie -project durch -workspace. Der erste Build dient dazu, die vollständige Build-Kette zu überprüfen. Lassen Sie den Quellcode danach unverändert und führen Sie einen weiteren Build ohne clean aus. Erfassen Sie, wie oft die Run Scripts ausgeführt werden, wie lange sie insgesamt benötigen und wann ihre Ausgabedateien zuletzt geändert wurden. Die beiden Protokolle bilden die Referenz für einen sauberen beziehungsweise einen inkrementellen Build.

Ziel des Audits ist nicht, dass sämtliche Skripte übersprungen werden. Jedes Skript soll vielmehr nur dann ausgeführt werden, wenn sich seine Eingaben geändert haben, Ausgaben fehlen oder eine ausdrücklich definierte Ausführungsbedingung dies verlangt.

Alle Run-Script-Phasen erfassen

Lokalisieren Sie die Skripte zunächst in der Projektdatei. Prüfen Sie anschließend in Xcode, zu welchem Target sie gehören, ob sie vor oder nach der Kompilierung ausgeführt werden und ob die Abhängigkeitsanalyse aktiviert ist. Eine Textsuche eignet sich für die erste Bestandsaufnahme:

grep -nE "PBXShellScriptBuildPhase|shellScript =|inputPaths =|outputPaths =" \
  App.xcodeproj/project.pbxproj

Erstellen Sie für jede Phase eine Tabelle und notieren Sie nicht nur den Skriptnamen. Häufig lautet dieser lediglich „Run Script“ und hilft bei der Fehlersuche daher nicht weiter.

Prüfaspekt Zu klärende Frage
Ausführungsbedingung Wird das Skript immer oder nur bei geänderten Abhängigkeiten ausgeführt?
Eingaben Welche Quelldateien, Konfigurationen, Werkzeuge und Dateilisten werden gelesen?
Ausgaben Wohin werden generierte Dateien, Berichte oder Abschlussmarkierungen geschrieben?
Nebeneffekte Werden Quellcode, globale Konfigurationen oder gemeinsam genutzte Caches verändert?
Parallelität Schreiben zwei gleichzeitig laufende Jobs in denselben Pfad?
Fehlerstrategie Wird nach dem Fehlschlagen eines Unterbefehls sofort ein Status ungleich null zurückgegeben?

Am Anfang jedes Skripts empfiehlt sich set -euo pipefail. Prüfen Sie außerdem Befehle in Pipelines: Ohne pipefail kann ein Fehler in einem früheren Befehl durch ein abschließend erfolgreiches tee verdeckt werden.

Dateigrenzen mit xcfilelist deklarieren

Eine kleine Anzahl von Pfaden kann direkt unter Input Files und Output Files eingetragen werden. Bei vielen Dateien sind .xcfilelist-Dateien leichter zu überprüfen. Pfade sollten nach Möglichkeit auf Build-Variablen wie $(SRCROOT) und $(DERIVED_FILE_DIR) basieren, statt fest codierte Benutzerverzeichnisse zu enthalten.

Eine Phase, die beispielsweise aus einer YAML-Konfiguration eine Prüfsumme erzeugt, kann folgende Eingabeliste verwenden:

$(SRCROOT)/Config/app.yml
$(SRCROOT)/Scripts/generate-config.sh

In der Ausgabeliste dürfen nur Dateien stehen, die das Skript tatsächlich erzeugt:

$(DERIVED_FILE_DIR)/Generated/config.sha256

Das zugehörige Skript sollte die temporäre Datei zunächst in das Zielverzeichnis schreiben und anschließend die endgültige Datei atomar ersetzen. So können parallele Prozesse keine unvollständige Datei lesen:

set -euo pipefail

SOURCE="$SRCROOT/Config/app.yml"
OUTPUT="$DERIVED_FILE_DIR/Generated/config.sha256"
TEMP="$OUTPUT.tmp.$$"

mkdir -p "$(dirname "$OUTPUT")"
shasum -a 256 "$SOURCE" > "$TEMP"
mv "$TEMP" "$OUTPUT"

Deklarieren Sie weder pauschal das gesamte Repository als Eingabe noch das Stammverzeichnis des Quellcodes als Ausgabe. Ein zu weit gefasster Bereich kann zwar Fehlermeldungen beseitigen, führt jedoch dazu, dass beliebige Dateiänderungen das Skript erneut auslösen, und verschleiert die tatsächlichen Abhängigkeiten. Generierte Artefakte sollten vorzugsweise in Derived Data geschrieben werden. Muss ein Schritt zur Codegenerierung zwingend in das Repository zurückschreiben, sollte er separat ausgeführt und das Ergebnis anschließend über die Versionsverwaltung auf Änderungen geprüft werden.

Sandbox aktivieren und Ablehnungsprotokolle auswerten

Nachdem die erste Runde der Deklarationen abgeschlossen ist, validieren Sie diese zunächst, indem Sie die Build-Einstellung über die Befehlszeile überschreiben. Es ist nicht erforderlich, sofort sämtliche Konfigurationen zu ändern:

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$PWD/.audit-derived" \
  ENABLE_USER_SCRIPT_SANDBOXING=YES \
  build | tee "$PWD/audit-sandbox.log"

Wenn eine Sandbox-Ablehnung auftritt, ermitteln Sie zuerst den abgelehnten Pfad, die Art des Vorgangs und die zugehörige Phase. Wird eine Konfiguration gelesen, ohne als Eingabe deklariert zu sein, ergänzen Sie sie in der Eingabeliste. Wird eine nicht deklarierte Datei erstellt oder verändert, fügen Sie sie der Ausgabeliste hinzu. Systembibliotheken, die ein Werkzeug für seine eigene Ausführung liest, müssen üblicherweise nicht durch die Aufnahme ganzer Systemverzeichnisse freigegeben werden. Konzentrieren Sie sich auf Projekt- und Konfigurationsdateien sowie selbst bereitgestellte Werkzeugpfade, auf die das Skript ausdrücklich zugreift.

Ein häufiger Fehler besteht darin, die Sandbox einfach zu deaktivieren oder das Benutzerverzeichnis vollständig als Eingabebereich einzutragen. Im ersten Fall bleiben implizite Abhängigkeiten bestehen, im zweiten entstehen unkontrollierbare Neubuilds. Benötigt ein Skript eine Datei außerhalb des Repositorys, sollte diese in das Arbeitsverzeichnis des Jobs kopiert, überprüft und anschließend als eindeutige Eingabe deklariert werden.

Inkrementelle und parallele Prüfungen in CI integrieren

Führen Sie nach den Korrekturen mindestens vier Testgruppen aus: einen sauberen Build mit leerem Derived Data, einen zweiten Build ohne Änderungen am Quellcode, einen Build nach Änderung genau einer deklarierten Eingabe sowie zwei parallele Builds mit getrennten Derived-Data-Verzeichnissen. Parallele Jobs dürfen eine schreibgeschützte Kopie des Quellcodes gemeinsam nutzen, nicht jedoch dasselbe Ausgabeverzeichnis.

Prüfen Sie die Ergebnisse in dieser Reihenfolge:

  1. Der saubere Build kann alle erforderlichen Artefakte vollständig neu erzeugen.
  2. Beim zweiten Build werden Skripte mit bereits vorhandenen, stabilen Ausgaben nicht bedingungslos erneut ausgeführt.
  3. Nach Änderung einer deklarierten Eingabe wird die zugehörige Phase erneut ausgeführt.
  4. Änderungen an nicht relevanten Dateien lösen die Phase nicht aus.
  5. Zwei parallele Jobs überschreiben weder dieselbe temporäre Datei noch denselben Bericht.
  6. Ein Skriptfehler führt dazu, dass xcodebuild einen Status ungleich null zurückgibt.
  7. Die Protokolle enthalten weder Token noch private Schlüssel oder vollständige Umgebungsvariablen.

Übernehmen Sie die Sandbox-Einstellung abschließend in die vom Team tatsächlich verwendete Build-Konfiguration und behalten Sie einen regelmäßig ausgeführten Clean-Build-Job bei. Inkrementelle Builds sorgen für Geschwindigkeit, während saubere Builds fehlende Abhängigkeiten sichtbar machen. Erst wenn beide Varianten erfolgreich sind, verfügen die Run Scripts über zuverlässig reproduzierbare Ausführungsgrenzen.

Häufig gestellte Fragen

Warum läuft eine Run-Script-Phase bei jedem Build?

Meist fehlen deklarierte Ausgabedateien oder die abhängigkeitsbasierte Ausführung ist deaktiviert. Stabile Ein- und Ausgaben ermöglichen Xcode die Entscheidung, ob die Phase erneut laufen muss.

Wie behebe ich eine Sandbox-Deny-Meldung?

Ermitteln Sie im Build-Protokoll den abgelehnten Pfad und die Zugriffsart. Deklarieren Sie den benötigten Pfad anschließend als Ein- oder Ausgabe, direkt oder über eine xcfilelist.

Wie lässt sich ein inkrementeller Build überprüfen?

Führen Sie denselben Build zweimal aus und ändern Sie danach genau eine deklarierte Eingabe. Ohne Änderung muss die Phase entfallen; nach der Änderung muss sie laufen und ihre Ausgabe aktualisieren.

Exklusiver physischer Mac

Den nächsten iOS-Build auf einem MiniDebug M4 ausführen.

M4, 16 GB RAM und 256 GB SSD sind standardmäßig verfügbar. Die Miete ist tage-, wochen-, monats- oder quartalsweise möglich. Zur Auswahl stehen fünf Standorte: Singapur, Tokio in Japan, Seoul in Südkorea, Hongkong und die US-Ostküste. Die tatsächliche Verfügbarkeit wird in Echtzeit von der Konsole angezeigt.

Standort auswählen und bestellen