Engineering-Praxis

Inkompatible Framework-Änderungen vor dem Merge erkennen

Inkompatible Framework-Änderungen vor dem Merge erkennen

Inkompatible Framework-Änderungen vor dem Merge erkennen

Wird ein Swift Framework von mehreren Apps verwendet, können bereits das Entfernen einer öffentlichen Methode, das Einschränken eines Parametertyps oder ein neues Pflichtmitglied in einem öffentlichen Protokoll dazu führen, dass nachgelagerte Projekte nach einem Upgrade nicht mehr kompilieren. Unit-Tests prüfen in der Regel das Verhalten der Implementierung, weisen aber nicht ausdrücklich darauf hin, dass eine Änderung die öffentliche API beschädigt. Auf den cloudbasierten Mac-Build-Knoten von OnceMac lässt sich Swift API Digester in die Merge-Prüfung integrieren: Zuerst wird ein API-Snapshot der veröffentlichten Version gespeichert, anschließend mit exakt derselben Toolchain ein Snapshot des Änderungskandidaten erzeugt und zuletzt werden beide miteinander verglichen.

Zuerst die Vergleichsbedingungen festlegen

Ein API-Snapshot wird nicht allein durch den Quellcode bestimmt. Auch die Xcode-Version, der Swift-Compiler, das SDK, die Zielarchitektur, die Build-Konfiguration und die Flags für bedingte Kompilierung beeinflussen das Ergebnis. Deshalb sollten diese Eingaben zunächst protokolliert und festgeschrieben werden, bevor der eigentliche Vergleich beginnt.

set -euo pipefail

xcodebuild -version
xcrun swift --version
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
echo "SDK_PATH=$SDK_PATH"

Die CI sollte Xcode explizit auswählen und für den Baseline- sowie den Kandidaten-Branch dasselbe Image oder dieselbe Knotenvorlage verwenden. Auch das Ziel-Tripel muss identisch sein, beispielsweise einheitlich arm64-apple-ios17.0-simulator. Erzeugen Sie nicht auf einer Seite eine Schnittstelle für ein physisches Gerät und auf der anderen eine für den Simulator, da Unterschiede in Plattformbedingungen sonst fälschlich als Codeänderungen erkannt werden.

Die API-Baseline ist ein Release-Vertrag und kein Build-Cache. Sie gehört in die Versionsverwaltung, muss geprüft werden und darf nicht automatisch von regulären Build-Jobs überschrieben werden.

Ein analysierbares Framework erstellen

Das folgende Beispiel setzt voraus, dass sowohl das Scheme als auch das Modul MyKit heißen. Zunächst werden separate Derived Data bereinigt, anschließend wird das Projekt mit der Release-Konfiguration gebaut. BUILD_LIBRARY_FOR_DISTRIBUTION=YES erzeugt die für Modulstabilität benötigten Schnittstellendateien und bildet damit die Bedingungen einer Binärdistribution genauer ab.

DERIVED_DATA="$PWD/.build/api-dd"
PRODUCTS="$DERIVED_DATA/Build/Products/Release-iphonesimulator"

rm -rf "$DERIVED_DATA"

xcodebuild build \
  -scheme MyKit \
  -configuration Release \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_DATA" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO \
  CODE_SIGNING_ALLOWED=NO

Prüfen Sie nach dem Build zunächst, ob das Modul tatsächlich vorhanden ist. So vermeiden Sie schwer verständliche „module not found“-Fehler, die entstehen, wenn Digester im falschen Suchpfad arbeitet.

test -d "$PRODUCTS/MyKit.framework"
find "$PRODUCTS/MyKit.framework/Modules" -maxdepth 3 -type f -print

Verwaltet das Projekt seine Abhängigkeiten über einen Workspace, muss zusätzlich -workspace angegeben werden. Ist das Scheme nicht freigegeben, kann die CI es nicht finden. Geben Sie es in den Projekteinstellungen frei, statt das Skript Pfade erraten zu lassen.

API-Snapshots erzeugen und vergleichen

Bauen Sie die aktuelle Release-Baseline und den Änderungskandidaten jeweils einmal und geben Sie die Ergebnisse anschließend als JSON aus. Die Baseline-Datei sollte im Repository unter api-baselines/ liegen und die Plattform im Dateinamen enthalten. Maschinenpfade oder Build-Nummern gehören nicht in den Namen.

mkdir -p api-baselines .build/api-report

xcrun swift-api-digester \
  -dump-sdk \
  -module MyKit \
  -sdk "$SDK_PATH" \
  -target arm64-apple-ios17.0-simulator \
  -F "$PRODUCTS" \
  -o ".build/api-report/current-ios-simulator.json"

xcrun swift-api-digester \
  -diagnose-sdk \
  -input-paths "api-baselines/MyKit-ios-simulator.json" \
  -input-paths ".build/api-report/current-ios-simulator.json" \
  > ".build/api-report/diagnostics.txt"

Kopieren Sie bei der erstmaligen Einrichtung die geprüfte Datei current-ios-simulator.json als Baseline und committen Sie sie. Danach liest die CI diese Datei nur noch ein. Liefert der Befehl einen Status ungleich null zurück, sollte der vollständige Diagnosetext als Build-Artefakt erhalten bleiben, statt lediglich die Meldung „Kompatibilitätsprüfung fehlgeschlagen“ anzuzeigen.

Welche Änderungen besonders sorgfältig geprüft werden müssen

Änderung Standardbewertung Vorgehensweise
Öffentlichen Typ oder öffentliche Methode entfernen Inkompatibel Schnittstelle wiederherstellen oder eine explizite neue Hauptversion planen
Parameter, Rückgabewert oder generische Einschränkung ändern Inkompatibel Kompatiblen Overload bereitstellen und einen Deprecation-Zeitraum einplanen
Einem öffentlichen Protokoll ein Pflichtmitglied hinzufügen Hohes Risiko Eine Standardimplementierung in Betracht ziehen
Öffentliche Methode oder öffentlichen Typ hinzufügen In der Regel kompatibel Benennung, Sichtbarkeit und Plattformattribute prüfen
Nur die interne Implementierung ändern Sollte nicht erscheinen Prüfen, ob die Zugriffsebene versehentlich erweitert wurde

public bedeutet nicht immer, dass eine Schnittstelle bewusst zugesichert wurde. Soll eine Deklaration nicht von außen aufgerufen werden, ist es besser, ihre Zugriffsebene einzuschränken, als dauerhaft Ausnahmeregeln zu pflegen.

Diagnoseergebnisse in ein wartbares CI-Gate überführen

Es empfiehlt sich, den Workflow in vier Schritte aufzuteilen: „Build“, „Snapshot erzeugen“, „Vergleichen“ und „Bericht hochladen“. Aktivieren Sie im Skript set -euo pipefail und legen Sie Derived Data in einem jobspezifischen Verzeichnis ab, damit parallele Jobs nicht gegenseitig ihre Artefakte lesen. Für Repositorys mit mehreren Modulen sollte eine Modulliste gepflegt und nacheinander abgearbeitet werden, statt den PRODUCTS-Pfad des vorherigen Moduls wiederzuverwenden.

Schlägt das Gate fehl, benötigen Reviewer drei Informationen: die verwendeten Xcode- und Swift-Versionen, den Baseline- und den Kandidaten-Snapshot sowie den vollständigen Diagnosetext. Die Baseline darf nur dann im selben Merge Request aktualisiert werden, wenn bestätigt wurde, dass die Änderung mit der Versionsstrategie übereinstimmt. Darf ein fehlgeschlagener Job seine Baseline selbst neu schreiben, wird die Prüfung immer erfolgreich sein und damit ihren Zweck verlieren.

Reihenfolge beim Untersuchen von False Positives

Prüfen Sie zuerst die Toolchain, danach das SDK und das Ziel-Tripel, anschließend die Build-Parameter und Flags für bedingte Kompilierung. Erst zuletzt sollte untersucht werden, ob es sich lediglich um Rauschen in der Digester-Ausgabe handelt. Mit der folgenden Checkliste lässt sich die Fehlersuche verkürzen:

  1. Ist die Ausgabe von xcodebuild -version vollständig identisch?
  2. Stimmen Scheme, Konfiguration und Destination überein?
  3. Ist BUILD_LIBRARY_FOR_DISTRIBUTION auf beiden Seiten aktiviert?
  4. Verweist der Modulsuchpfad auf die Artefakte des aktuellen Jobs?
  5. Stammt die Baseline von der zuletzt veröffentlichten Schnittstelle und nicht von einem beliebigen historischen Commit?
  6. Enthalten die erzeugten Dateien veränderliche absolute Pfade, etwa das Arbeitsverzeichnis?

Mit einer Versionsstrategie abschließen

Das Kompatibilitäts-Gate erkennt lediglich Änderungen; es kann dem Team die Entscheidung über die Versionsnummer nicht abnehmen. Das Entfernen von Schnittstellen, Änderungen an öffentlichen Typen oder neue Protokollanforderungen müssen in der Regel als inkompatible Version geplant werden. Auch neue Schnittstellen benötigen eine Prüfung ihrer Benennung und Verfügbarkeit. Als veraltet markierte Schnittstellen sollten für den vereinbarten Zeitraum erhalten bleiben und erst in einer späteren Version entfernt werden.

Am zuverlässigsten ist folgender Ablauf: Der Release-Branch speichert die Baseline, Merge Requests erzeugen ausschließlich Kandidaten-Snapshots, die CI liefert maschinenlesbare Diagnosen und die Maintainer entscheiden anhand der Versionsstrategie. Dadurch werden weder pauschal alle API-Änderungen untersagt noch wird eine unbeabsichtigte public-Änderung direkt an sämtliche nachgelagerten Projekte weitergegeben.

Häufig gestellte Fragen

Ersetzt Swift API Digester die Unit-Tests?

Nein. Das Werkzeug prüft strukturelle Änderungen an öffentlichen Swift-Schnittstellen, aber weder Geschäftslogik noch Laufzeitverhalten. Beide Prüfarten müssen parallel laufen.

Wann darf die API-Baseline aktualisiert werden?

Erst nachdem die Änderung gemäß Versionsstrategie geprüft und freigegeben wurde. Die Baseline gehört in denselben Review wie der Quellcode und darf nicht automatisch überschrieben werden.

Warum unterscheiden sich Ergebnisse bei identischem Quellcode?

Meist unterscheiden sich Xcode, Swift-Compiler, SDK, Zielarchitektur oder Build-Konfiguration. Baseline und Vergleich müssen mit derselben Werkzeugkette erzeugt werden.

Exklusiver physischer Mac mini

Führe deinen nächsten Build auf einem dedizierten physischen Knoten aus.

Wähle Chip, Arbeitsspeicher, Speicher, Knoten und Laufzeit und erhalte einen Cloud-Mac mit Ressourcen, die nicht mit anderen Kunden geteilt werden.

Konfiguration auswählen und mieten