Derselbe Commit lässt sich auf dem Entwickler-Mac erfolgreich archivieren und wird auch auf einem Cloud-Mac oder in der nächtlichen Pipeline als erfolgreich gemeldet, dennoch unterscheiden sich die Hashwerte der beiden Artefakte. Dafür sollte nicht vorschnell der Xcode-Cache verantwortlich gemacht werden. Signaturdaten, Komprimierungszeitpunkte, generierter Quellcode, die Ausgabereihenfolge von Skripten und absolute Pfade können allesamt in das Artefakt einfließen. Aussagekräftiger ist es, auf demselben ArmVMS-Knoten zwei vollständig isolierte Build-Verzeichnisse anzulegen, nacheinander zwei unsignierte Archive zu erzeugen und die Unterschiede anschließend von außen nach innen zu untersuchen.
Zuerst die Grenzen der Reproduzierbarkeit festlegen
Signierte IPA-Dateien lassen sich nur schwer vollständig Byte für Byte reproduzieren, da sich Signaturdaten, Provisioning-Profile und Paketmetadaten ändern können. Die Projektprüfung sollte deshalb in zwei Ebenen aufgeteilt werden:
- Inhaltsebene: Derselbe Commit muss mit identischer Toolchain, identischen Lockdateien für Abhängigkeiten und identischen Build-Parametern dieselben Anwendungsressourcen und ausführbaren Dateien erzeugen.
- Auslieferungsebene: Exportverfahren, Signaturkonfiguration, Berechtigungsdeklarationen und die im Paket enthaltenen Dateien müssen die Veröffentlichungsanforderungen erfüllen. Ein identischer Hashwert der gesamten IPA-Datei ist jedoch nicht zwingend erforderlich.
Zuerst muss die Stabilität der unsignierten Anwendungsinhalte nachgewiesen werden. Erst danach sollten Signatur und Export geprüft werden. Der Vergleich der Gesamthashwerte zweier IPA-Dateien bestätigt lediglich, dass sie unterschiedlich sind, erklärt aber nicht die Ursache.
Vor Beginn sollten der Commit-Hash, der Xcode-Pfad, das SDK, das Scheme, die Configuration sowie Lockdateien wie Package.resolved und Podfile.lock protokolliert werden. Der Workspace muss sauber sein. Nicht eingecheckte generierte Dateien sind ebenfalls in die Prüfung einzubeziehen.
Zwei Archive in isolierten Verzeichnissen erzeugen
Die beiden Builds dürfen nicht dasselbe DerivedData-Verzeichnis verwenden. Andernfalls könnte der zweite Build auf Zwischenprodukte des ersten Builds zurückgreifen. Das folgende Skript legt Sprachumgebung und Zeitzone fest, weist jedem Archiv ein separates Verzeichnis zu und deaktiviert die Codesignierung:
#!/bin/zsh
set -euo pipefail
export LC_ALL=C
export LANG=C
export TZ=UTC
ROOT="$PWD"
SCHEME="ExampleApp"
for RUN in A B; do
rm -rf "$ROOT/.repro/$RUN"
mkdir -p "$ROOT/.repro/$RUN"
xcodebuild archive \
-workspace "$ROOT/ExampleApp.xcworkspace" \
-scheme "$SCHEME" \
-configuration Release \
-destination "generic/platform=iOS" \
-derivedDataPath "$ROOT/.repro/$RUN/DerivedData" \
-archivePath "$ROOT/.repro/$RUN/App.xcarchive" \
CODE_SIGNING_ALLOWED=NO \
COMPILER_INDEX_STORE_ENABLE=NO
done
Wenn das Projekt während einer Skriptphase Umgebungsvariablen auslesen muss, sollten stabile Werte explizit übergeben werden. Temporäre Einstellungen aus der interaktiven Shell dürfen nicht ungeprüft übernommen werden. Beide Archivierungsläufe müssen außerdem dasselbe DEVELOPER_DIR verwenden, damit nicht zwischen unterschiedlichen standardmäßigen Xcode-Pfaden gewechselt wird.
Unterschiede anhand der Dateiliste eingrenzen
Binärdateien sollten nicht sofort hexadezimal verglichen werden. Zunächst ist zu prüfen, ob beide .app-Pakete dieselben Pfade enthalten:
APP_A=".repro/A/App.xcarchive/Products/Applications/ExampleApp.app"
APP_B=".repro/B/App.xcarchive/Products/Applications/ExampleApp.app"
find "$APP_A" -type f | sed "s#^$APP_A/##" | LC_ALL=C sort > /tmp/files-a.txt
find "$APP_B" -type f | sed "s#^$APP_B/##" | LC_ALL=C sort > /tmp/files-b.txt
diff -u /tmp/files-a.txt /tmp/files-b.txt
Eine unterschiedliche Anzahl von Dateien weist meist darauf hin, dass eine Run-Script-Phase, ein Ressourcengenerator oder eine Kopierregel von instabilen Eingaben abhängt. Stimmen die Pfade überein, werden anschließend die Hashwerte der einzelnen Dateien berechnet. Dabei sind Signaturverzeichnisse auszuschließen, von denen bekannt ist, dass sie sich ändern können:
(
cd "$APP_A"
find . -type f ! -path "./_CodeSignature/*" -print0 |
sort -z | xargs -0 shasum -a 256
) > /tmp/hash-a.txt
(
cd "$APP_B"
find . -type f ! -path "./_CodeSignature/*" -print0 |
sort -z | xargs -0 shasum -a 256
) > /tmp/hash-b.txt
diff -u /tmp/hash-a.txt /tmp/hash-b.txt
| Stelle des ersten Unterschieds | Vorrangig zu prüfen |
|---|---|
| Info.plist | Automatisch eingetragene Datumswerte, Hostnamen, Pfade oder Pipeline-Nummern |
| Assets.car | Eingaberessourcen, Toolchain-Version und Reihenfolge der Ressourcenverzeichnisse |
| Ausführbare Datei | Compilerparameter, generierter Quellcode, Objektdateien und Link-Reihenfolge |
| Framework | Lockdateien der Abhängigkeiten, Versionen binärer Abhängigkeiten und Einbettungsskripte |
| Text oder JSON | Unsortierte Mengen, zufällige Kennungen und regionale Formate |
Konfiguration und Mach-O-Dateien genauer untersuchen
Property Lists können allein aufgrund einer unterschiedlichen Schlüsselreihenfolge Byte-Abweichungen aufweisen. Die Dateien sollten zunächst kopiert und anschließend mit plutil -convert xml1 normalisiert werden, bevor ein Textvergleich erfolgt. Die ursprünglichen Archive dürfen nicht direkt verändert werden, da dies die Beweislage für spätere Untersuchungen verfälschen würde.
Wenn sich die ausführbaren Dateien unterscheiden, sollten zuerst ihre UUIDs geprüft werden:
dwarfdump --uuid "$APP_A/ExampleApp"
dwarfdump --uuid "$APP_B/ExampleApp"
xcodebuild \
-workspace ExampleApp.xcworkspace \
-scheme ExampleApp \
-configuration Release \
-showBuildSettings > /tmp/build-settings.txt
Unterschiedliche UUIDs bedeuten lediglich, dass sich die Link-Ergebnisse unterscheiden. Sie belegen nicht automatisch eine Änderung am Anwendungscode. Anschließend sollten OTHER_SWIFT_FLAGS, OTHER_LDFLAGS, Suchpfade, das Deployment Target und die tatsächlich verwendeten Tool-Pfade abgeglichen werden. Schreibt generierter Code das aktuelle Datum, ein temporäres Verzeichnis oder das Ergebnis einer ungeordneten Dictionary-Iteration fest, weichen auch alle nachfolgenden Kompilierungsergebnisse voneinander ab.
Besonders zu beachten sind #filePath, Debug-Mappings und absolute Workspace-Pfade in Skriptausgaben. Wenn sich das Arbeitsverzeichnis der CI ändert, können ein einheitlicher Checkout-Pfad oder sinnvoll konfigurierte Debug-Prefix-Mappings die Pfadabweichungen reduzieren. Für identische Hashwerte dürfen jedoch keine Informationen entfernt werden, die für das Debugging erforderlich sind.
Die Prüfung als wartbares Gate etablieren
Ein Gate sollte nicht lediglich verlangen, dass „die Hashwerte beider IPA-Dateien identisch sein müssen“. Robuster ist es, einen Differenzbericht aufzubewahren und Änderungen nach Ebenen zu bewerten:
Änderungen, die den Build blockieren müssen
Fehlen bei demselben Commit Dateien, ändern sich Ressourceninhalte oder Berechtigungsdeklarationen, wechselt die Version eines eingebetteten Frameworks oder ist die ausführbare Hauptdatei bei identischen Eingaben instabil, muss der Build sofort blockiert werden. Beide Archive sind für die weitere Untersuchung aufzubewahren.
Änderungen, die separat geprüft werden können
Signaturverzeichnisse, Exportprotokolle und ausdrücklich gekennzeichnete Paketierungszeitpunkte gehören zur Auslieferungsebene und können separat geprüft werden. Die Pfade, in denen Änderungen zulässig sind, müssen jedoch genau eingegrenzt werden. Zu weit gefasste Regeln wie „alle plist-Dateien ignorieren“ sind ungeeignet.
Nach einer Korrektur müssen für die erneute Prüfung wieder zwei leere Verzeichnisse angelegt werden, anstatt das bisherige DerivedData wiederzuverwenden. Bei großen Projekten ist eine doppelte Archivierung nicht für jeden Commit erforderlich. Sie kann gezielt ausgelöst werden, wenn sich die Xcode-Version, Lockdateien für Abhängigkeiten, Generatoren, Run-Script-Phasen oder die Veröffentlichungskonfiguration ändern. So entsteht keine fragile Hashregel, sondern eine nachvollziehbare Build-Beweiskette, die zeigt, auf welcher Ebene eine Abweichung aufgetreten ist.
Häufig gestellte Fragen
Warum reicht der Hash-Vergleich zweier signierter IPA-Dateien nicht aus?
Signaturen, Profile und Paketzeitpunkte können erwartbar abweichen. Vergleichen Sie zuerst den unsignierten App-Inhalt und prüfen Sie den Export getrennt.
Bedeuten unterschiedliche Mach-O-UUIDs immer unterschiedlichen Quellcode?
Nein. Auch die Reihenfolge von Objektdateien, generierter Code, Compilerargumente und Werkzeugpfade können das Linkergebnis verändern.
Muss die Reproduzierbarkeitsprüfung bei jedem Commit laufen?
Bei großen Projekten genügt sie meist bei Änderungen an Toolchain, Sperrdateien, Build-Skripten oder Veröffentlichungsprofilen.
Wählen Sie für Ihren nächsten Build einen exklusiven physischen Apple-Silicon-Knoten
Mieten Sie ArmVMS M4 und ArmVMS M4 Pro tage-, wochen-, monats- oder quartalsweise. Konfiguration, Knoten und Zusatzoptionen werden vor der Bestellung einzeln bestätigt.