MiniDebug 工程筆記

在雲端 Mac 自動化 Unity iOS 輸出與 Xcode 建置驗收

在雲端 Mac 自動化 Unity iOS 輸出與 Xcode 建置驗收

Unity 專案在本機可以順利匯出 iOS 工程,移到雲端 Mac 後卻可能卡在資源匯入、原生相依套件或連結階段。最棘手的不是建置失敗本身,而是所有步驟都塞進同一個命令:日誌只留下「建置失敗」,無法判斷應該重新執行 Unity、清除快取,還是檢查 Xcode 工程。更穩妥的做法是將流水線拆成兩個獨立關卡:先產生可稽核的 Xcode 工程,再執行不含簽署動作的原生編譯驗收。

先定義輸入與產物邊界

一次可重現的建置至少要固定四類輸入:提交版本、Unity 編輯器版本、套件相依鎖定檔,以及場景清單。不要讓指令碼臨時讀取某位開發者電腦上的編輯器設定,也不要直接覆寫上一次匯出的 Xcode 工程後繼續編譯。

建議為每次任務準備獨立目錄:

路徑 用途 是否可快取
Source/ 目前提交版本的 Unity 工程
Library/ 資源匯入結果 有條件
Build/iOS/ 本次匯出的 Xcode 工程
.build/DerivedData/ 原生編譯中間檔案 有條件
Artifacts/ 日誌、摘要與驗收結果

Library 的快取鍵應包含 Unity 版本、目標平台,以及 Packages/manifest.jsonPackages/packages-lock.json 的摘要。只要其中任一項發生變化,就應重新匯入。DerivedData 也不應直接在不同 Xcode 版本或不同工程設定之間重複使用。

快取只能加速輸入已固定的工作,不能取代版本鎖定。命中邊界不明確的快取,通常比完整重建更難除錯。

使用批次模式匯出 Xcode 工程

將匯出進入點放在 Assets/Editor/IosExport.cs,並根據建置設定中已啟用的項目產生場景清單,避免在指令碼與編輯器介面中維護兩份清單。

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.");
    }
}

執行時明確傳入專案目錄、日誌路徑與方法名稱:

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

任務開始前應刪除 Build/iOS,但不要無條件刪除 Library。任務結束後,應同時檢查結束碼、日誌檔案,以及 Build/iOS/Unity-iPhone.xcodeproj 是否存在。只看到目錄存在並不代表匯出成功,失敗的任務也可能留下未完成的產物。

分離原生相依套件與編譯驗收

Unity 匯出成功只代表產生器已完成工作,無法證明 Objective-C、Swift、IL2CPP 產物與原生程式庫能夠一起編譯。應先執行專案規定的原生相依套件安裝步驟,再判斷要建置 workspace 還是 project。當相依套件工具產生 .xcworkspace 後,若仍繼續建置 .xcodeproj,通常會發生找不到模組或缺少連結程式庫的錯誤。

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

這裡的目標是驗證編譯與連結,而不是產生可供散佈的安裝套件。簽署應安排在後續受控階段,避免一般提交驗證任務接觸不必要的敏感資料。

讓快取依規則失效,而不是靠猜測

Unity 專案常見的錯誤做法,是只用分支名稱作為快取鍵。兩個提交位於同一個分支,並不代表資源匯入結果相同。可以先產生輸入摘要:

{
  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

不要快取整個工作目錄

將整個工程目錄封裝成快取,會混入舊的匯出產物、暫存設定與已刪除檔案。應分別管理 LibraryDerivedData,並在還原後繼續驗證輸入摘要。若出現無法解釋的編譯差異,應先保留失敗日誌,再使用空快取重新執行一次;如果空快取能夠成功,問題應歸類為快取邊界錯誤,而不是直接修改業務程式碼。

封存足以重現問題的最小證據集

建置通過後,不要只保留一個成功標記。至少應封存完整的 Unity 日誌、Xcode 輸出、提交雜湊、工具版本、快取鍵與匯出工程摘要。工程摘要可以記錄 scheme、設定與建置設定,但應排除憑證、權杖及本機私有路徑。

常見失敗可以依階段快速分流:

最後,將「匯出成功」與「原生編譯成功」設為兩個獨立狀態。前者失敗時不必執行 Xcode;後者失敗時,也不必反覆匯入全部資源。如此一來,流水線更容易重試,日誌更精簡,責任邊界也更清楚。

常見問題

為什麼不讓 Unity 一次產生最終安裝檔?

拆開後能分辨 Unity 腳本、資源匯入、原生相依套件與 Xcode 編譯問題;先做無簽署編譯,也能在使用受控憑據前驗證程式碼與連結結果。

Library 快取可以跨 Unity 版本共用嗎?

不建議。快取鍵至少要包含 Unity 編輯器版本、相依套件鎖定檔與目標平台,任一條件不同就應重新產生 Library。

什麼情況要使用 xcworkspace?

原生相依套件安裝後若產生 workspace,就應建置 xcworkspace;沒有 workspace 時才使用 xcodeproj,判斷時點應放在相依套件準備完成之後。

獨享實體 Mac

把下一次 iOS 建置放到 MiniDebug M4 上。

固定提供 M4、16GB RAM 與 256GB SSD,可按日、週、月或季租用,並可從新加坡、日本東京、韓國首爾、香港、美国東部五個節點中選擇。實際可用狀態以控制台即時回傳為準。

選擇節點並訂購