Bei iOS-Projekten mit Notification-Service-, Share- oder Widget-Extensions tritt häufig folgendes Problem auf: Die Haupt-App lässt sich erfolgreich kompilieren und archivieren, doch erst beim Export, bei der Installation oder während der Veröffentlichungsprüfung wird die Extension als ungültig beanstandet. Die Ursache liegt meist nicht im Quellcode. Stattdessen sind Versionsnummern, Deployment Targets, Einbettungspfade oder Signaturen zwischen .app und .appex nicht mehr konsistent. Wird die Prüfung direkt an den Archivierungsschritt auf einem Cloud-Mac angeschlossen, liefert sie ein eindeutiges Ergebnis, bevor das Artefakt den Build-Knoten verlässt.
Abnahmekriterien für das Archiv festlegen
Eine App Extension ist kein eigenständiges Auslieferungsartefakt. Sie muss sich im Verzeichnis PlugIns der Haupt-App befinden und nachweisbar korrekt in diese eingebettet sein. Das CI sollte mindestens die folgenden Punkte prüfen:
| Prüfpunkt | Haupt-App | App Extension | Empfohlene Regel |
|---|---|---|---|
CFBundleVersion |
Erforderlich | Erforderlich | Vollständig identisch |
CFBundleShortVersionString |
Erforderlich | Erforderlich | Vollständig identisch |
MinimumOSVersion |
Erforderlich | Erforderlich | Bei einheitlicher Teamvorgabe identisch |
NSExtension |
Nicht erforderlich | Erforderlich | Dictionary ist vorhanden und lesbar |
| Codesignatur | Gültig | Gültig | Strikte Prüfung erfolgreich |
| Einbettungspfad | Applications |
PlugIns/*.appex |
Keine verstreuten Kopien zulassen |
Die Mindestversion des Betriebssystems muss nicht in jedem Projekt identisch sein. Wenn das Team jedoch nicht bewusst unterschiedliche Deployment Targets verwaltet, lassen sich Konfigurationsabweichungen durch eine strikte Gleichheitsprüfung leichter erkennen. Sind Unterschiede tatsächlich erforderlich, sollte der zulässige Bereich im Repository definiert werden, anstatt ihn im Skript stillschweigend zu ignorieren.
„Build erfolgreich“ bedeutet lediglich, dass jedes Target ein Artefakt erzeugen kann. Ein „abgenommenes Archiv“ muss zusätzlich belegen, dass diese Artefakte in der richtigen Beziehung zueinander eine auslieferbare App bilden.
Ein eindeutiges xcarchive für die Prüfung erzeugen
Das Gate sollte das Archiv prüfen und nicht ein Zwischenverzeichnis unter DerivedData. Dort kann noch eine .appex aus einem vorherigen Build liegen; außerdem bildet dieses Verzeichnis die endgültige Einbettungsstruktur nicht zuverlässig ab. Löschen Sie zunächst den festgelegten Archivpfad und führen Sie anschließend genau eine Release-Archivierung aus:
set -euo pipefail
ARCHIVE_PATH="$PWD/build/App.xcarchive"
rm -rf "$ARCHIVE_PATH"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE_PATH" \
clean archive
Unterschiedliche Branches dürfen nicht dasselbe Archivverzeichnis verwenden. Parallele Jobs können anhand des Commit-Hashs oder der CI-Jobnummer separate Pfade anlegen. An die nachfolgenden Skripte sollte jedoch nur ein eindeutig bestimmtes .xcarchive übergeben werden.
Einbettungsergebnis zuerst kontrollieren
Nach der Archivierung befindet sich die Haupt-App üblicherweise unter Products/Applications. Die Extensions liegen im Verzeichnis PlugIns innerhalb des App-Bundles. Prüfen Sie das tatsächliche Ergebnis zunächst mit find:
find build/App.xcarchive/Products/Applications \
\( -name '*.app' -o -name '*.appex' \) \
-print
Wenn im Projekt ein Extension-Target definiert ist, aber keine .appex gefunden wird, prüfen Sie zuerst die Build-Phase Embed App Extensions des Haupt-App-Targets. Stellen Sie außerdem sicher, dass die Extension vom aktuellen Scheme gebaut wird. Kopieren Sie keine Extension aus einem anderen Verzeichnis, um das Archiv nachträglich zu „vervollständigen“.
Metadaten und Signaturen per Skript prüfen
Das folgende Bash-Skript ist mit den in macOS enthaltenen Werkzeugen kompatibel. Es ermittelt die Haupt-App, liest die Info.plist jeder Extension aus, vergleicht Versionen und Mindestversion des Betriebssystems und prüft die Signaturen separat:
#!/bin/bash
set -euo pipefail
ARCHIVE="${1:?usage: validate-extensions.sh App.xcarchive}"
APP=$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)
if [ -z "$APP" ]; then
printf '%s
' "main app not found"
exit 1
fi
read_plist() {
/usr/libexec/PlistBuddy -c "Print :$2" "$1/Info.plist"
}
host_build=$(read_plist "$APP" CFBundleVersion)
host_version=$(read_plist "$APP" CFBundleShortVersionString)
host_minimum=$(read_plist "$APP" MinimumOSVersion)
count=0
failed=0
while IFS= read -r extension; do
count=$((count + 1))
ext_build=$(read_plist "$extension" CFBundleVersion)
ext_version=$(read_plist "$extension" CFBundleShortVersionString)
ext_minimum=$(read_plist "$extension" MinimumOSVersion)
[ "$ext_build" = "$host_build" ] || failed=1
[ "$ext_version" = "$host_version" ] || failed=1
[ "$ext_minimum" = "$host_minimum" ] || failed=1
/usr/libexec/PlistBuddy \
-c "Print :NSExtension" \
"$extension/Info.plist" >/dev/null
codesign --verify --strict --verbose=2 "$extension"
done < <(find "$APP/PlugIns" -maxdepth 1 -name '*.appex' -print)
[ "$count" -gt 0 ] || {
printf '%s
' "no app extensions found"
exit 1
}
codesign --verify --deep --strict --verbose=2 "$APP"
[ "$failed" -eq 0 ] || {
printf '%s
' "extension metadata mismatch"
exit 1
}
Speichern Sie das Skript als ci/validate-extensions.sh, führen Sie chmod +x aus und rufen Sie es nach dem Archivierungsschritt auf. Wenn das Repository sowohl ein Scheme „mit Extension“ als auch eines „ohne Extension“ erzeugt, muss die erwartete Anzahl der Extensions über einen Parameter angegeben werden. Null Extensions dürfen nicht pauschal als Erfolg gelten.
Versionsangaben in den Build-Einstellungen zentralisieren
Die häufigste Ursache für Abweichungen ist die manuelle Pflege mehrerer Info.plist-Dateien. Wird die Build Number der Haupt-App geändert, während die Extension den alten Wert behält, schlägt die Kompilierung des Quellcodes deswegen nicht fehl. Zuverlässiger ist es, für alle Targets dieselben Build-Variablen zu verwenden:
MARKETING_VERSION = 4.8.0
CURRENT_PROJECT_VERSION = 8204
IPHONEOS_DEPLOYMENT_TARGET = 17.0
Die zugehörigen plist-Werte verwenden $(MARKETING_VERSION), $(CURRENT_PROJECT_VERSION) und $(IPHONEOS_DEPLOYMENT_TARGET). Werden die Einstellungen über .xcconfig verwaltet, sollten Haupt-App und Extensions dieselbe Basisdatei einbinden. Nur Schlüssel, die tatsächlich abweichen müssen, werden anschließend in wenigen Target-spezifischen Dateien überschrieben.
Probleme nicht durch erneutes Signieren nach der Archivierung verdecken
codesign --deep eignet sich zur Prüfung des gesamten Bundles, nicht als universeller Reparaturbefehl. Rekursives Neusignieren nach der Archivierung kann die ursprünglichen Einbettungsbeziehungen verändern und dazu führen, dass das CI-Artefakt nicht mehr der Projektkonfiguration entspricht. Wenn die Signaturprüfung fehlschlägt, müssen die Signierungseinstellungen des Targets, die Build Phases und die Exportkonfiguration korrigiert werden. Anschließend ist ein vollständiges neues Archiv zu erzeugen.
Belege bei einem Fehler geordnet sichern
Nach einem fehlgeschlagenen Gate darf das Arbeitsverzeichnis nicht sofort bereinigt werden. Sichern Sie zunächst folgende Informationen:
- die relativen Pfade aller
.app- und.appex-Bundles im.xcarchive; - Versionsnummer, Mindestversion des Betriebssystems und Bundle Identifier jedes Bundles;
- die vollständige Ausgabe von
codesign --verify --strict --verbose=2; - das verwendete Scheme, die Configuration und die tatsächlich aufgelösten Build-Einstellungen;
- die Commit-Kennung und Xcode-Version des Archivierungsjobs.
Mit dem folgenden Befehl lassen sich die Build-Einstellungen exportieren. Dadurch können die Variablen verglichen werden, die Haupt-App und Extensions tatsächlich erhalten:
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings > build/build-settings.txt
Das endgültige Gate sollte zwei Anforderungen erfüllen: Die Fehlermeldung muss direkt benennen, welche .appex und welcher Schlüssel nicht übereinstimmen. Außerdem muss die Korrektur in der Projektkonfiguration erfolgen und nicht erst nach der Erzeugung des Artefakts. So bleiben die Abnahmekriterien unabhängig vom jeweils verwendeten Cloud-Mac-Knoten im Repository verankert, und das Archiv kann jederzeit erneut geprüft werden.
Häufig gestellte Fragen
Warum kann eine App Extension trotz erfolgreichem Xcode-Archiv fehlschlagen?
Beim Archivieren werden vor allem die Targets gebaut. Export und Installation validieren zusätzlich Versionen, Einbettung, Mindest-iOS, Extension-Metadaten und Signaturen.
Müssen App und Extension dieselbe Buildnummer verwenden?
Ja, CFBundleVersion sollte übereinstimmen. Auch CFBundleShortVersionString sollte aus derselben zentralen Build-Konfiguration stammen.
Sollte eine fehlerhafte Extension nach dem Archivieren neu signiert werden?
Nein. Korrigieren Sie Build-Einstellungen, Embed-Phase oder Signaturkonfiguration und erzeugen Sie anschließend ein sauberes neues Archiv.
Dedizierter physischer Knoten
Entwicklungs- und Build-Aufgaben auf einem dedizierten Cloud-Mac ausführen
Wählen Sie Modell, Knotenregion und Mietdauer passend zur Aufgabe. Jede Instanz läuft auf einem dedizierten physischen Rechner und nicht in einer virtuellen Umgebung.