MessagePack-Csharp と MagicOnion のコード生成を自動化する

## はじめに

[MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp) および [MagicOnion](https://github.com/cysharp/MagicOnion) を IL2CPP 環境下で動かすには事前のコード生成が必要です。
これを必要なときに漏れなく自動実行されるように設定したいと思います。
実装は、 [AssetPostprocessor](https://docs.unity3d.com/ja/current/ScriptReference/AssetPostprocessor.html) を使ってスクリプトの更新時に自動でツールを動かす方針で行きます。

## MessagePack-Csharp
現状の生成ツールが Mac でうまく動かないので [新しいほう](https://github.com/neuecc/MessagePack-CSharp/pull/396) を使います。

```csharp:MessagePackPostprocessor.cs
using System;
using System.Diagnostics;
using System.Linq;
using UnityEditor;

public class MessagePackPostprocessor : AssetPostprocessor
{
    // 監視するディレクトリ
    private const string TargetDirectory = "Assets/ServerDefinition";

    private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        // TargetDirectory のファイルが更新されたか確認
        var files = new[] { importedAssets, deletedAssets, movedAssets, movedFromAssetPaths }.SelectMany(_ => _);
        if (files.All(x => !x.StartsWith(TargetDirectory))) return;

        // 外部プロセス起動
        var process = Process.Start(new ProcessStartInfo
        {
            FileName = "/path/to/mpc",
            Arguments = "-i Assets -o Assets/GeneratedScripts/GeneratedResolver.cs",
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        });
        // 標準出力
        UnityEngine.Debug.Log(process.StandardOutput.ReadToEnd());
        // 標準エラー出力
        var error = process.StandardError.ReadToEnd();
        if (!string.IsNullOrEmpty(error)) UnityEngine.Debug.LogError(error);
    }
}
```

上記を Assets/Editor あたりに入れておくと TargetDirectory が更新されたときに Assets/GeneratedScripts/GeneratedResolver.cs が生成されるようになります。
なお、外部プロセスを起動したときのカレントディレクトリは .sln があるディレクトリになるようです。

## MagicOnion

ディレクトリ構成を以下と仮定します。

- Client
 - Assets
     - ServerDefinition(共通コード)
- Server
 - ServerDefinition(共通コード)

共通コードの実体は Client/Assets/ServerDefinition に格納し、サーバ側からは .csproj に以下のように追記することで、クライアント側のコードを参照することにします。

```xml:ServerDefinition.csproj
<ItemGroup>
  <Compile Include="../../Client/Assets/ServerDefinition/**/*.cs" />
</ItemGroup>
```

現状の生成ツールは Unity の生成する .csproj を対象にすると動かないのですが、上記で作成したサーバ側の ServerDefinition.csproj を対象にするとうまく動きます。

ただし、Unity から Process を起動すると、環境変数の値がおかしいらしく『dotnet コマンドが見つからない』という旨のエラーを吐いて終了してしまうので、以下のように予め dotnet コマンドまでのパスを設定してやる必要があります。

```csharp
Environment.SetEnvironmentVariable("PATH", "/usr/local/share/dotnet");
```

実装本体は MessagePack-Csharp とほぼ同じなので省略します。

### 全体コード

自動ビルドをOn/Offする機能、手動ビルド機能を入れた全体コードが以下です。
諸々のパスが現プロジェクトのものになっているのでいい感じに書き換えてご使用ください。
なお、Macのみ対応です。

```csharp
using System;
using System.Diagnostics;
using System.Linq;
using UnityEditor;

public class MessagePackPostprocessor : AssetPostprocessor
{
    // 監視するディレクトリ
    private const string TargetDirectory = "Assets/ServerDefinition";

    // 自動ビルド設定のMenuPath
    private const string AutoBuildMenuPath = "Tools/MessagePack/Auto Build";

    /// <summary>
    /// 自動ビルドするかどうかを切り替える
    /// </summary>
    [MenuItem("Tools/MessagePack/Auto Build")]
    private static void ToggleAutoBuild()
    {
        Menu.SetChecked(AutoBuildMenuPath, !Menu.GetChecked(AutoBuildMenuPath));
    }

    /// <summary>
    /// アセットインポート時の処理
    /// </summary>
    private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        // 自動ビルドがOFFなら終了
        if (!Menu.GetChecked(AutoBuildMenuPath)) return;

        // TargetDirectory のファイルが更新されたか確認
        var files = new[] { importedAssets, deletedAssets, movedAssets, movedFromAssetPaths }.SelectMany(_ => _);
        if (files.Any(x => x.StartsWith("Assets/ServerDefinition"))) Generate();
    }

    /// <summary>
    /// 実処理
    /// </summary>
    [MenuItem("Tools/MessagePack/Build Resolvers")]
    private static void Generate()
    {
        // dotnet コマンドのパスを通す
        Environment.SetEnvironmentVariable("PATH", "/usr/local/share/dotnet");

        // MagicOnionCodeGenerator
        DoProcess(
            command: "../Tools/MagicOnionCodeGenerator/osx-x64/moc",
            arguments: "-i ../Server/ServerDefinition/ServerDefinition.csproj -o Assets/GeneratedScripts/MagicOnionResolver.cs"
        );

        // MessagePackCompiler
        DoProcess(
            command: "../Tools/MessagePackCompiler/osx-x64/mpc",
            arguments: "-i Assets -o Assets/GeneratedScripts/GeneratedResolver.cs"
        );
    }

    /// <summary>
    /// 外部プロセスを起動して、標準出力・標準エラー出力をログに表示する
    /// </summary>
    /// <param name="command">コマンド</param>
    /// <param name="arguments">引数</param>
    private static void DoProcess(string command, string arguments)
    {
        UnityEngine.Debug.Log($"コマンドを実行します: {command} {arguments}");
        var process = Process.Start(new ProcessStartInfo
        {
            FileName = command,
            Arguments = arguments,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
        });
        UnityEngine.Debug.Log(process.StandardOutput.ReadToEnd());
        var error = process.StandardError.ReadToEnd();
        if (!string.IsNullOrEmpty(error)) UnityEngine.Debug.LogError(error);
    }
}

```

### 他OSについて

諸々問題があるので調査中です…