MiniDebug エンジニアリングノート

クラウドMacでUnity iOS書き出しとXcodeビルド検証を自動化する

クラウドMacでUnity iOS書き出しとXcodeビルド検証を自動化する

UnityプロジェクトをローカルではiOS向けに書き出せても、クラウドMacへ移すとアセットの取り込み、ネイティブ依存関係、リンクのいずれかで止まることがあります。厄介なのは失敗そのものではなく、すべての処理を1つのコマンドに詰め込んでいることです。ログには「ビルド失敗」としか残らず、Unityを再実行すべきか、キャッシュを削除すべきか、Xcodeプロジェクトを調べるべきか判断できません。より確実なのは、パイプラインを独立した2つのチェックポイントに分ける方法です。まず監査可能なXcodeプロジェクトを生成し、その後、署名を行わずにネイティブコンパイルを検証します。

入力と成果物の境界を先に定義する

再現可能なビルドには、少なくともコミット、Unityエディターのバージョン、パッケージ依存関係のロックファイル、シーン一覧という4種類の入力を固定する必要があります。スクリプトから特定の開発者のマシンにあるエディター設定をその場で読み込ませたり、前回書き出したXcodeプロジェクトをそのまま上書きしてコンパイルを続行したりしないでください。

ジョブごとに専用ディレクトリを用意することを推奨します。

パス 用途 キャッシュ可否
Source/ 現在のコミットに対応するUnityプロジェクト 不可
Library/ アセット取り込み結果 条件付き
Build/iOS/ 今回書き出したXcodeプロジェクト 不可
.build/DerivedData/ ネイティブコンパイルの中間ファイル 条件付き
Artifacts/ ログ、サマリー、検証結果 不可

Libraryのキャッシュキーには、Unityのバージョン、ターゲットプラットフォーム、Packages/manifest.jsonPackages/packages-lock.jsonのダイジェストを含めます。いずれか1つでも変わった場合は、アセットを再度取り込んでください。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プロジェクトでよくある誤りは、ブランチ名だけをキャッシュキーにすることです。同じブランチ上の2つのコミットでも、アセットの取り込み結果が同じとは限りません。まず入力のダイジェストを生成します。

{
  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、構成、ビルド設定を記録できますが、資格情報、トークン、マシン固有の非公開パスは除外してください。

よくある失敗は、発生した工程に応じてすばやく切り分けられます。

最後に、「書き出し成功」と「ネイティブコンパイル成功」を独立した2つの状態として扱います。前者が失敗した場合はXcodeを実行する必要がなく、後者が失敗した場合も全アセットの取り込みを何度も繰り返す必要はありません。これによりパイプラインを再実行しやすくなり、ログが短くなり、各工程の責任範囲も明確になります。

よくある質問

Unityから最終パッケージまで一度に作らない理由は何ですか?

Unityスクリプトやアセット取り込みの問題と、ネイティブ依存関係、コンパイラ、リンカの問題を分離できるためです。署名なしビルドなら、管理された認証情報を使う前にコードを検証できます。

Libraryキャッシュを別のプロジェクトでも共有できますか?

無条件の共有は避けます。Unityのバージョン、依存関係のロックファイル、対象プラットフォームをキャッシュキーに含め、どれかが変わった場合はLibraryを再生成します。

xcworkspaceを使うのはどのような場合ですか?

ネイティブ依存関係の準備後にworkspaceが生成された場合はxcworkspaceを使います。生成されなかった場合だけxcodeprojを選び、判定は依存関係の準備後に行います。

専有物理Mac

次回のiOSビルドをMiniDebug M4で実行しましょう。

M4、16GB RAM、256GB SSDを標準搭載。日単位、週単位、月単位、四半期単位でレンタルでき、シンガポール、東京、日本、ソウル、韓国、香港、米国東部の5つのノードから選択できます。実際の利用可能状況はコンソールのリアルタイム表示をご確認ください。

ノードを選択して注文する