Dasselbe iOS-Projekt lässt sich lokal problemlos archivieren, kann in einer Release-Pipeline auf einem Cloud Mac jedoch plötzlich mit einer falschen Bundle-ID, einem Debug-URL-Scheme oder ohne Beschreibung für den Kamerazugriff gebaut werden. Die Ursache liegt meist nicht in der Info.plist im Repository, sondern im endgültigen Ergebnis, das Xcode nach dem Zusammenführen von Build-Einstellungen, generierten Konfigurationen und Target-Eigenschaften in die App schreibt. Eine zuverlässige CI sollte deshalb das gebaute Produkt prüfen, statt lediglich Quelldateien textuell zu kontrollieren.
Zuerst die tatsächlich ausgelieferte Konfiguration prüfen
Moderne Xcode-Projekte können die Info.plist automatisch generieren oder Werte aus verschiedenen Targets, Configurations und .xcconfig-Dateien beziehen. Die durch INFOPLIST_FILE referenzierte Datei ist nur eine der Eingaben. Einstellungen wie PRODUCT_BUNDLE_IDENTIFIER, MARKETING_VERSION und CURRENT_PROJECT_VERSION werden beim Build ebenfalls zusammengeführt.
Führe den Build zunächst mit eindeutig angegebenem Workspace, Scheme, Configuration und Ausgabeverzeichnis aus:
set -euo pipefail
ROOT="$(pwd)"
DERIVED_DATA="$ROOT/.ci/DerivedData"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED_DATA" \
CODE_SIGNING_ALLOWED=NO \
build
Das Deaktivieren der Signierung eignet sich hier nur für eine schnelle Konfigurationsprüfung. Es bedeutet nicht, dass die Signierung auch beim Release-Archiv deaktiviert werden sollte. Nach Abschluss des Builds sollte der App-Pfad nicht erraten werden. Produktverzeichnis und Produktname lassen sich direkt aus den Build-Einstellungen auslesen:
SETTINGS="$(mktemp)"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED_DATA" \
-showBuildSettings > "$SETTINGS"
BUILD_DIR="$(awk -F ' = ' '/ TARGET_BUILD_DIR = /{print $2; exit}' "$SETTINGS")"
WRAPPER_NAME="$(awk -F ' = ' '/ WRAPPER_NAME = /{print $2; exit}' "$SETTINGS")"
APP_PATH="$BUILD_DIR/$WRAPPER_NAME"
PLIST_PATH="$APP_PATH/Info.plist"
test -d "$APP_PATH"
test -f "$PLIST_PATH"
plutil -lint "$PLIST_PATH"
Geprüft werden muss die App, die im aktuellen Job erzeugt wurde. Wird ein fester Pfad aus einem früheren Build wiederverwendet, kann das Gate fälschlicherweise ein veraltetes Produkt bewerten.
Erwartungswerte in der Versionsverwaltung ablegen
Zahlreiche Erwartungswerte sollten nicht direkt in der Oberfläche der CI-Plattform fest codiert werden. Leichter überprüfbar ist eine kleine JSON-Datei pro Release-Umgebung, beispielsweise ci/plist-release.json:
{
"bundleIdentifier": "com.example.product",
"urlSchemes": ["example"],
"requiredUsageKeys": [
"NSCameraUsageDescription",
"NSPhotoLibraryUsageDescription"
],
"allowedBackgroundModes": ["remote-notification"]
}
Hier gehören nur Regeln hinein, die öffentlich im Repository gespeichert werden dürfen, nicht jedoch Token, private Schlüssel oder Signierungskennwörter. Versions- und Buildnummern werden üblicherweise aus Pipeline-Parametern erzeugt. Das Skript sollte daher ihr Format und ihre Übereinstimmung mit den Build-Eingaben prüfen, statt dauerhaft eine bestimmte Zahl festzuschreiben.
Unterschiedliche App-Targets, Erweiterungen und Test-Hosts benötigen jeweils eigene Regeln. Die Bundle-ID-Regel der Haupt-App darf ein Widget nicht fälschlich ablehnen. Ebenso sollte eine Erweiterung, die keinen Kamerazugriff benötigt, nicht dieselbe Prüfliste mit einer Kamerabeschreibung verwenden.
Ein Audit-Skript schreiben, das fehlschlagen kann
plutil -extract eignet sich zum Lesen von Dictionaries und Arrays, während PlistBuddy für einfache skalare Werte praktisch ist. Das folgende Skript zeigt ein minimales Grundgerüst:
#!/bin/bash
set -euo pipefail
PLIST_PATH="${1:?missing Info.plist path}"
EXPECTED_BUNDLE_ID="${EXPECTED_BUNDLE_ID:?missing bundle id}"
EXPECTED_VERSION="${EXPECTED_VERSION:?missing version}"
EXPECTED_BUILD="${EXPECTED_BUILD:?missing build number}"
read_key() {
/usr/libexec/PlistBuddy -c "Print :$1" "$PLIST_PATH" 2>/dev/null
}
require_key() {
local key="$1"
local value
value="$(read_key "$key" || true)"
if [[ -z "$value" ]]; then
printf 'Missing required key: %s
' "$key" >&2
exit 1
fi
}
[[ "$(read_key CFBundleIdentifier)" == "$EXPECTED_BUNDLE_ID" ]]
[[ "$(read_key CFBundleShortVersionString)" == "$EXPECTED_VERSION" ]]
[[ "$(read_key CFBundleVersion)" == "$EXPECTED_BUILD" ]]
require_key NSCameraUsageDescription
require_key NSPhotoLibraryUsageDescription
SCHEMES_JSON="$(plutil -extract CFBundleURLTypes json -o - "$PLIST_PATH")"
printf '%s' "$SCHEMES_JSON" | grep -q '"example"'
MODES="$(plutil -extract UIBackgroundModes raw -o - "$PLIST_PATH" 2>/dev/null || true)"
if [[ "$MODES" == *"audio"* ]]; then
printf 'Unexpected background mode: audio
' >&2
exit 1
fi
Ein Produktionsskript sollte zusätzlich Schlüsselname, Erwartungswert und tatsächlichen Wert ausgeben, jedoch nicht die vollständige plist protokollieren. Berechtigungstexte, abgefragte Schemes und Drittanbieterkonfigurationen können interne Kennungen enthalten. Werden sie unverändert in Logs hochgeladen, vergrößert sich die offengelegte Datenmenge.
Positive und negative Regeln gemeinsam definieren
Regeln nach dem Muster „muss vorhanden sein“ erkennen nur fehlende Konfigurationen, aber keine versehentlich übernommenen Debug-Einträge. Daher sollte zusätzlich eine Verbotsliste gepflegt werden. Release-Produkte dürfen beispielsweise keine Kennzeichnungen für Testserver, Debug-URL-Schemes, Dateifreigaben oder nicht genehmigte Hintergrundmodi enthalten.
Bei Array-Feldern sollte eine einfache Teilstring-Suche nicht die endgültige Implementierung sein. Robuster ist es, mit plutil -extract ... json JSON auszugeben und die Einträge anschließend mit Ruby, Python oder einem bereits im Projekt vorhandenen Skript einzeln zu vergleichen. Dabei müssen fehlende Felder, falsche Typen und doppelte Werte ausdrücklich behandelt werden.
App, Erweiterungen und Archiv in zwei Stufen prüfen
Ein Archiv kann neben der Haupt-App gleichzeitig Widgets, Notification-Service-Erweiterungen und weitere .appex-Bundles enthalten. Wer nur die Haupt-App prüft, übersieht möglicherweise falsche Kennungen, Versionsnummern oder Berechtigungskonfigurationen in Erweiterungen. Nach dem Archivieren lassen sich alle Bundles durchlaufen:
find "$ARCHIVE_PATH/Products/Applications" \
\( -name "*.app" -o -name "*.appex" \) -print0 |
while IFS= read -r -d '' bundle; do
plist="$bundle/Info.plist"
plutil -lint "$plist"
printf 'Auditing %s
' "$bundle"
done
Empfehlenswert sind zwei Gates: Nach einem normalen Release-Build erfolgt eine schnelle Prüfung für frühes Feedback. Nach Abschluss von archive werden die Produkte im Archiv erneut geprüft, bevor die Freigabe erteilt wird. Das zweite Gate darf nicht entfallen, da beim Archivieren eine andere Configuration, andere Exportparameter oder andere Pipeline-Variablen verwendet werden können.
| Prüfobjekt | Zentrale Felder | Vorgehen bei Fehlern |
|---|---|---|
| Haupt-App | Kennung, Version, Berechtigungstexte, URL-Scheme | Archivierung blockieren |
| App Extension | Kennungspräfix, Version, Erweiterungstyp | Archivierung blockieren |
| Endgültiges Archive | Alle Bundles und in Produktion verbotene Einträge | Veröffentlichung blockieren |
Häufige Fehlalarme vermeiden und Nachweise sichern
Die häufigsten Fehlalarme entstehen durch leere Zeichenfolgen, boolesche Datentypen und die Reihenfolge von Arrays. Bei Berechtigungstexten genügt es nicht, nur die Existenz des Schlüssels zu prüfen. Nach dem Entfernen führender und nachgestellter Leerzeichen muss der Wert weiterhin nicht leer sein. Boolesche Werte sollten anhand ihres plist-Typs gelesen werden; die Zeichenfolge "false" darf nicht als boolescher Wert behandelt werden. URL-Schemes und Hintergrundmodi werden üblicherweise als Mengen verglichen, ohne sich auf die Reihenfolge im Array zu verlassen.
Wenn das Skript fehlschlägt, sollte ein bereinigter Audit-Bericht aufbewahrt werden. Er enthält Scheme, Configuration, SDK, App-Pfad, den fehlgeschlagenen Schlüsselnamen und einen Hash. Weder die vollständige Build-Umgebung noch sämtliche Umgebungsvariablen sollten protokolliert werden. Führt derselbe Cloud Mac mehrere Jobs aus, benötigt jeder Job ein eigenes DerivedData. Vor dem Audit muss außerdem geprüft werden, ob der Änderungszeitpunkt des Produkts zum aktuellen Job gehört.
Das Audit-Skript sollte wie regulärer Code gepflegt werden: Regeländerungen durchlaufen einen Merge-Review, und für „fehlender Schlüssel“, „falscher Typ“, „verbotener Wert“ sowie „übersehene Erweiterung“ gibt es jeweils einen Testfall. So werden Probleme mit der Info.plist in der Pipeline zu eindeutigen, reproduzierbaren Fehlern, statt erst beim Archivieren oder Einreichen manuell untersucht werden zu müssen.
Häufig gestellte Fragen
Warum reicht die Prüfung der Info.plist im Repository nicht aus?
Xcode führt generierte Werte, Build-Einstellungen und konfigurationsabhängige Dateien zusammen. Maßgeblich ist die Info.plist in der tatsächlich gebauten App.
Welche Schlüssel sollte ein CI-Gate zuerst prüfen?
Beginnen Sie mit CFBundleIdentifier, Version, Build-Nummer, Berechtigungstexten, URL-Schemes und UIBackgroundModes. Fehlende oder in Produktion verbotene Werte führen zum Abbruch.
Soll das Audit vor oder nach dem Archivieren laufen?
Für schnelles Feedback läuft es nach einem normalen Build. Nach dem Archivieren wird die App im Archiv erneut geprüft; dieses Ergebnis entscheidet über die Freigabe.
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.