Ein iOS-Projekt kann im lokalen Simulator einwandfrei laufen und dennoch erst bei der Installation Probleme zeigen, nachdem das Archive auf einem Cloud-Mac von HopVM erstellt wurde: Ein vorkompiliertes Framework enthält möglicherweise einen x86_64-Slice, eine Erweiterung wurde für die falsche Plattform gelinkt oder eine eingebettete dynamische Bibliothek hebt unbemerkt die Mindestversion des Betriebssystems an. Statt solche Fehler erst nach der Auslieferung zu untersuchen, sollte die exportierte .app direkt gescannt werden. So werden die tatsächlichen Binäreigenschaften zu einem verbindlichen Quality Gate in der Pipeline.
Warum das endgültige Exportpaket geprüft werden muss
Die Xcode-Projekteinstellungen zeigen lediglich, wie ein Build erstellt werden soll. Erst das endgültige Paket zeigt, was tatsächlich ausgeliefert wird. Hauptprogramm, App Extension, Frameworks und .dylib-Dateien können jeweils eigene Mach-O-Header besitzen. Auch von einem Dependency-Manager heruntergeladene Binärpakete übernehmen nicht zwangsläufig die Einstellungen des Hauptprojekts.
Das Quality Gate sollte mindestens drei Fragen beantworten:
- Enthalten die für reale iOS-Geräte bestimmten Binärdateien ausschließlich zulässige Architekturen?
- Ist die Plattform jeder Mach-O-Datei iOS und nicht der Simulator oder macOS?
- Liegt die Mindestversion des Betriebssystems einer eingebetteten Binärdatei über der Deklaration der Haupt-App?
Ein erfolgreiches Archive ist keine Abnahme für die Auslieferung. Der Linker prüft nur, ob sich das aktuelle Target zu einem Produkt zusammensetzen lässt. Er beurteilt nicht für das Team, ob alle eingebetteten Dateien die Veröffentlichungsanforderungen erfüllen.
Geprüft werden sollte das mit xcodebuild -exportArchive erzeugte endgültige Paket, nicht nur DerivedData. Beim Export werden Frameworks, Erweiterungen und Signaturinhalte neu zusammengestellt. Dieses Ergebnis entspricht daher am ehesten dem tatsächlichen Auslieferungszustand.
Eingaben und Abnahmekriterien fest vorgeben
Zunächst sollten die Verzeichnisse für Archive, Export und Bericht fest definiert werden, damit das Skript keine verbliebenen Dateien aus einem vorherigen Job scannt. Wenn mehrere Pipelines denselben Cloud-Mac nacheinander verwenden, ist eine saubere Trennung der Arbeitsverzeichnisse besonders wichtig.
set -euo pipefail
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_PATH="$PWD/output/export"
REPORT_PATH="$PWD/output/macho-report.tsv"
rm -rf "$EXPORT_PATH"
mkdir -p "$EXPORT_PATH"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$PWD/ci/ExportOptions.plist"
APP_PATH="$(find "$EXPORT_PATH" -maxdepth 2 -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
Die Produktanforderungen gehören in das Repository und sollten nicht über verschiedene CI-Konfigurationsoberflächen verteilt werden. Zulässige Architekturen und Mindestversion können beispielsweise in ci/macho-policy.env hinterlegt werden:
EXPECTED_ARCHS="arm64"
EXPECTED_PLATFORM="IOS"
DECLARED_MIN_IOS="17.0"
DECLARED_MIN_IOS muss dem tatsächlich unterstützten Versionsbereich des Projekts entsprechen. Die hier verwendete Version dient nur als Skriptbeispiel und darf nicht unverändert als Produktentscheidung übernommen werden.
Alle Mach-O-Dateien rekursiv erfassen
Eine Suche ausschließlich nach Dateiendungen reicht nicht aus, da die Hauptbinärdatei eines Frameworks üblicherweise keine Endung besitzt. Zuverlässiger ist es, alle Dateien zu durchlaufen und mit file festzustellen, ob es sich jeweils um eine Mach-O-Datei handelt.
: > "$REPORT_PATH"
failure=0
while IFS= read -r -d '' candidate; do
if ! file -b "$candidate" | grep -q 'Mach-O'; then
continue
fi
archs="$(lipo -archs "$candidate" 2>/dev/null || true)"
build="$(vtool -show-build "$candidate" 2>/dev/null || true)"
platform="$(awk '/platform/{print $2; exit}' <<<"$build")"
minos="$(awk '/minos/{print $2; exit}' <<<"$build")"
printf '%s %s %s %s
' \
"${candidate#"$APP_PATH"/}" "$archs" "$platform" "$minos" \
>> "$REPORT_PATH"
if [[ "$archs" != "$EXPECTED_ARCHS" ]]; then
printf 'architecture mismatch: %s (%s)
' "$candidate" "$archs" >&2
failure=1
fi
if [[ "$platform" != "$EXPECTED_PLATFORM" ]]; then
printf 'platform mismatch: %s (%s)
' "$candidate" "$platform" >&2
failure=1
fi
done < <(find "$APP_PATH" -type f -print0)
exit "$failure"
Der Bericht ist tabulatorgetrennt. Dadurch lässt er sich sowohl als Build-Artefakt aufbewahren als auch später unkompliziert in eine Tabelle umwandeln. Fehlermeldungen müssen den Dateipfad und den tatsächlich ermittelten Wert enthalten. Eine pauschale Meldung wie „Architekturprüfung fehlgeschlagen“ zwingt die zuständige Person andernfalls dazu, den gesamten Job erneut auszuführen.
Pakete nicht innerhalb des Quality Gates verändern
Nach dem Fund von x86_64 direkt lipo -remove auszuführen, wirkt zunächst praktisch. Dadurch werden jedoch bereits signierte Inhalte verändert und zugleich Probleme im Erstellungsprozess der Abhängigkeit verschleiert. Stattdessen muss ermittelt werden, ob die betreffende Datei aus einem Quellcode-Build, einer binären Abhängigkeit oder einem Kopierskript stammt. Anschließend wird die Ursache im vorgelagerten Prozess behoben und das Archive neu erstellt.
Mindestversion des Betriebssystems vergleichen
Zuerst wird die Produktdeklaration der Haupt-App ausgelesen:
PLIST="$APP_PATH/Info.plist"
APP_MIN_IOS="$(/usr/libexec/PlistBuddy \
-c 'Print :MinimumOSVersion' "$PLIST")"
printf 'declared minimum iOS: %s
' "$APP_MIN_IOS"
Anschließend wird für jede Mach-O-Datei der Wert minos aus LC_BUILD_VERSION gelesen. Für die Auswertung ist ein semantischer Versionsvergleich erforderlich. Ein gewöhnlicher Zeichenkettenvergleich ist ungeeignet, da er 17.10 fälschlicherweise vor 17.9 einordnen kann.
| Prüfobjekt | Ausgelesene Stelle | Fehlerbedingung |
|---|---|---|
| Deklaration der Haupt-App | MinimumOSVersion in Info.plist |
Stimmt nicht mit der Vorgabe im Repository überein |
| Hauptprogramm | minos in LC_BUILD_VERSION |
Liegt über der Produktdeklaration |
| Frameworks und dynamische Bibliotheken | Jeweiliges LC_BUILD_VERSION |
Liegt über der Produktdeklaration |
| App Extension | Info.plist und Mach-O der Erweiterung | Deklarierter oder tatsächlicher Wert überschreitet die Vorgabe |
Für den Vergleich kann die Versionssortierung des Systems verwendet werden:
version_gt() {
[[ "$1" != "$2" ]] &&
[[ "$(printf '%s
%s
' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
}
if version_gt "$minos" "$APP_MIN_IOS"; then
printf 'minimum iOS mismatch: %s requires %s
' \
"$candidate" "$minos" >&2
failure=1
fi
Wenn vtool keine Plattform oder Version zurückgibt, darf die Datei nicht stillschweigend übersprungen werden. Zunächst sollten mit otool -l die vollständigen Load Commands gespeichert werden. Danach lässt sich klären, ob es sich um eine Binärdatei im alten Format, eine fehlerhafte Datei oder eine falsche Erkennung durch das Skript handelt.
In CI integrieren und typische Fehlalarme behandeln
Die Prüfung gehört nach Archive und Export, aber vor den Upload. macho-report.tsv sollte immer hochgeladen werden. Auch bei einem fehlgeschlagenen Job muss der Bericht über den Mechanismus der CI-Plattform für Artefakte nach Fehlern erhalten bleiben.
Typische Fehlalarme lassen sich hauptsächlich in drei Kategorien einteilen:
- Gescannt wurden Symboldateien oder das Archive-Verzeichnis statt der endgültigen
.app. - Plattformnamen verwenden in den Ausgaben verschiedener Werkzeuge unterschiedliche Groß- und Kleinschreibung, werden aber direkt auf Zeichenkettengleichheit geprüft.
- Eine für macOS-Hilfsprogramme bestimmte Richtlinie wird auf eine iOS-App angewendet, wodurch zulässige Universal Binaries fälschlicherweise blockiert werden.
Die Richtlinien im Skript sollten daher nach Produkttyp getrennt verwaltet werden. Ein einzelner Regelsatz darf nicht gleichzeitig iOS-Produkte, Simulator-Testpakete und macOS-Werkzeuge abdecken. Erzeugt die Pipeline mehrere Produkttypen, muss die Prüfung für jedes Exportverzeichnis separat ausgeführt und der Produkttyp in die erste Spalte des Berichts geschrieben werden.
Zum Abschluss sollte die Signaturstruktur erneut geprüft werden:
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
Diese Prüfung ersetzt die Mach-O-Analyse nicht, kann aber nachgelagerte Probleme erkennen, etwa versehentlich durch ein Skript veränderte Binärdateien oder ungültig gewordene eingebettete Signaturen. Die vollständige Reihenfolge lautet: exportieren, scannen, Versionen vergleichen, Signaturen prüfen und den Bericht speichern. Erst danach darf das Produkt ausgeliefert werden. So lässt sich jeder Fehler einer konkreten Datei, einem konkreten Feld und einem konkreten Ansatzpunkt für die Korrektur zuordnen, statt erst nach einer fehlgeschlagenen Installation über die Ursache zu spekulieren.
Häufig gestellte Fragen
Warum reicht die Prüfung der Hauptdatei der App nicht aus?
Frameworks, App-Erweiterungen und dynamische Bibliotheken enthalten eigene Mach-O-Dateien. Bereits eine fehlerhafte eingebettete Datei kann Installation oder Start verhindern.
Was ist zu tun, wenn x86_64 im iOS-Paket auftaucht?
Ermitteln Sie die erzeugende Abhängigkeit und korrigieren Sie deren Build oder den Archivschritt. Der Slice sollte nicht erst unmittelbar vor der Auslieferung mit lipo entfernt werden.
Welche Angabe bestimmt die minimale iOS-Version?
Vergleichen Sie MinimumOSVersion aus Info.plist mit minos aus LC_BUILD_VERSION. Keine eingebettete Binärdatei darf eine höhere Version als die App voraussetzen.
Workflows mit einem cloudbasierten Mac reproduzieren, der projektbezogen gestartet und beendet wird
Wählen Sie aus drei Apple-Silicon-Konfigurationen die passende Ressource und mieten Sie sie tage-, wochen-, monats- oder quartalsweise. Die Geräte sind exklusive physische Maschinen, keine virtuellen Maschinen. Maßgeblich ist der in Echtzeit von der Konsole gemeldete Verfügbarkeitsstatus.