Die iOS-Abhängigkeitskette auf einem Cloud-Mac prüfen

Die iOS-Abhängigkeitskette auf einem Cloud-Mac prüfen

Dass derselbe Commit gestern erfolgreich gebaut wurde, heute aber eine andere transitive Abhängigkeit bezieht, gehört zu den am schwersten nachvollziehbaren Problemen bei Builds auf einem Cloud-Mac. Oberflächlich zeigt sich der Fehler möglicherweise nur als Compilerfehler. Tatsächlich kann sich jedoch eine Paket-Revision, der heruntergeladene Inhalt oder eine neu hinzugefügte Skriptphase geändert haben. Ein erneuter Durchlauf reicht deshalb nicht aus. Jeder Build muss drei Fragen beantworten können: Was wurde aufgelöst, hat sich der Inhalt geändert und wurde diese Änderung geprüft?

Zuerst die erforderlichen Nachweise festlegen

Ein Abhängigkeitsaudit sollte sowohl direkte als auch transitive Abhängigkeiten von SwiftPM und CocoaPods erfassen. Sinnvoll ist eine Unterteilung der Nachweise in drei Ebenen:

Ebene Nachweise Beantwortete Frage
Deklaration Package.swift, Podfile, Referenzen im Xcode-Projekt Was möchte das Team einbinden?
Auflösung Package.resolved, Podfile.lock Welche Version oder Revision wurde tatsächlich fixiert?
Build Abhängigkeitsinventar, Datei-Hashes, Build-Protokolle Was wurde in diesem Build tatsächlich verwendet?

Package.resolved und Podfile.lock müssen in das Repository eingecheckt werden. Sie dürfen in CI weder ignoriert noch automatisch aktualisiert werden, während die Pipeline anschließend mit der Veröffentlichung fortfährt.

Eine Sperrdatei ist noch kein Sicherheitsurteil. Sie liefert lediglich stabile Eingabedaten. Erst Prüfung, Hashes und dokumentierte Unterschiede zeigen, warum diese Eingaben für einen Build zugelassen werden können.

Auf den dedizierten Knoten von ZoomMini kann das Arbeitsverzeichnis dauerhaft erhalten bleiben. Das Audit muss sich dennoch auf die Dateien im Repository stützen. Der aktuelle Zustand im Cache eines einzelnen Rechners darf nicht als alleiniger Nachweis gelten.

Auflösungsverzeichnis fixieren und sauber neu auflösen

Legen Sie zunächst das Downloadverzeichnis von SwiftPM im Workspace fest. So wird verhindert, dass verschiedene Runner globale Caches verwenden, die sich nicht zuverlässig vergleichen lassen. Führen Sie für ein Projekt mit Workspace Folgendes aus:

set -euo pipefail
ROOT="$(pwd)"
SPM_DIR="$ROOT/.audit/spm"
DERIVED_DIR="$ROOT/.audit/DerivedData"

rm -rf "$SPM_DIR" "$DERIVED_DIR"
mkdir -p "$SPM_DIR" "$DERIVED_DIR"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$SPM_DIR"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DIR" \
  -clonedSourcePackagesDirPath "$SPM_DIR" \
  build

Besteht das Projekt nur aus einer .xcodeproj, ersetzen Sie -workspace durch -project. Prüfen Sie unmittelbar nach der Auflösung, ob Dateien im Repository verändert wurden:

git diff --exit-code -- '**/Package.resolved' Podfile.lock

Schlägt dieser Befehl fehl, muss der Build gestoppt werden. Häufige Ursachen sind eine nach einem Abhängigkeitsupdate nicht eingecheckte Sperrdatei, eine durch eine andere Werkzeugversion neu geschriebene Dateistruktur oder Abhängigkeitsbedingungen, die eine neue transitive Version zulassen.

Grenzen der Toolchain protokollieren

Die Auditdaten sollten außerdem die Ausgaben von xcodebuild -version, swift --version und ruby --version sowie die Versionen der Werkzeuge zur Abhängigkeitsverwaltung enthalten. Diese Angaben müssen nicht zwingend in das Abhängigkeitsinventar aufgenommen werden, sollten aber zusammen mit den Build-Protokollen archiviert werden. Andernfalls lässt sich bei identischen Sperrdateien und unterschiedlichem Auflösungsverhalten kaum feststellen, ob die Abweichung von der Toolchain oder vom Quellcode verursacht wurde.

Abhängigkeitsinventar aus Sperrdateien erzeugen

Das folgende Skript durchsucht das Repository nach Package.resolved. Es unterstützt sowohl die verbreitete Struktur mit pins auf der obersten Ebene als auch object.pins und erfasst Version, Branch, Revision sowie den Hash der Sperrdatei:

import glob
import hashlib
import json
import os

items = []

for path in glob.glob("**/Package.resolved", recursive=True):
    if "/.audit/" in f"/{path}":
        continue
    with open(path, "rb") as handle:
        raw = handle.read()
    data = json.loads(raw)
    pins = data.get("pins") or data.get("object", {}).get("pins", [])
    packages = []
    for pin in pins:
        state = pin.get("state", {})
        packages.append({
            "identity": pin.get("identity") or pin.get("package"),
            "location": pin.get("location") or pin.get("repositoryURL"),
            "version": state.get("version"),
            "branch": state.get("branch"),
            "revision": state.get("revision")
        })
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest(),
        "packages": sorted(packages, key=lambda item: item["identity"] or "")
    })

for path in glob.glob("**/Podfile.lock", recursive=True):
    with open(path, "rb") as handle:
        raw = handle.read()
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest()
    })

os.makedirs(".audit", exist_ok=True)
with open(".audit/dependency-manifest.json", "w") as handle:
    json.dump({"artifacts": items}, handle, indent=2, sort_keys=True)

Speichern Sie dependency-manifest.json nach der Ausführung als Build-Artefakt. Falls das Team die Datei in das Repository eincheckt, muss sie im selben Commit wie die aktualisierten Abhängigkeiten angepasst werden. Der Veröffentlichungsjob darf sie nicht unbemerkt überschreiben.

Differenzprüfung zur Veröffentlichungsschranke machen

Die Schranke sollte nicht pauschal jede Änderung verbieten. Stattdessen muss sie verlangen, dass Änderungen nachvollziehbar erklärt werden. Eine praktikable Reihenfolge ist:

  1. Prüfen, ob Deklarations- und Sperrdateien gemeinsam geändert wurden.
  2. Das isolierte Abhängigkeitsverzeichnis leeren und die Auflösung erneut durchführen.
  3. Sicherstellen, dass die Auflösung keine Sperrdateien neu geschrieben hat.
  4. Ein neues Abhängigkeitsinventar erzeugen und mit der Baseline vergleichen.
  5. Bei neuen Abhängigkeiten Lizenz, Wartungsquelle und die Möglichkeit zur Skriptausführung prüfen.
  6. Nach Kompilierung und Tests das Inventar, die Hashes der Sperrdateien und die Toolchain-Versionen speichern.

Als erste maschinelle Schranke eignen sich die üblichen Werkzeuge für Differenzvergleiche:

python3 tools/dependency_manifest.py
diff -u audit-baseline/dependency-manifest.json .audit/dependency-manifest.json

Wenn eine Aktualisierung zulässig ist, darf die Schranke nicht einfach deaktiviert werden. Committen Sie stattdessen die neue Baseline in einem separaten Branch. Bei der Prüfung sollte besonders auf neue Repository-Adressen, den Wechsel von einer Version zu einem Branch, geänderte Revisions-Hashes und Abhängigkeiten ohne Versionsnummer geachtet werden.

Skriptphasen gesondert prüfen

Abhängigkeiten können während einer Buildphase Shell-Skripte ausführen. Prüfen Sie im Xcode-Projekt und in den generierten Abhängigkeitsprojekten, ob solche Skripte auf das Netzwerk zugreifen, Anmeldedaten außerhalb des Workspace lesen, das Quellcodeverzeichnis verändern oder Umgebungsvariablen in Protokolle schreiben. Das CI-Konto sollte ausschließlich die minimal erforderlichen Berechtigungen zum Auschecken, Auflösen und Erstellen des Builds erhalten.

Behandlung von Abweichungen und Checkliste für die Auslieferung

Wenn sich die Sperrdatei nicht geändert hat, der Hash des Abhängigkeitsinventars jedoch abweicht, stoppen Sie zuerst die Veröffentlichung. Bewahren Sie anschließend den Workspace, die Auflösungsprotokolle und das Downloadverzeichnis auf. Prüfen Sie danach, ob Dateien neu erzeugt wurden, ob eine Abhängigkeit einen veränderlichen Branch verwendet, ob Inhalte eines internen Mirrors ersetzt wurden oder ob sich die Version des Auflösungswerkzeugs geändert hat. Leeren Sie nicht zuerst den Cache, da dadurch die wertvollsten Informationen über den Zustand zum Zeitpunkt des Vorfalls verloren gehen.

Vor jeder regulären Auslieferung kann die folgende feste Checkliste verwendet werden:

  • Deklarationsdateien, Sperrdateien und Abhängigkeitsinventar gehören zum selben Commit.
  • Alle SwiftPM-Abhängigkeiten haben eine eindeutige Version oder eine unveränderliche Revision.
  • Das Auflösungsergebnis von CocoaPods stimmt mit Podfile.lock überein.
  • Neue Abhängigkeiten wurden hinsichtlich ihrer Lizenz und Skriptphasen geprüft.
  • Eine saubere Auflösung erzeugt keine Git-Differenzen.
  • Die Build-Artefakte sind mit dem Inventar-Hash und den Toolchain-Versionen verknüpft.
  • Abhängigkeitsskripte erhalten in CI keinen Zugriff auf nicht benötigte Token oder private Schlüssel.

Ziel eines Audits der Abhängigkeits-Lieferkette ist nicht, eine längere Liste zu erzeugen. Entscheidend ist ein reproduzierbares Urteil: Bei unveränderten Eingaben lässt sich das Ergebnis verifizieren, bei geänderten Eingaben wird die Veröffentlichung angehalten, und Prüfer können genau erkennen, an welcher Stelle die Änderung aufgetreten ist.

Häufig gestellte Fragen

Ersetzt eine versionierte Package.resolved-Datei das Abhängigkeitsinventar?

Nein. Die Sperrdatei fixiert die Auflösung, während das Inventar Namen, Versionen, Revisionen, Quellen und Hashes für Vergleich und Archivierung vereinheitlicht.

Wann sollte die Abhängigkeitsprüfung im Build-Ablauf laufen?

Nach einer sauberen Auflösung, aber vor dem vollständigen Xcode-Build. Dadurch werden Abweichungen erkannt, bevor Skriptphasen des Projekts ausgeführt werden.

Wie behandelt man eine unerwartet geänderte transitive Abhängigkeit?

Die Veröffentlichung wird angehalten. Danach werden übergeordnete Abhängigkeit, Revision und Lizenz geprüft und der neue Graph in einem isolierten Branch getestet.

ZoomMini Cloud-Mac

Wählen Sie dedizierte physische Knoten für Builds, Tests und Experimente

Zwei M4-Konfigurationen sind an Knotenstandorten in Singapur, Tokio, Seoul und Hongkong verfügbar. Maßgeblich ist der bei der Bestellung angezeigte aktuelle Verfügbarkeitsstatus.

Modell auswählen und bestellen