Engineering-Notizen von MiniDebug

Unity-iOS-Export und Xcode-Build auf einem Cloud-Mac automatisieren

Unity-iOS-Export und Xcode-Build auf einem Cloud-Mac automatisieren

Ein Unity-Projekt lässt sich lokal als iOS-Projekt exportieren, bleibt auf einem Cloud-Mac jedoch beim Asset-Import, bei nativen Abhängigkeiten oder beim Linken hängen. Das eigentliche Problem ist dabei weniger der Fehlschlag selbst als die Bündelung aller Schritte in einem einzigen Befehl: Im Protokoll steht nur „Build fehlgeschlagen“, ohne erkennen zu lassen, ob Unity erneut ausgeführt, der Cache geleert oder das Xcode-Projekt geprüft werden muss. Robuster ist eine Pipeline mit zwei unabhängigen Prüfstufen: Zuerst wird ein nachvollziehbares Xcode-Projekt erzeugt, anschließend folgt eine native Kompilierungsprüfung ohne Codesignierung.

Eingaben und Artefakte klar abgrenzen

Für einen reproduzierbaren Build müssen mindestens vier Arten von Eingaben festgeschrieben werden: Commit-Version, Unity-Editor-Version, Lockdatei der Paketabhängigkeiten und Szenenliste. Das Skript sollte weder kurzfristig Editor-Einstellungen vom Rechner eines Entwicklers einlesen noch das zuletzt exportierte Xcode-Projekt überschreiben und anschließend weiterkompilieren.

Für jeden Auftrag empfiehlt sich eine eigene Verzeichnisstruktur:

Pfad Zweck Cachefähig
Source/ Unity-Projekt des aktuellen Commits Nein
Library/ Ergebnisse des Asset-Imports Bedingt
Build/iOS/ Für diesen Auftrag exportiertes Xcode-Projekt Nein
.build/DerivedData/ Zwischendateien der nativen Kompilierung Bedingt
Artifacts/ Protokolle, Zusammenfassungen und Prüfergebnisse Nein

Der Cache-Schlüssel für Library sollte die Unity-Version, die Zielplattform sowie Prüfsummen von Packages/manifest.json und Packages/packages-lock.json enthalten. Ändert sich eine dieser Angaben, müssen die Assets erneut importiert werden. Auch DerivedData sollte nicht unverändert zwischen verschiedenen Xcode-Versionen oder unterschiedlichen Projekteinstellungen wiederverwendet werden.

Ein Cache kann nur fest definierte Eingaben beschleunigen; eine Versionsfixierung ersetzt er nicht. Ein Treffer in einem unklar abgegrenzten Cache ist meist schwieriger zu untersuchen als ein vollständiger Neuaufbau.

Xcode-Projekt im Batch-Modus exportieren

Der Export-Einstiegspunkt wird in Assets/Editor/IosExport.cs abgelegt. Die Szenenliste entsteht aus den in den Build Settings aktivierten Einträgen, damit nicht parallel je eine Liste im Skript und in der Editor-Oberfläche gepflegt werden muss.

using System;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;

public static class IosExport
{
    public static void Run()
    {
        var scenes = EditorBuildSettings.scenes
            .Where(scene => scene.enabled)
            .Select(scene => scene.path)
            .ToArray();

        if (scenes.Length == 0)
            throw new InvalidOperationException("No enabled scenes.");

        var options = new BuildPlayerOptions
        {
            scenes = scenes,
            locationPathName = "Build/iOS",
            target = BuildTarget.iOS,
            options = BuildOptions.CleanBuildCache
        };

        var report = BuildPipeline.BuildPlayer(options);
        if (report.summary.result != BuildResult.Succeeded)
            throw new InvalidOperationException("Unity export failed.");
    }
}

Beim Aufruf werden Projektverzeichnis, Protokollpfad und Methodenname explizit übergeben:

"$UNITY_EDITOR" \
  -batchmode \
  -nographics \
  -quit \
  -projectPath "$PWD" \
  -executeMethod IosExport.Run \
  -logFile "$PWD/Artifacts/unity-export.log"

Vor Beginn des Auftrags sollte Build/iOS gelöscht werden, Library jedoch nicht pauschal. Nach Abschluss müssen Exit-Code, Protokolldatei und Build/iOS/Unity-iPhone.xcodeproj gemeinsam geprüft werden. Ein vorhandenes Verzeichnis allein belegt keinen erfolgreichen Export, da auch fehlgeschlagene Aufträge unvollständige Artefakte hinterlassen können.

Native Abhängigkeiten und Kompilierungsprüfung trennen

Ein erfolgreicher Unity-Export bestätigt lediglich, dass der Generator seine Arbeit abgeschlossen hat. Er beweist nicht, dass Objective-C, Swift, IL2CPP-Artefakte und native Bibliotheken gemeinsam kompiliert werden können. Führen Sie zunächst den projektspezifischen Installationsschritt für native Abhängigkeiten aus und entscheiden Sie danach, ob der Workspace oder das Projekt gebaut werden muss. Hat das Abhängigkeitswerkzeug eine .xcworkspace erzeugt, führt das weitere Bauen der .xcodeproj häufig zu nicht gefundenen Modulen oder fehlenden Bibliotheken beim Linken.

set -euo pipefail

cd Build/iOS
rm -rf ../../.build/DerivedData

if [ -d "Unity-iPhone.xcworkspace" ]; then
  xcodebuild \
    -workspace Unity-iPhone.xcworkspace \
    -scheme Unity-iPhone \
    -configuration Release \
    -sdk iphoneos \
    -derivedDataPath ../../.build/DerivedData \
    CODE_SIGNING_ALLOWED=NO \
    build
else
  xcodebuild \
    -project Unity-iPhone.xcodeproj \
    -scheme Unity-iPhone \
    -configuration Release \
    -sdk iphoneos \
    -derivedDataPath ../../.build/DerivedData \
    CODE_SIGNING_ALLOWED=NO \
    build
fi

Ziel dieses Schritts ist die Prüfung von Kompilierung und Linker-Lauf, nicht die Erzeugung eines verteilbaren Installationspakets. Die Signierung gehört in eine nachgelagerte, kontrollierte Phase, damit gewöhnliche Commit-Prüfungen keinen Zugriff auf unnötige sensible Materialien benötigen.

Cache-Invalidierung statt Rätselraten

Ein häufiger Fehler bei Unity-Projekten besteht darin, ausschließlich den Branch-Namen als Cache-Schlüssel zu verwenden. Zwei Commits im selben Branch müssen jedoch nicht dieselben Asset-Import-Ergebnisse erzeugen. Zunächst lässt sich eine Prüfsumme der Eingaben erstellen:

{
  printf '%s
' "$UNITY_VERSION"
  printf '%s
' "$XCODE_VERSION"
  shasum -a 256 Packages/manifest.json
  shasum -a 256 Packages/packages-lock.json
  find Assets -name '*.asmdef' -o -name '*.rsp' | sort | xargs shasum -a 256
} | shasum -a 256 | awk '{print $1}' > Artifacts/cache-key.txt

Nicht das gesamte Arbeitsverzeichnis cachen

Wird das komplette Projektverzeichnis als Cache gepackt, gelangen alte Exportartefakte, temporäre Einstellungen und bereits gelöschte Dateien hinein. Library und DerivedData sollten getrennt verwaltet werden; nach der Wiederherstellung ist die Prüfsumme der Eingaben weiterhin zu validieren. Treten nicht erklärbare Unterschiede bei der Kompilierung auf, sichern Sie zuerst die Fehlerprotokolle und wiederholen Sie den Build einmal mit leerem Cache. Ist dieser Durchlauf erfolgreich, liegt das Problem an der Cache-Abgrenzung und sollte nicht vorschnell durch Änderungen am Anwendungscode behoben werden.

Minimalen Beweissatz für spätere Analysen archivieren

Nach einem erfolgreichen Build genügt es nicht, nur eine Erfolgsmeldung aufzubewahren. Archivieren Sie mindestens das vollständige Unity-Protokoll, die Xcode-Ausgabe, den Commit-Hash, die Werkzeugversionen, den Cache-Schlüssel und eine Zusammenfassung des exportierten Projekts. Die Projektzusammenfassung kann Scheme, Konfiguration und Build-Einstellungen enthalten, sollte aber Anmeldedaten, Token und private lokale Pfade ausschließen.

Typische Fehler lassen sich anhand der jeweiligen Phase schnell einordnen:

Definieren Sie abschließend „Export erfolgreich“ und „native Kompilierung erfolgreich“ als zwei unabhängige Zustände. Schlägt der erste fehl, muss Xcode nicht gestartet werden. Schlägt der zweite fehl, müssen nicht wiederholt sämtliche Assets importiert werden. Dadurch lässt sich die Pipeline gezielter neu ausführen, die Protokolle bleiben kürzer und die Zuständigkeiten sind klarer abgegrenzt.

Häufig gestellte Fragen

Warum sollte Unity nicht direkt das endgültige Paket erzeugen?

Getrennte Stufen unterscheiden Unity-Skript- und Importfehler von Problemen mit nativen Abhängigkeiten, Compiler oder Linker. Ein unsignierter Build prüft den Code, bevor kontrollierte Signaturdaten eingesetzt werden.

Darf das Library-Verzeichnis zwischen Projekten geteilt werden?

Nicht ohne passenden Cache-Schlüssel. Dieser muss Unity-Version, Abhängigkeits-Sperrdateien und Zielplattform enthalten; bei einer Abweichung wird Library neu erzeugt.

Wann muss ein xcworkspace gebaut werden?

Wenn die Vorbereitung nativer Abhängigkeiten einen Workspace erzeugt, wird xcworkspace gebaut. Nur ohne Workspace kommt xcodeproj zum Einsatz; die Prüfung erfolgt nach der Abhängigkeitsinstallation.

Exklusiver physischer Mac

Den nächsten iOS-Build auf einem MiniDebug M4 ausführen.

M4, 16 GB RAM und 256 GB SSD sind standardmäßig verfügbar. Die Miete ist tage-, wochen-, monats- oder quartalsweise möglich. Zur Auswahl stehen fünf Standorte: Singapur, Tokio in Japan, Seoul in Südkorea, Hongkong und die US-Ostküste. Die tatsächliche Verfügbarkeit wird in Echtzeit von der Konsole angezeigt.

Standort auswählen und bestellen